Skip to content

feat: git notes based AI attribution (git ai log/blame/inspect)#22

Merged
naheel0 merged 5 commits intomainfrom
feat/git-ai-attribution
Apr 15, 2026
Merged

feat: git notes based AI attribution (git ai log/blame/inspect)#22
naheel0 merged 5 commits intomainfrom
feat/git-ai-attribution

Conversation

@naheel0
Copy link
Copy Markdown
Member

@naheel0 naheel0 commented Apr 14, 2026

Summary

  • Add AI attribution storage using git notes (refs/notes/git-ai) with a versioned schema and minimal per-commit index.
  • Add git ai subcommands: log, blame, inspect, record, export/import, explore, explain, validate, scan, notes.
  • Add tests for notes roundtrip and notes rewrite-on-amend.

Why

  • Git notes keep metadata separate from source but still versioned and shareable.
  • notes.rewriteRef preserves notes across rebases/cherry-picks/amends.
  • Line-hash anchors enable best-effort correlation through refactors.

Usage

  • git ai record --intent "refactor" --prompt "simplify parser" --path src/parser.ts --lines-from-file
  • git ai log -n 200 --model gemini-1.5-flash
  • git ai blame src/parser.ts
  • git ai inspect <id> --commit HEAD
  • git ai notes

Notes

  • Share notes: git push origin refs/notes/git-ai and git fetch origin refs/notes/git-ai:refs/notes/git-ai
  • Preserve on rewrites: git config --add notes.rewriteRef refs/notes/git-ai

Summary by CodeRabbit

  • New Features

    • Full AI attribution CLI: record, log, blame, inspect, explain, export, import, validate, scan, interactive explore, and notes; mode-aware startup (git-ai vs ai-git) and a tree command in standalone mode. Init optionally prompts for a GitHub token.
  • Documentation

    • Added comprehensive README with installation, command usage, attribution workflow, notes sharing, and import/export instructions.
  • Chores

    • Packaging, license, CI matrix, and build/test script updates.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests Changes to test files or test infrastructure labels Apr 14, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 99f35439-5cf9-4a38-89f9-861679387e91

📥 Commits

Reviewing files that changed from the base of the PR and between 48f71e2 and 19da44f.

📒 Files selected for processing (8)
  • .gitignore
  • git-ai/LICENSE
  • git-ai/package.json
  • git-ai/scripts/clean.mjs
  • git-ai/scripts/run-tests.mjs
  • git-ai/src/commands/ai/AiLogCommand.ts
  • git-ai/tsconfig.build.json
  • git-ai/tsconfig.test.json
✅ Files skipped from review due to trivial changes (4)
  • git-ai/LICENSE
  • .gitignore
  • git-ai/tsconfig.test.json
  • git-ai/tsconfig.build.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • git-ai/package.json
  • git-ai/src/commands/ai/AiLogCommand.ts
  • git-ai/scripts/run-tests.mjs

📝 Walkthrough

Walkthrough

Adds a full AI-attribution subsystem: schemas, hashing, attribution builder, git-notes persistence, CLI commands (record/log/blame/inspect/export/import/explain/scan/validate/interactive/notes), an Ink explorer UI, tests, GitService helpers, and dual-mode CLI wiring for git-ai vs ai-git.

Changes

Cohort / File(s) Summary
Schemas & Line-hashing
git-ai/src/ai/schema.ts, git-ai/src/ai/line-hash.ts
Add Zod schemas/types for attributions and index entries; line normalization and SHA-256 hashing utilities for anchors.
Attribution builder
git-ai/src/ai/attribution.ts
New AttributionService + AttributionInput to construct versioned AiAttribution records (id, createdAt, commit/tree, anchors).
Notes storage
git-ai/src/ai/notes-store.ts
New AiNotesStore persisting per-commit JSON payloads in refs/notes/git-ai with upsert, index rebuild, read/validate, list/get APIs.
Query/filtering
git-ai/src/ai/query.ts
Add AiFilter type and matchesFilter predicate for filtering attribution entries.
AI CLI commands
git-ai/src/commands/ai/..., git-ai/src/commands/ai/index.ts
Add many ai subcommands (record, log, blame, inspect, export, import, explain, scan, validate, notes, interactive) and aggregator wiring with help about refs/notes/git-ai.
Command implementations
git-ai/src/commands/ai/AiBlameCommand.ts, .../AiExplainCommand.ts, .../AiExportCommand.ts, .../AiImportCommand.ts, .../AiInspectCommand.ts, .../AiInteractiveCommand.ts, .../AiScanCommand.ts, .../AiValidateCommand.ts
Implement blame-style annotations, explain (AIService call), JSONL export/import, inspect, interactive explorer, heuristic scan, and validation flows with error handling and exit codes.
Record & notes helpers
git-ai/src/commands/ai/AiRecordCommand.ts, git-ai/src/commands/ai/AiNotesCommand.ts
ai record builds/persists attributions (optional lines-from-file anchoring); ai notes prints ref sharing commands and remote checks.
Tests & test runner
git-ai/src/ai/__tests__/notes-store.test.ts, git-ai/src/ai/__tests__/notes-rewrite.test.ts, git-ai/scripts/run-tests.mjs
Add end-to-end tests for notes store and notes-rewrite on amend; add script to discover/run compiled .test.js files.
Core Git service
git-ai/src/core/GitService.ts
Add raw, rawQuiet, and getRemotes methods and redact sensitive git-arg logging; improve error logging.
CLI entrypoint & package
git-ai/src/index.ts, git-ai/package.json
Detect invoked binary name to support git-ai (AI-only) vs ai-git (standalone) modes; add git-ai bin entry and adjust build/test scripts and package files.
Command adjustments
git-ai/src/commands/InitCommand.ts, git-ai/src/commands/ResolveCommand.ts, git-ai/src/cli/pr-command.ts
Init: non-TTY handling, optional GitHub PAT prompt, cleanup and error message tweaks; Resolve: re-export implementation from CLI; pr-command uses public getRemotes().
Interactive UI
git-ai/src/ui/AiExplorer.tsx, git-ai/src/ui/PRList.tsx, git-ai/src/ui/TreeUI.tsx
Add Ink AiExplorer component to browse attributions; minor loading/error text changes.
Build, test, config & tooling
git-ai/tsconfig.build.json, git-ai/tsconfig.test.json, git-ai/tsconfig.json, .gitignore, git-ai/scripts/clean.mjs, git-ai/scripts/run-tests.mjs, .github/workflows/ci.yml
Add build/test tsconfigs, include test files in main tsconfig, ignore dist-test, add clean/run-tests scripts, update CI Node matrix to 20/22.
Docs & license
git-ai/README.md, git-ai/LICENSE
Add README documenting installation, commands, and attribution workflow; add MIT license file.
Package cleanup
git-ai/package.json
Add files whitelist, new bin mapping (git-ai./dist/index.js), and refined build/test/clean scripts.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI as CLI
    participant GitSvc as GitService
    participant AttrSvc as AttributionService
    participant NotesStore as AiNotesStore
    participant Git as git

    User->>CLI: git ai record --commit SHA --intent "..." --prompt "..." --path file.ts
    CLI->>GitSvc: raw(['rev-parse', commit])
    GitSvc->>Git: git rev-parse
    Git-->>GitSvc: resolved SHA

    CLI->>GitSvc: raw(['show', 'SHA:path'])    %% optional lines-from-file
    GitSvc->>Git: git show
    Git-->>GitSvc: file contents

    CLI->>AttrSvc: buildRecord(commit, input)
    AttrSvc->>GitSvc: raw(['show','-s','--format=%T',commit])
    GitSvc->>Git: git show (tree)
    Git-->>GitSvc: tree hash
    AttrSvc-->>CLI: AiAttribution {id, commit, tree, anchors, ...}

    CLI->>NotesStore: upsertAttribution(record)
    NotesStore->>GitSvc: raw(['notes','--ref','refs/notes/git-ai','show',commit])
    GitSvc->>Git: git notes show
    Git-->>GitSvc: note JSON or error
    NotesStore->>NotesStore: merge index & records
    NotesStore->>GitSvc: raw(['notes','--ref','refs/notes/git-ai','add','-f','-m',payload])
    GitSvc->>Git: git notes add
    Git-->>GitSvc: success
    NotesStore-->>CLI: success
    CLI-->>User: ✓ Record saved (id)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • jaseel0

