Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
53 changes: 53 additions & 0 deletions .claude/sessions/2026-04-11.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Session 2026-04-11

Branch: `fix/git-snapshot-error-handling`. Phase 1 complete, Phase 2 pending.

## Context

Prompt: `2026-04-11_242_git-snapshot-error-handling.md` (Phase 1 of 2).

Issue #242: `gatherGitSnapshot` runs four git commands in parallel via `Promise.all`. When any command fails (e.g. `git rev-parse HEAD` in a repo with no commits, exit code 128), the `Promise.all` rejection propagates unhandled and crashes the process.

## Done

### Phase 1: Refactor and failing test

**Step 1: Refactor** — Added optional `runner` parameter to `gatherGitSnapshot` with `runGit` as the default:

```typescript
export async function gatherGitSnapshot(runner: (args: string[]) => Promise<string> = runGit): Promise<GitSnapshot>
```

Updated the `Promise.all` body to call `runner(...)` instead of `runGit(...)`. No callers pass a runner, so default behaviour is unchanged.

**Step 2: Failing test** — Added `gatherGitSnapshot` describe block to `test/gitSnapshot.spec.ts`. The test injects a runner that rejects for `['rev-parse', 'HEAD']` and returns empty string for all other args. Asserts the snapshot resolves with `head: ''`.

**Step 3: Test run** — Test is failing as required:

```
FAIL test/gitSnapshot.spec.ts > gatherGitSnapshot > resolves with head empty string when rev-parse HEAD fails
Error: fatal: ambiguous argument HEAD
at runner test/gitSnapshot.spec.ts:177:31
at gatherGitSnapshot src/gitSnapshot.ts:74:110

Test Files 1 failed | 17 passed (18)
Tests 1 failed | 426 passed (427)
```

Committed: `73707b2 Add injectable runner to gatherGitSnapshot and a failing test for #242`

## Phase 2: Fix and ship

**Fix** - Added `.catch(() => '')` to each `runner(...)` call inside the `Promise.all`. Biome reformatted the `Promise.all` to a single line.

**Checks** - `pnpm build`, `pnpm type-check`, `pnpm test` (427/427), `pnpm run ci` all pass.

**Changelog** - Added entry to `apps/claude-sdk-cli/changes.jsonl`.

**Commits**:
- `ce8e120 Catch per-command failures in gatherGitSnapshot`
- `b81f684 Add changelog entry for gatherGitSnapshot fix`

**Script fix** - `~/.claude/skills/github-pr/scripts/create-github-pr.sh` was building label args via unquoted string concatenation, breaking labels with spaces (`pkg: claude-sdk-cli`). Fixed to use `set -- "$@" --label "$label"` and pass `"$@"` to `gh pr create`, which preserves quoting correctly.

**PR**: https://github.com/shellicar/claude-cli/pull/243 - auto-merge enabled, checks in progress.
1 change: 1 addition & 0 deletions apps/claude-sdk-cli/changes.jsonl
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
{"description":"Fix `GitStateMonitor` reporting the agent's own file edits and commits as human activity between turns","category":"fixed"}
{"description":"Fix `gatherGitSnapshot` crashing when any git command fails (e.g. `rev-parse HEAD` in a repo with no commits)","category":"fixed"}
4 changes: 2 additions & 2 deletions apps/claude-sdk-cli/src/gitSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ async function runGit(args: string[]): Promise<string> {
return stdout;
}

export async function gatherGitSnapshot(): Promise<GitSnapshot> {
const [branchOut, headOut, statusOut, stashOut] = await Promise.all([runGit(['branch', '--show-current']), runGit(['rev-parse', 'HEAD']), runGit(['status', '--porcelain']), runGit(['stash', 'list', '--no-decorate'])]);
export async function gatherGitSnapshot(runner: (args: string[]) => Promise<string> = runGit): Promise<GitSnapshot> {
const [branchOut, headOut, statusOut, stashOut] = await Promise.all([runner(['branch', '--show-current']).catch(() => ''), runner(['rev-parse', 'HEAD']).catch(() => ''), runner(['status', '--porcelain']).catch(() => ''), runner(['stash', 'list', '--no-decorate']).catch(() => '')]);
return {
branch: parseBranch(branchOut),
head: parseHead(headOut),
Expand Down
19 changes: 18 additions & 1 deletion apps/claude-sdk-cli/test/gitSnapshot.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { parseBranch, parseHead, parseStash, parseStatus } from '../src/gitSnapshot.js';
import { gatherGitSnapshot, parseBranch, parseHead, parseStash, parseStatus } from '../src/gitSnapshot.js';

// ---------------------------------------------------------------------------
// parseBranch
Expand Down Expand Up @@ -165,3 +165,20 @@ describe('parseStash', () => {
expect(actual).toEqual(expected);
});
});

// ---------------------------------------------------------------------------
// gatherGitSnapshot
// ---------------------------------------------------------------------------

describe('gatherGitSnapshot', () => {
it('resolves with head empty string when rev-parse HEAD fails', async () => {
const runner = (args: string[]): Promise<string> => {
if (args[0] === 'rev-parse' && args[1] === 'HEAD') {
return Promise.reject(new Error('fatal: ambiguous argument HEAD'));
}
return Promise.resolve('');
};
const snapshot = await gatherGitSnapshot(runner);
expect(snapshot.head).toEqual('');
});
});
Loading