"🐰
I hop and nibble lines of code,
Stamping notes where AI seeds grow,
Records, blame, and logs take flight,
Tiny hashes in the night,
A carrot, a commit, and glow 🥕"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main feature: adding git notes-based AI attribution with log/blame/inspect commands.
Description check ✅ Passed The PR description provides a clear summary, rationale, and usage examples. However, it omits several template sections: Related Issue link, Type of Change checkbox, Checklist items, How Has This Been Tested, and Screenshots.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/git-ai-attribution

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (17)
git-ai/src/commands/ai/index.ts (1)

30-39: Surface rewrite preservation in the top-level help too.

This block already documents how to share refs/notes/git-ai; adding git config --add notes.rewriteRef refs/notes/git-ai here would also cover the setup users need to keep notes across amend/rebase flows.

✏️ Suggested help text
   ai.addHelpText(
     'after',
     [
       '',
       'Notes storage:',
       '  This tool stores metadata in `git notes --ref refs/notes/git-ai`.',
+      '  Preserve notes across rebases/amends with:',
+      '    git config --add notes.rewriteRef refs/notes/git-ai',
       '  Share notes with teammates by pushing/fetching that ref:',
       '    git push origin refs/notes/git-ai',
       '    git fetch origin refs/notes/git-ai:refs/notes/git-ai',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git-ai/src/commands/ai/index.ts` around lines 30 - 39, The top-level help
added via ai.addHelpText should mention preserving notes across rewrites; update
the message array passed to ai.addHelpText to include a line advising users to
run "git config --add notes.rewriteRef refs/notes/git-ai" (or equivalent) so
`refs/notes/git-ai` is automatically rewritten during amend/rebase flows; edit
the string block in the ai.addHelpText call to add that single-line instruction
near the notes/sharing lines so users see setup and preservation together.
git-ai/tsconfig.json (1)

33-33: Consider a separate test tsconfig.

Dropping the test-file exclusion makes the main tsc build emit compiled tests into dist so the new package.json test flow works, but it also couples npm run build/prepare to test-only code and ships test artifacts with the release build. A tsconfig.build.json + tsconfig.test.json split would keep the workflow without bloating dist.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git-ai/tsconfig.json` at line 33, Replace the current single tsconfig setup
that removes "exclude": ["node_modules", "dist"] with a split config: create
tsconfig.build.json (extends the base tsconfig and keeps tests excluded so tsc
--build/--project for production builds doesn't emit test artifacts) and
tsconfig.test.json (extends base but includes test files for the test flow).
Update the build/test npm scripts to point to the appropriate config (e.g., tsc
-p tsconfig.build.json for build/prepare and tsc -p tsconfig.test.json or your
test runner compile step for tests) and ensure the base tsconfig or the original
"exclude" remains as the canonical source for editor tooling.
git-ai/src/commands/ai/AiLogCommand.ts (1)

67-73: Sequential note fetching may be slow for large limits.

Each commit triggers a separate git notes show call. For large --limit values, this could be slow. Consider batching or parallelizing if performance becomes an issue, though it's acceptable for typical usage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git-ai/src/commands/ai/AiLogCommand.ts` around lines 67 - 73, The loop
fetches notes sequentially (for const commit of commits { await
store.listIndexForCommit(commit) }) which can be slow for large limits; change
to parallelize or batch the calls by mapping commits to promises and using
Promise.all (or processing in configurable batches) to call
store.listIndexForCommit concurrently, then flatten results and apply
matchesFilter to populate rows (keep references: commits,
store.listIndexForCommit, matchesFilter, rows).
git-ai/src/commands/ai/AiImportCommand.ts (1)

42-51: Consider logging line numbers for invalid entries.

When records fail to parse, users have no way to identify which lines are problematic for debugging or correction.

♻️ Suggested improvement
+      let lineNum = 0;
       for (const line of lines) {
+        lineNum++;
         try {
           const parsed = JSON.parse(line);
           const rec = AiAttributionSchema.parse(parsed);
           await store.upsertAttribution(rec);
           ok++;
         } catch {
+          console.warn(`Line ${lineNum}: invalid or unparseable`);
           bad++;
         }
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git-ai/src/commands/ai/AiImportCommand.ts` around lines 42 - 51, When
iterating lines in AiImportCommand (the for loop that calls JSON.parse and
AiAttributionSchema.parse and upserts via store.upsertAttribution), update the
catch to log which line index/text failed and include the parsing error;
specifically capture the current index (or line number) and the caught error,
and write a clear processLogger or console.error message referencing the failing
line, the index, and the error before incrementing bad so users can locate and
fix invalid entries.
git-ai/src/index.ts (1)

31-44: Version string is duplicated.

The version 1.0.0 appears in both the banner (line 31) and .version() (line 44). Consider extracting to a constant to avoid drift.

♻️ Suggested improvement
+const VERSION = '1.0.0';
+
 if (process.stdout.isTTY) {
   const banner = `
-  ${chalk.bold.magenta('●')} ${chalk.bold('GIT-AI')} ${chalk.dim('v1.0.0')}
+  ${chalk.bold.magenta('●')} ${chalk.bold('GIT-AI')} ${chalk.dim(`v${VERSION}`)}
   ${chalk.dim('————————————————————————————————')}
 `;
   console.log(banner);
 }

 program
   .name(nameFromBin)
   .description(...)
-  .version('1.0.0');
+  .version(VERSION);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git-ai/src/index.ts` around lines 31 - 44, Extract the duplicated version
string into a single constant (e.g., VERSION) and use that constant both when
building the banner (the variable banner) and when calling program.version() so
the value is defined in one place; update any references (banner construction
and program.version) to reference the new VERSION constant to prevent drift.
git-ai/src/commands/ai/AiScanCommand.ts (1)

48-54: Consider reporting all matches per rule, not just the first.

Currently diff.match(rule.pattern) returns only the first match. For a thorough scan, users may want to see all occurrences. Using matchAll with the global flag would capture repeated violations.

♻️ Suggested enhancement for multiple matches
+// Add 'g' flag to patterns in RISK_RULES, e.g.:
+// { id: 'eval', pattern: /\beval\s*\(/g, message: '...' },

       const hits: { rule: string; message: string; sample: string }[] = [];
       for (const rule of RISK_RULES) {
-        const m = diff.match(rule.pattern);
-        if (m) {
-          hits.push({ rule: rule.id, message: rule.message, sample: m[0] });
-        }
+        const matches = [...diff.matchAll(rule.pattern)];
+        for (const m of matches) {
+          hits.push({ rule: rule.id, message: rule.message, sample: m[0] });
+        }
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git-ai/src/commands/ai/AiScanCommand.ts` around lines 48 - 54, The scan only
captures the first match because it uses diff.match(rule.pattern); update the
loop over RISK_RULES in AiScanCommand to iterate over all matches: for each
rule, obtain a global RegExp (if rule.pattern isn’t already global, construct
one preserving flags plus 'g'), call diff.matchAll(globalPattern), and for each
match push a hit object (use rule.id, rule.message and the match text like m[0])
into the hits array so every occurrence is reported rather than only the first.
git-ai/src/commands/ai/AiNotesCommand.ts (1)

35-41: Consider using proper typing instead of any.

The any[] type and (r: any) reduce type safety. simple-git (or GitService) likely returns a typed remote object.

♻️ Suggested improvement
-      let remotes: any[] = [];
+      let remotes: { name: string; refs?: { fetch?: string; push?: string } }[] = [];
       try {
         remotes = await git.getRemotes(true);
       } catch {
         // ignore
       }
-      const found = remotes.find((r: any) => r.name === remote);
+      const found = remotes.find((r) => r.name === remote);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git-ai/src/commands/ai/AiNotesCommand.ts` around lines 35 - 41, The code uses
unsafe any for remotes and the find callback; change remotes: any[] to a
concrete remote type (e.g. RemoteWithRefs[] from simple-git or your project's
GitService remote type) and update the find callback to (r: RemoteWithRefs) =>
r.name === remote; if the project doesn't expose that type, add a small local
interface (e.g. interface GitRemote { name: string; refs?: Record<string,string>
}) and use GitRemote[] and (r: GitRemote) in the find. Also add the necessary
import for RemoteWithRefs (or declare the local interface) so the compiler can
enforce proper typing for git.getRemotes and the found variable.
git-ai/src/commands/ai/AiBlameCommand.ts (1)

89-96: First-match semantics may produce non-deterministic results.

Line 94 selects matches[0].id when multiple records claim the same line hash. Since the order depends on commit iteration and map insertion order, results could vary. Consider sorting candidates by createdAt or commit date for deterministic output.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git-ai/src/commands/ai/AiBlameCommand.ts` around lines 89 - 96, The current
annotation loop in AiBlameCommand uses the first element of matches
(matches[0].id) which can be nondeterministic when multiple records share a line
hash; update the logic in the loop that builds annotated (referencing
lineHashes, hashToRecord, matches) to deterministically pick a record by sorting
or selecting by a stable timestamp field (e.g., record.createdAt or commit date)
before taking the id, so recordId is derived from the earliest/desired
createdAt-sorted candidate rather than insertion order.
git-ai/src/commands/ai/AiValidateCommand.ts (2)

50-61: Redundant validation: getRecord already validates internally.

Per notes-store.ts:105-112, getRecord returns null when AiAttributionSchema.safeParse fails. The second safeParse on line 56 will always succeed if rec is non-null.

If the intent is to catch schema drift between index and full record, the current approach is fine as defensive coding. Otherwise, you could simplify by trusting getRecord's internal validation.

♻️ Optional simplification
         for (const entry of idx) {
           const rec = await store.getRecord(c, entry.id);
           if (!rec) {
             bad++;
             console.error(`Invalid: missing record payload for ${entry.id} on ${c}`);
             continue;
           }
-          const parsed = AiAttributionSchema.safeParse(rec);
-          if (!parsed.success) {
-            bad++;
-            console.error(`Invalid: schema mismatch for ${entry.id} on ${c}`);
-            continue;
-          }
           ok++;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git-ai/src/commands/ai/AiValidateCommand.ts` around lines 50 - 61, The second
schema check is redundant because store.getRecord already returns null on schema
failure; remove the AiAttributionSchema.safeParse block (the parsed variable and
its check) from AiValidateCommand.ts and rely on getRecord's null check (rec) to
mark bad and log the error; keep the existing error message for missing/invalid
payloads and continue the loop, ensuring no other logic depends on parsed being
present.

10-14: Consider extracting toNum to a shared utility.

This helper is duplicated across AiValidateCommand, AiBlameCommand, AiExportCommand, and AiLogCommand. Extracting it to a shared module (e.g., utils/parse.ts) would reduce duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git-ai/src/commands/ai/AiValidateCommand.ts` around lines 10 - 14, The helper
function toNum is duplicated across AiValidateCommand, AiBlameCommand,
AiExportCommand and AiLogCommand; extract it into a single shared parse utility
module (exporting function toNum(value: string | undefined, fallback: number):
number) and update each command to import and use that exported toNum instead of
the inline copy, removing the duplicated implementations and adjusting
imports/exports so TypeScript compiles cleanly.
git-ai/src/commands/ai/AiExportCommand.ts (1)

47-47: Consider using a static import for fs/promises.

The dynamic await import('fs/promises') is unusual for a Node.js built-in. A static import at the top of the file would be cleaner and has negligible overhead.

♻️ Use static import
 import { Command } from 'commander';
 import { GitService } from '../../core/GitService.js';
 import { AiNotesStore } from '../../ai/notes-store.js';
+import fs from 'fs/promises';

Then remove line 47 and use fs directly on line 59.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git-ai/src/commands/ai/AiExportCommand.ts` at line 47, Replace the dynamic
import of fs/promises in the AiExportCommand module with a static top-level
import and use that exported binding where fs is referenced: remove the await
import('fs/promises') statement and add a static import for fs/promises at the
top of the file so functions like the code that writes the exported file in
AiExportCommand (the place currently using the dynamically imported fs) call the
imported fs methods directly.
git-ai/src/commands/InitCommand.ts (1)

134-136: Consider including the log file location in the error message.

The generic "Check logs for details" message is helpful, but users may not know where logs are stored. Consider including the log path or the actual error summary.

💡 Suggested improvement
   } catch (error) {
     logger.error('Init failed', error as any);
-    console.error('\n❌ Setup failed. Check logs for details.');
+    const msg = error instanceof Error ? error.message : 'Unknown error';
+    console.error(`\n❌ Setup failed: ${msg}`);
   } finally {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git-ai/src/commands/InitCommand.ts` around lines 134 - 136, The catch block
in InitCommand.ts currently logs the error via logger.error and prints a generic
console.error message; update the catch handler (the catch in the init flow
where logger.error(...) and console.error(...) are called) to include either the
actual error summary (e.g., error.message or a trimmed stack) and the log file
path in the console output so users know where to look; modify the logger.error
call to still record the full error and change the console.error line to print
"❌ Setup failed: <short error message>. See logs at <logPath>" (use the existing
log path/config variable used by your app).
git-ai/src/ai/schema.ts (1)

24-26: Consider stricter validation for createdAt timestamp.

The createdAt field only validates that it's a non-empty string. For better data integrity, consider validating ISO 8601 format with z.string().datetime() or a custom refinement.

💡 Suggested improvement
   // Actors and time.
   author: z.string().min(1).optional(),
-  createdAt: z.string().min(1),
+  createdAt: z.string().datetime({ message: 'createdAt must be ISO 8601 format' }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git-ai/src/ai/schema.ts` around lines 24 - 26, The createdAt field currently
only ensures a non-empty string; update its Zod validation to enforce an ISO
8601 timestamp by replacing z.string().min(1) for the createdAt property with
z.string().datetime() (or, if you need timezone control, use z.string().refine(s
=> !Number.isNaN(Date.parse(s)), { message: 'createdAt must be a valid ISO 8601
datetime' })) so the schema's createdAt property is strictly validated; adjust
any tests or consumers expecting the looser format accordingly.
git-ai/src/commands/ai/AiExplainCommand.ts (2)

60-69: Consider using a typed array instead of any[].

Using any[] loses type safety for the records being collected. Since store.getRecord() returns AiAttribution | null, consider typing this more precisely.

♻️ Suggested improvement
-      const records: any[] = [];
+      const records: Array<AiAttribution & { noteCommit: string }> = [];

You'll need to import AiAttribution from the schema:

 import { AiNotesStore } from '../../ai/notes-store.js';
+import { AiAttribution } from '../../ai/schema.js';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git-ai/src/commands/ai/AiExplainCommand.ts` around lines 60 - 69, The records
array is typed as any[], losing type safety; change it to a properly typed array
such as (AiAttribution & { noteCommit: typeof c })[] (or replace typeof c with
the actual commit id type) and import AiAttribution from your schema; update the
declaration of records and any related usages so that records.push({ noteCommit:
c, ...rec }) only accepts non-null rec (guard rec's nullability or narrow its
type after the await from store.getRecord), and ensure store.getRecord is
treated as returning AiAttribution | null in the loop that iterates commits and
calls store.listIndexForCommit and store.getRecord.

89-104: Consider token limits for large files or many records.

The prompt includes the full file content and all attribution records (up to 200 commits worth). For large files or extensive attribution history, this could exceed model token limits and lead to truncation or API errors.

For now this is acceptable, but consider adding content truncation or summarization for future robustness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git-ai/src/commands/ai/AiExplainCommand.ts` around lines 89 - 104, The prompt
assembly in AiExplainCommand.ts (the prompt variable built from records and
fileContent) can exceed model token limits for large files or many AI
attribution records; update the logic that prepares records and fileContent
before JSON.stringify and concatenation to truncate or summarize them (e.g.,
keep only the most recent N records from records, or collapse records to a brief
summary, and trim fileContent to a token/character limit or generate a short
diff/summary) and/or add a lightweight token-estimation step to ensure the final
prompt fits model limits; apply these changes where prompt is constructed so
prompt uses the trimmed/summarized records and fileContent.
git-ai/src/ai/notes-store.ts (2)

100-112: Commit field override is intentional but may mask inconsistencies.

Both listIndexForCommit and getRecord override the stored commit field with the parameter. This ensures consistency but silently ignores any mismatch between the stored commit and the requested one. Consider logging a warning if they differ, which could indicate data corruption.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git-ai/src/ai/notes-store.ts` around lines 100 - 112, Both listIndexForCommit
and getRecord currently overwrite the stored commit with the provided commit
parameter, which can hide mismatches; after calling readCommitNote(check
readCommitNote) validate whether each entry's commit (for listIndexForCommit:
payload.index entries' commit; for getRecord: the stored rec.commit) differs
from the commit parameter and emit a warning via the class logger (e.g.,
this.logger.warn) naming the function (listIndexForCommit/getRecord), the id or
index entry info, the stored commit value and the requested commit; keep the
existing behavior of returning the result with commit set to the parameter but
add the warning when they differ to surface potential data corruption involving
AiIndexEntry and AiAttribution.

61-65: Large JSON payloads may exceed command-line argument limits.

Passing the serialized JSON via -m can fail if the payload grows large (many records). Most systems have an ARG_MAX limit (128KB–2MB). Consider using stdin via git notes add -F - for robustness.

♻️ Suggested improvement using stdin
   public async writeCommitNote(commit: string, payload: NotesPayload): Promise<void> {
     const serialized = JSON.stringify(payload);
-    // Replace existing note.
-    await this.git.raw(['notes', '--ref', NOTES_REF, 'add', '-f', '-m', serialized, commit]);
+    // Replace existing note using stdin to avoid ARG_MAX limits.
+    await this.git.rawWithStdin(['notes', '--ref', NOTES_REF, 'add', '-f', '-F', '-', commit], serialized);
   }

This requires adding a rawWithStdin method to GitService that pipes input to the command.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git-ai/src/ai/notes-store.ts` around lines 61 - 65, The current
writeCommitNote in NotesStore serializes payload and passes it with -m which can
hit ARG_MAX for large JSON; change writeCommitNote to call a new
GitService.rawWithStdin method that pipes serialized JSON to git via stdin and
uses git notes --ref NOTES_REF add -f -F - (or
['notes','--ref',NOTES_REF,'add','-f','-F','-']) instead of -m, and implement
rawWithStdin on GitService to spawn the git process, write the provided string
to stdin, handle stream/exit errors and return/throw on failures so
writeCommitNote can await it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@git-ai/src/ai/line-hash.ts`:
- Around line 12-15: The current hashLines implementation maps all input lines
through hashLine then filters hashes, which is ineffective because hashLine
always returns a digest; instead filter out blank/whitespace-only lines before
hashing so you don't produce SHA-256 of the empty string. Update hashLines to
first filter lines (e.g., line.trim().length > 0) and then map the remaining
lines with hashLine (referencing function hashLines and helper hashLine) so only
non-blank lines produce anchors.

In `@git-ai/src/commands/ai/AiExportCommand.ts`:
- Around line 59-60: The file write operation using fs.writeFile (writing
outFile with lines.join and lines.length) lacks error handling; wrap the await
fs.writeFile(...) call in a try/catch inside the same function (the
AiExportCommand export handler) and on error call console.error with a
user-friendly message that includes the outFile path and the error.message (or
error object), then exit or rethrow to surface failure (e.g., process.exit(1) or
throw) so write failures (permission, disk full) are handled gracefully.

In `@git-ai/src/commands/ai/AiImportCommand.ts`:
- Around line 54-56: The import command currently only warns when invalid lines
are skipped (in AiImportCommand where the code checks "if (bad > 0) {
console.warn(...); }") but does not set a non-zero exit status; update that
block to set a failure exit code (e.g., set process.exitCode = 1 or call
process.exit(1)) immediately after logging so CI/consumers detect the partial
failure. Ensure you modify the "if (bad > 0)" branch in AiImportCommand.ts (the
run/execute method handling import results) to both warn and set the non-zero
exit code.

In `@git-ai/src/commands/ai/AiInspectCommand.ts`:
- Around line 31-33: The recovery message in AiInspectCommand.ts currently
hardcodes "ai-git ai log"; update the two console.error lines that print the
recovery suggestion (the lines referencing id and resolved) to avoid the
hardcoded prefix—either compute the CLI prefix dynamically (e.g., derive from
process.argv[1] or a shared config/utility) and interpolate it into the
suggestion, or replace the hardcoded string with a generic, context-neutral
suggestion such as "git ai log" so the message matches how the user invoked the
command; ensure you modify the console.error that prints the recovery hint and
keep id/resolved interpolation intact.

In `@git-ai/src/core/GitService.ts`:
- Around line 12-18: The raw method is leaking value-bearing git arguments into
logs; update GitService.raw (around the this.git.raw call and logger.error) to
sanitize args before joining: detect flags that carry values (e.g., "-m", "-F")
and either remove their following argument or replace the flag's value with a
redaction token (e.g., "<REDACTED>"), and/or log only the subcommand (args[0])
plus sanitized args; then use the sanitizedArgs.join(' ') in the logger.error
call while keeping the original error thrown.

In `@git-ai/src/index.ts`:
- Around line 29-35: The banner always hardcodes "GIT-AI"; update the display
logic in the TTY banner block (the conditional using process.stdout.isTTY and
the banner constant) to compute the app name dynamically from the invocation
(e.g., derive a friendly name via path.basename(process.argv[1] or
process.argv[0]) and map/normalize it to "GIT-AI" or "ai-git" as appropriate),
then interpolate that computed name into the banner string instead of the
literal 'GIT-AI', and keep the rest of the banner formatting and console.log
call unchanged.

In `@git-ai/src/ui/AiExplorer.tsx`:
- Around line 53-56: The finally block currently uses "if (!alive) return;"
which returns from inside finally and can mask exceptions; change it to avoid
returning in finally by moving the alive check to guard the setLoading call
instead — e.g., replace the return pattern with "if (alive) setLoading(false);"
so the finally block simply conditionally calls setLoading based on the alive
flag (refer to the finally block that calls setLoading and the alive and
setLoading symbols in AiExplorer.tsx).
- Around line 117-125: The current slice rows.slice(0, 30) only shows the first
30 items while selected can be beyond that, so compute a sliding window around
selected (e.g., start = clamp(selected - 15, 0, Math.max(0, rows.length - 30))
and end = start + 30) and render rows.slice(start, end) instead; update the
isSelected test to compare (i + start) === selected and keep the same key and
Text rendering (references: rows.slice(0, 30), selected, Text, map callback) so
the highlighted item is always visible.

---

Nitpick comments:
In `@git-ai/src/ai/notes-store.ts`:
- Around line 100-112: Both listIndexForCommit and getRecord currently overwrite
the stored commit with the provided commit parameter, which can hide mismatches;
after calling readCommitNote(check readCommitNote) validate whether each entry's
commit (for listIndexForCommit: payload.index entries' commit; for getRecord:
the stored rec.commit) differs from the commit parameter and emit a warning via
the class logger (e.g., this.logger.warn) naming the function
(listIndexForCommit/getRecord), the id or index entry info, the stored commit
value and the requested commit; keep the existing behavior of returning the
result with commit set to the parameter but add the warning when they differ to
surface potential data corruption involving AiIndexEntry and AiAttribution.
- Around line 61-65: The current writeCommitNote in NotesStore serializes
payload and passes it with -m which can hit ARG_MAX for large JSON; change
writeCommitNote to call a new GitService.rawWithStdin method that pipes
serialized JSON to git via stdin and uses git notes --ref NOTES_REF add -f -F -
(or ['notes','--ref',NOTES_REF,'add','-f','-F','-']) instead of -m, and
implement rawWithStdin on GitService to spawn the git process, write the
provided string to stdin, handle stream/exit errors and return/throw on failures
so writeCommitNote can await it.

In `@git-ai/src/ai/schema.ts`:
- Around line 24-26: The createdAt field currently only ensures a non-empty
string; update its Zod validation to enforce an ISO 8601 timestamp by replacing
z.string().min(1) for the createdAt property with z.string().datetime() (or, if
you need timezone control, use z.string().refine(s =>
!Number.isNaN(Date.parse(s)), { message: 'createdAt must be a valid ISO 8601
datetime' })) so the schema's createdAt property is strictly validated; adjust
any tests or consumers expecting the looser format accordingly.

In `@git-ai/src/commands/ai/AiBlameCommand.ts`:
- Around line 89-96: The current annotation loop in AiBlameCommand uses the
first element of matches (matches[0].id) which can be nondeterministic when
multiple records share a line hash; update the logic in the loop that builds
annotated (referencing lineHashes, hashToRecord, matches) to deterministically
pick a record by sorting or selecting by a stable timestamp field (e.g.,
record.createdAt or commit date) before taking the id, so recordId is derived
from the earliest/desired createdAt-sorted candidate rather than insertion
order.

In `@git-ai/src/commands/ai/AiExplainCommand.ts`:
- Around line 60-69: The records array is typed as any[], losing type safety;
change it to a properly typed array such as (AiAttribution & { noteCommit:
typeof c })[] (or replace typeof c with the actual commit id type) and import
AiAttribution from your schema; update the declaration of records and any
related usages so that records.push({ noteCommit: c, ...rec }) only accepts
non-null rec (guard rec's nullability or narrow its type after the await from
store.getRecord), and ensure store.getRecord is treated as returning
AiAttribution | null in the loop that iterates commits and calls
store.listIndexForCommit and store.getRecord.
- Around line 89-104: The prompt assembly in AiExplainCommand.ts (the prompt
variable built from records and fileContent) can exceed model token limits for
large files or many AI attribution records; update the logic that prepares
records and fileContent before JSON.stringify and concatenation to truncate or
summarize them (e.g., keep only the most recent N records from records, or
collapse records to a brief summary, and trim fileContent to a token/character
limit or generate a short diff/summary) and/or add a lightweight
token-estimation step to ensure the final prompt fits model limits; apply these
changes where prompt is constructed so prompt uses the trimmed/summarized
records and fileContent.

In `@git-ai/src/commands/ai/AiExportCommand.ts`:
- Line 47: Replace the dynamic import of fs/promises in the AiExportCommand
module with a static top-level import and use that exported binding where fs is
referenced: remove the await import('fs/promises') statement and add a static
import for fs/promises at the top of the file so functions like the code that
writes the exported file in AiExportCommand (the place currently using the
dynamically imported fs) call the imported fs methods directly.

In `@git-ai/src/commands/ai/AiImportCommand.ts`:
- Around line 42-51: When iterating lines in AiImportCommand (the for loop that
calls JSON.parse and AiAttributionSchema.parse and upserts via
store.upsertAttribution), update the catch to log which line index/text failed
and include the parsing error; specifically capture the current index (or line
number) and the caught error, and write a clear processLogger or console.error
message referencing the failing line, the index, and the error before
incrementing bad so users can locate and fix invalid entries.

In `@git-ai/src/commands/ai/AiLogCommand.ts`:
- Around line 67-73: The loop fetches notes sequentially (for const commit of
commits { await store.listIndexForCommit(commit) }) which can be slow for large
limits; change to parallelize or batch the calls by mapping commits to promises
and using Promise.all (or processing in configurable batches) to call
store.listIndexForCommit concurrently, then flatten results and apply
matchesFilter to populate rows (keep references: commits,
store.listIndexForCommit, matchesFilter, rows).

In `@git-ai/src/commands/ai/AiNotesCommand.ts`:
- Around line 35-41: The code uses unsafe any for remotes and the find callback;
change remotes: any[] to a concrete remote type (e.g. RemoteWithRefs[] from
simple-git or your project's GitService remote type) and update the find
callback to (r: RemoteWithRefs) => r.name === remote; if the project doesn't
expose that type, add a small local interface (e.g. interface GitRemote { name:
string; refs?: Record<string,string> }) and use GitRemote[] and (r: GitRemote)
in the find. Also add the necessary import for RemoteWithRefs (or declare the
local interface) so the compiler can enforce proper typing for git.getRemotes
and the found variable.

In `@git-ai/src/commands/ai/AiScanCommand.ts`:
- Around line 48-54: The scan only captures the first match because it uses
diff.match(rule.pattern); update the loop over RISK_RULES in AiScanCommand to
iterate over all matches: for each rule, obtain a global RegExp (if rule.pattern
isn’t already global, construct one preserving flags plus 'g'), call
diff.matchAll(globalPattern), and for each match push a hit object (use rule.id,
rule.message and the match text like m[0]) into the hits array so every
occurrence is reported rather than only the first.

In `@git-ai/src/commands/ai/AiValidateCommand.ts`:
- Around line 50-61: The second schema check is redundant because
store.getRecord already returns null on schema failure; remove the
AiAttributionSchema.safeParse block (the parsed variable and its check) from
AiValidateCommand.ts and rely on getRecord's null check (rec) to mark bad and
log the error; keep the existing error message for missing/invalid payloads and
continue the loop, ensuring no other logic depends on parsed being present.
- Around line 10-14: The helper function toNum is duplicated across
AiValidateCommand, AiBlameCommand, AiExportCommand and AiLogCommand; extract it
into a single shared parse utility module (exporting function toNum(value:
string | undefined, fallback: number): number) and update each command to import
and use that exported toNum instead of the inline copy, removing the duplicated
implementations and adjusting imports/exports so TypeScript compiles cleanly.

In `@git-ai/src/commands/ai/index.ts`:
- Around line 30-39: The top-level help added via ai.addHelpText should mention
preserving notes across rewrites; update the message array passed to
ai.addHelpText to include a line advising users to run "git config --add
notes.rewriteRef refs/notes/git-ai" (or equivalent) so `refs/notes/git-ai` is
automatically rewritten during amend/rebase flows; edit the string block in the
ai.addHelpText call to add that single-line instruction near the notes/sharing
lines so users see setup and preservation together.

In `@git-ai/src/commands/InitCommand.ts`:
- Around line 134-136: The catch block in InitCommand.ts currently logs the
error via logger.error and prints a generic console.error message; update the
catch handler (the catch in the init flow where logger.error(...) and
console.error(...) are called) to include either the actual error summary (e.g.,
error.message or a trimmed stack) and the log file path in the console output so
users know where to look; modify the logger.error call to still record the full
error and change the console.error line to print "❌ Setup failed: <short error
message>. See logs at <logPath>" (use the existing log path/config variable used
by your app).

In `@git-ai/src/index.ts`:
- Around line 31-44: Extract the duplicated version string into a single
constant (e.g., VERSION) and use that constant both when building the banner
(the variable banner) and when calling program.version() so the value is defined
in one place; update any references (banner construction and program.version) to
reference the new VERSION constant to prevent drift.

In `@git-ai/tsconfig.json`:
- Line 33: Replace the current single tsconfig setup that removes "exclude":
["node_modules", "dist"] with a split config: create tsconfig.build.json
(extends the base tsconfig and keeps tests excluded so tsc --build/--project for
production builds doesn't emit test artifacts) and tsconfig.test.json (extends
base but includes test files for the test flow). Update the build/test npm
scripts to point to the appropriate config (e.g., tsc -p tsconfig.build.json for
build/prepare and tsc -p tsconfig.test.json or your test runner compile step for
tests) and ensure the base tsconfig or the original "exclude" remains as the
canonical source for editor tooling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4746b49d-1ec6-4f27-8a43-4724420e8630

📥 Commits

Reviewing files that changed from the base of the PR and between 7bab1f1 and 84b305d.

📒 Files selected for processing (30)
  • git-ai/README.md
  • git-ai/package.json
  • git-ai/src/ai/__tests__/notes-rewrite.test.ts
  • git-ai/src/ai/__tests__/notes-store.test.ts
  • git-ai/src/ai/attribution.ts
  • git-ai/src/ai/line-hash.ts
  • git-ai/src/ai/notes-store.ts
  • git-ai/src/ai/query.ts
  • git-ai/src/ai/schema.ts
  • git-ai/src/cli/pr-command.ts
  • git-ai/src/commands/InitCommand.ts
  • git-ai/src/commands/ResolveCommand.ts
  • git-ai/src/commands/ai/AiBlameCommand.ts
  • git-ai/src/commands/ai/AiExplainCommand.ts
  • git-ai/src/commands/ai/AiExportCommand.ts
  • git-ai/src/commands/ai/AiImportCommand.ts
  • git-ai/src/commands/ai/AiInspectCommand.ts
  • git-ai/src/commands/ai/AiInteractiveCommand.ts
  • git-ai/src/commands/ai/AiLogCommand.ts
  • git-ai/src/commands/ai/AiNotesCommand.ts
  • git-ai/src/commands/ai/AiRecordCommand.ts
  • git-ai/src/commands/ai/AiScanCommand.ts
  • git-ai/src/commands/ai/AiValidateCommand.ts
  • git-ai/src/commands/ai/index.ts
  • git-ai/src/core/GitService.ts
  • git-ai/src/index.ts
  • git-ai/src/ui/AiExplorer.tsx
  • git-ai/src/ui/PRList.tsx
  • git-ai/src/ui/TreeUI.tsx
  • git-ai/tsconfig.json

Comment thread git-ai/src/ai/line-hash.ts Outdated
Comment thread git-ai/src/commands/ai/AiExportCommand.ts Outdated
Comment thread git-ai/src/commands/ai/AiImportCommand.ts
Comment thread git-ai/src/commands/ai/AiInspectCommand.ts
Comment thread git-ai/src/core/GitService.ts
Comment thread git-ai/src/index.ts
Comment thread git-ai/src/ui/AiExplorer.tsx
Comment thread git-ai/src/ui/AiExplorer.tsx Outdated
@github-actions github-actions Bot added the ci/cd Changes to CI/CD configuration or workflows label Apr 14, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (2)
git-ai/src/ai/__tests__/notes-rewrite.test.ts (1)

58-63: Strengthen the amend-rewrite assertion by checking record identity.

Line 60 confirms cardinality, but adding an explicit id equality check makes this test validate true note carry-over, not just a single surviving record.

Proposed patch
     const idx2 = await store.listIndexForCommit(sha2);
     // If git notes rewrite is working, the note should have been copied.
     assert.equal(idx2.length, 1);
+    assert.equal(idx2[0].id, rec.id);
     const loaded2 = await store.getRecord(sha2, idx2[0].id);
     assert.ok(loaded2);
     assert.equal(loaded2!.prompt, 'amend rewrite');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git-ai/src/ai/__tests__/notes-rewrite.test.ts` around lines 58 - 63, The test
currently asserts only that one note exists after amend-rewrite but not that it
is the same record; update the assertions in the test around
store.listIndexForCommit(sha2) and store.getRecord(sha2, idx2[0].id) to also
verify that the record id matches the original note's id (compare idx2[0].id or
loaded2.id against the original id captured earlier, e.g., the id from the first
commit's index/loaded record) so the test validates true note carry-over rather
than just cardinality.
git-ai/scripts/run-tests.mjs (1)

26-33: Sort discovered test files for deterministic execution order.

Line 26 currently preserves filesystem traversal order, which can vary across environments. Sorting before spawn keeps runs more reproducible.

Proposed patch
-const testFiles = await collectTestFiles(distDir);
+const testFiles = (await collectTestFiles(distDir)).sort((a, b) =>
+  a.localeCompare(b),
+);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git-ai/scripts/run-tests.mjs` around lines 26 - 33, Discovered test file
order is non-deterministic; sort the array returned by collectTestFiles before
spawning the test runner. Locate where testFiles is assigned/used (variable
testFiles from collectTestFiles) and sort it (e.g., testFiles.sort()) before
passing to spawn/process.execPath, so the spawn call always receives a
deterministically ordered list of files.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@git-ai/src/commands/ai/AiBlameCommand.ts`:
- Around line 159-166: The sort comparator in the deterministic selection uses
localeCompare on recordMeta.get(...).createdAt which is not guaranteed to be
chronological; update the comparator in the sort for sorted (operating on
matches, recordMeta) to parse createdAt into a numeric timestamp (e.g.,
Date.parse(createdAt) or new Date(createdAt).getTime()) for both a and b, treat
invalid/missing dates as 0, compare the numeric timestamps (bTime - aTime for
newest-first), and keep the existing tie-breaker of returning
a.id.localeCompare(b.id) when timestamps are equal.
- Around line 66-99: The loop currently collapses metadata by entry.id so the
same record id on multiple commits overwrites earlier data; preserve (commit,id)
as a single identity: change recordMeta to key by a composite key (e.g.
`${commit}:${entry.id}`) and keep hashToRecord entries as {commit, id} (already
present), update pickDeterministicRecordId to accept/return that composite key
or the {commit,id} pair, and change annotated entries (and any downstream use
like printBlock/store.getRecord) to use the combined commit+id when fetching
record metadata via store.getRecord(commit, id) so you never lose which commit a
record came from.
- Around line 50-51: The split on fileContent is producing a trailing empty line
for files that end with a newline; before computing lines and mapping with
hashLine, trim a single terminal newline from fileContent (not full trim) so the
resulting lines array doesn't include a synthetic "" last element; update the
code that builds lines (the fileContent → lines split and the subsequent
lineHashes = lines.map(hashLine)) to first remove one trailing "\n" or "\r\n" if
present so displayed ranges and the hashLine correlations (see line-hash.ts
behavior) remain correct.
- Around line 147-150: The logged note fields (meta.prompt, meta.provider,
meta.model, meta.intent, etc.) are untrusted and can include ANSI/control
sequences that spoof/alter terminal output; sanitize them before logging by
adding a helper (e.g., sanitizeNote or stripAnsiAndControls) and use it wherever
you log note-derived strings: apply it to meta.prompt (after normalizing
whitespace), meta.provider, meta.model, meta.intent, and recordId, stripping
ANSI escape sequences and other control characters and truncating to a safe max
length; update the console.log lines in AiBlameCommand (the lines that reference
meta.prompt, meta.provider/meta.model, meta.intent, and recordId) to call that
sanitizer so only cleaned text is emitted.

In `@git-ai/src/commands/ai/AiScanCommand.ts`:
- Line 24: The command currently prefers staged (--cached) diffs even when a
user passed --commit; update the AiScanCommand flow that inspects staged changes
(the logic around the '--commit' option and the staged-diff check in the
run/execute path) so that if a commit value is provided (i.e., commit !== 'HEAD'
or user explicitly passed --commit) you generate the diff for that commit
instead of using --cached; only fall back to scanning staged changes when no
explicit commit was supplied. Locate the option definition ('-c, --commit
<sha>') and the staged-diff conditional (the block currently preferring
'--cached' around lines 35-44) and change the branching to prefer the requested
commit patch when commit is set, otherwise keep the existing staged-or-HEAD
behavior.
- Around line 53-57: The current loop in AiScanCommand scans the entire patch
text via diff.matchAll(toGlobal(rule.pattern)), which can flag removed lines;
change the scanning to only examine added content: extract the added lines from
the patch (lines starting with '+' but not file headers like '+++') or parse the
diff hunks to collect added hunks, join them into an addedText string, and then
run addedText.matchAll(toGlobal(rule.pattern)) to populate hits (keeping
rule.id, rule.message and sample from the match). Update the code around the
RISK_RULES loop (where diff.matchAll/toGlobal(rule.pattern)/hits.push are used)
to use the added-only text instead of the full diff.

---

Nitpick comments:
In `@git-ai/scripts/run-tests.mjs`:
- Around line 26-33: Discovered test file order is non-deterministic; sort the
array returned by collectTestFiles before spawning the test runner. Locate where
testFiles is assigned/used (variable testFiles from collectTestFiles) and sort
it (e.g., testFiles.sort()) before passing to spawn/process.execPath, so the
spawn call always receives a deterministically ordered list of files.

In `@git-ai/src/ai/__tests__/notes-rewrite.test.ts`:
- Around line 58-63: The test currently asserts only that one note exists after
amend-rewrite but not that it is the same record; update the assertions in the
test around store.listIndexForCommit(sha2) and store.getRecord(sha2, idx2[0].id)
to also verify that the record id matches the original note's id (compare
idx2[0].id or loaded2.id against the original id captured earlier, e.g., the id
from the first commit's index/loaded record) so the test validates true note
carry-over rather than just cardinality.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 28dd0e78-f50e-430c-a70c-91d3c6969694

📥 Commits

Reviewing files that changed from the base of the PR and between 84b305d and 48f71e2.

📒 Files selected for processing (16)
  • .github/workflows/ci.yml
  • git-ai/package.json
  • git-ai/scripts/run-tests.mjs
  • git-ai/src/ai/__tests__/notes-rewrite.test.ts
  • git-ai/src/ai/line-hash.ts
  • git-ai/src/commands/ai/AiBlameCommand.ts
  • git-ai/src/commands/ai/AiExportCommand.ts
  • git-ai/src/commands/ai/AiImportCommand.ts
  • git-ai/src/commands/ai/AiInspectCommand.ts
  • git-ai/src/commands/ai/AiNotesCommand.ts
  • git-ai/src/commands/ai/AiScanCommand.ts
  • git-ai/src/commands/ai/AiValidateCommand.ts
  • git-ai/src/commands/ai/index.ts
  • git-ai/src/core/GitService.ts
  • git-ai/src/index.ts
  • git-ai/src/ui/AiExplorer.tsx
✅ Files skipped from review due to trivial changes (2)
  • .github/workflows/ci.yml
  • git-ai/src/ai/line-hash.ts
🚧 Files skipped from review as they are similar to previous changes (10)
  • git-ai/package.json
  • git-ai/src/commands/ai/index.ts
  • git-ai/src/commands/ai/AiNotesCommand.ts
  • git-ai/src/commands/ai/AiInspectCommand.ts
  • git-ai/src/commands/ai/AiValidateCommand.ts
  • git-ai/src/index.ts
  • git-ai/src/core/GitService.ts
  • git-ai/src/commands/ai/AiImportCommand.ts
  • git-ai/src/commands/ai/AiExportCommand.ts
  • git-ai/src/ui/AiExplorer.tsx

Comment thread git-ai/src/commands/ai/AiBlameCommand.ts Outdated
Comment thread git-ai/src/commands/ai/AiBlameCommand.ts Outdated
Comment thread git-ai/src/commands/ai/AiBlameCommand.ts Outdated
Comment thread git-ai/src/commands/ai/AiBlameCommand.ts
export function buildAiScanCommand(): Command {
const cmd = new Command('scan')
.description('Heuristic scan for risky patterns in current staged diff (best-effort)')
.option('-c, --commit <sha>', 'Commit to scan diff for (default: HEAD)', 'HEAD')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Honor --commit instead of always preferring staged diff.

When staged changes exist, the current flow scans --cached and ignores the requested commit patch, which can produce results for the wrong target.

Suggested fix
-    .option('-c, --commit <sha>', 'Commit to scan diff for (default: HEAD)', 'HEAD')
+    .option('-c, --commit <sha>', 'Commit to scan patch for (if omitted, scan staged diff and fallback to HEAD)')
@@
-      const commit = (opts.commit ?? 'HEAD').trim();
+      const commit = opts.commit?.trim();
@@
-        // Default: scan staged diff. If in git subcommand mode, users can still stage before scan.
-        diff = await git.raw(['diff', '--cached']);
-        if (!diff.trim()) {
-          // Fallback: scan last commit patch.
-          diff = await git.raw(['show', '--format=', commit]);
-        }
+        if (commit) {
+          diff = await git.raw(['show', '--format=', commit]);
+        } else {
+          // Default: scan staged diff. If empty, fallback to HEAD patch.
+          diff = await git.raw(['diff', '--cached']);
+          if (!diff.trim()) {
+            diff = await git.raw(['show', '--format=', 'HEAD']);
+          }
+        }

Also applies to: 35-44

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git-ai/src/commands/ai/AiScanCommand.ts` at line 24, The command currently
prefers staged (--cached) diffs even when a user passed --commit; update the
AiScanCommand flow that inspects staged changes (the logic around the '--commit'
option and the staged-diff check in the run/execute path) so that if a commit
value is provided (i.e., commit !== 'HEAD' or user explicitly passed --commit)
you generate the diff for that commit instead of using --cached; only fall back
to scanning staged changes when no explicit commit was supplied. Locate the
option definition ('-c, --commit <sha>') and the staged-diff conditional (the
block currently preferring '--cached' around lines 35-44) and change the
branching to prefer the requested commit patch when commit is set, otherwise
keep the existing staged-or-HEAD behavior.

Comment on lines +53 to +57
for (const rule of RISK_RULES) {
for (const m of diff.matchAll(toGlobal(rule.pattern))) {
hits.push({ rule: rule.id, message: rule.message, sample: m[0] });
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid scanning removed/context lines; limit scan to added content.

Scanning the full patch text can flag code that was deleted, so risk checks may fail even when a risky pattern is being removed.

Suggested fix
-      const hits: { rule: string; message: string; sample: string }[] = [];
+      const scanText = diff
+        .split('\n')
+        .filter((line) => line.startsWith('+') && !line.startsWith('+++'))
+        .map((line) => line.slice(1))
+        .join('\n');
+
+      const hits: { rule: string; message: string; sample: string }[] = [];
       for (const rule of RISK_RULES) {
-        for (const m of diff.matchAll(toGlobal(rule.pattern))) {
+        for (const m of scanText.matchAll(toGlobal(rule.pattern))) {
           hits.push({ rule: rule.id, message: rule.message, sample: m[0] });
         }
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@git-ai/src/commands/ai/AiScanCommand.ts` around lines 53 - 57, The current
loop in AiScanCommand scans the entire patch text via
diff.matchAll(toGlobal(rule.pattern)), which can flag removed lines; change the
scanning to only examine added content: extract the added lines from the patch
(lines starting with '+' but not file headers like '+++') or parse the diff
hunks to collect added hunks, join them into an addedText string, and then run
addedText.matchAll(toGlobal(rule.pattern)) to populate hits (keeping rule.id,
rule.message and sample from the match). Update the code around the RISK_RULES
loop (where diff.matchAll/toGlobal(rule.pattern)/hits.push are used) to use the
added-only text instead of the full diff.

@jaseel0
Copy link
Copy Markdown
Collaborator

jaseel0 commented Apr 14, 2026

@copilot I’ll fix the issues mentioned by CodeRabbit.

@jaseel0
Copy link
Copy Markdown
Collaborator

jaseel0 commented Apr 14, 2026

@copilot Verify each finding against the current code and only fix it if needed.

In @git-ai/src/commands/ai/AiBlameCommand.ts around lines 50 - 51, The split on
fileContent is producing a trailing empty line for files that end with a
newline; before computing lines and mapping with hashLine, trim a single
terminal newline from fileContent (not full trim) so the resulting lines array
doesn't include a synthetic "" last element; update the code that builds lines
(the fileContent → lines split and the subsequent lineHashes =
lines.map(hashLine)) to first remove one trailing "\n" or "\r\n" if present so
displayed ranges and the hashLine correlations (see line-hash.ts behavior)
remain correct.

Verify each finding against the current code and only fix it if needed.

In @git-ai/src/commands/ai/AiBlameCommand.ts around lines 66 - 99, The loop
currently collapses metadata by entry.id so the same record id on multiple
commits overwrites earlier data; preserve (commit,id) as a single identity:
change recordMeta to key by a composite key (e.g. ${commit}:${entry.id}) and
keep hashToRecord entries as {commit, id} (already present), update
pickDeterministicRecordId to accept/return that composite key or the {commit,id}
pair, and change annotated entries (and any downstream use like
printBlock/store.getRecord) to use the combined commit+id when fetching record
metadata via store.getRecord(commit, id) so you never lose which commit a record
came from.
Verify each finding against the current code and only fix it if needed.

In @git-ai/src/commands/ai/AiBlameCommand.ts around lines 147 - 150, The logged
note fields (meta.prompt, meta.provider, meta.model, meta.intent, etc.) are
untrusted and can include ANSI/control sequences that spoof/alter terminal
output; sanitize them before logging by adding a helper (e.g., sanitizeNote or
stripAnsiAndControls) and use it wherever you log note-derived strings: apply it
to meta.prompt (after normalizing whitespace), meta.provider, meta.model,
meta.intent, and recordId, stripping ANSI escape sequences and other control
characters and truncating to a safe max length; update the console.log lines in
AiBlameCommand (the lines that reference meta.prompt, meta.provider/meta.model,
meta.intent, and recordId) to call that sanitizer so only cleaned text is
emitted.

Verify each finding against the current code and only fix it if needed.

In @git-ai/src/commands/ai/AiBlameCommand.ts around lines 159 - 166, The sort
comparator in the deterministic selection uses localeCompare on
recordMeta.get(...).createdAt which is not guaranteed to be chronological;
update the comparator in the sort for sorted (operating on matches, recordMeta)
to parse createdAt into a numeric timestamp (e.g., Date.parse(createdAt) or new
Date(createdAt).getTime()) for both a and b, treat invalid/missing dates as 0,
compare the numeric timestamps (bTime - aTime for newest-first), and keep the
existing tie-breaker of returning a.id.localeCompare(b.id) when timestamps are
equal.

Verify each finding against the current code and only fix it if needed.

In @git-ai/src/commands/ai/AiScanCommand.ts at line 24, The command currently
prefers staged (--cached) diffs even when a user passed --commit; update the
AiScanCommand flow that inspects staged changes (the logic around the '--commit'
option and the staged-diff check in the run/execute path) so that if a commit
value is provided (i.e., commit !== 'HEAD' or user explicitly passed --commit)
you generate the diff for that commit instead of using --cached; only fall back
to scanning staged changes when no explicit commit was supplied. Locate the
option definition ('-c, --commit ') and the staged-diff conditional (the
block currently preferring '--cached' around lines 35-44) and change the
branching to prefer the requested commit patch when commit is set, otherwise
keep the existing staged-or-HEAD behavior.

Verify each finding against the current code and only fix it if needed.

In @git-ai/src/commands/ai/AiScanCommand.ts around lines 53 - 57, The current
loop in AiScanCommand scans the entire patch text via
diff.matchAll(toGlobal(rule.pattern)), which can flag removed lines; change the
scanning to only examine added content: extract the added lines from the patch
(lines starting with '+' but not file headers like '+++') or parse the diff
hunks to collect added hunks, join them into an addedText string, and then run
addedText.matchAll(toGlobal(rule.pattern)) to populate hits (keeping rule.id,
rule.message and sample from the match). Update the code around the RISK_RULES
loop (where diff.matchAll/toGlobal(rule.pattern)/hits.push are used) to use the
added-only text instead of the full diff.

Copilot AI and others added 2 commits April 14, 2026 16:52
- Strip trailing newline before splitting file content to avoid synthetic empty line
- Use composite key (commit:id) for recordMeta to prevent metadata overwrite
- Sanitize untrusted note fields (ANSI/control chars) before terminal output
- Use numeric timestamps in sort comparator for deterministic record selection
- Respect explicit --commit flag in AiScanCommand instead of always checking staged

Agent-Logs-Url: https://github.com/BeyteFlow/git-ai/sessions/b0bd246a-0edd-485f-9eac-a96ba4764447

Co-authored-by: jaseel0 <225665919+jaseel0@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 14, 2026

@copilot Verify each finding against the current code and only fix it if needed.

In @git-ai/src/commands/ai/AiBlameCommand.ts around lines 50 - 51, The split on
...

All 5 findings verified and fixed in 3d546fc and b7acc92:

  1. Trailing newline (lines 50-51): Added fileContent.replace(/\r?\n$/, '') before splitting to avoid synthetic empty last element.
  2. Composite key (lines 66-99): recordMeta now keyed by ${commit}:${id}, pickDeterministicRecordKey returns and resolves composite keys, so records on different commits no longer overwrite each other.
  3. Terminal sanitization (lines 147-150): Added sanitizeForTerminal() helper stripping ANSI escape sequences (CSI, OSC with both BEL and ST terminators) and C0/C1 control characters with truncation; applied to all note-derived fields before logging.
  4. Numeric timestamps (lines 159-166): Sort comparator now uses Date.parse() with Number.isFinite() guard (invalid/missing → 0) and numeric subtraction (bTime - aTime).
  5. Explicit --commit (line 24): Uses command.getOptionValueSource('commit') === 'cli' to detect explicit flag; skips staged diff and goes directly to git show for the specified commit.

@naheel0 naheel0 merged commit 89b4c7e into main Apr 15, 2026
6 checks passed
@naheel0 naheel0 deleted the feat/git-ai-attribution branch April 15, 2026 16:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd Changes to CI/CD configuration or workflows documentation Improvements or additions to documentation tests Changes to test files or test infrastructure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants