diff --git a/apps/claude-sdk-cli/changes.jsonl b/apps/claude-sdk-cli/changes.jsonl index e69de29..f870af0 100644 --- a/apps/claude-sdk-cli/changes.jsonl +++ b/apps/claude-sdk-cli/changes.jsonl @@ -0,0 +1 @@ +{"description":"Fix `GitStateMonitor` reporting the agent's own file edits and commits as human activity between turns","category":"fixed"} diff --git a/apps/claude-sdk-cli/src/GitStateMonitor.ts b/apps/claude-sdk-cli/src/GitStateMonitor.ts index 0e32f98..b006967 100644 --- a/apps/claude-sdk-cli/src/GitStateMonitor.ts +++ b/apps/claude-sdk-cli/src/GitStateMonitor.ts @@ -6,9 +6,12 @@ export type SnapshotFn = () => Promise; /** * Tracks git state between turns so the agent sees what changed, not just what is. * - * First call: stores the baseline, returns null (no stale model yet, nothing to inject). - * Subsequent calls: computes delta against the stored baseline, updates it, returns - * the formatted delta string or null if nothing changed (silence = signal). + * Call `getDelta()` before `runAgent()` — diffs human activity since the last snapshot. + * Call `takeSnapshot()` after `runAgent()` — captures post-agent state as the new baseline. + * + * `getDelta()` returns undefined if no baseline exists yet (first turn, nothing to compare against). + * Separating the two calls ensures the agent's own file edits and commits are excluded + * from the delta reported to the next turn. */ export class GitStateMonitor { #previous: GitSnapshot | null = null; @@ -18,17 +21,18 @@ export class GitStateMonitor { this.#takeSnapshot = takeSnapshot; } - public async takeDelta(): Promise { - const current = await this.#takeSnapshot(); - + public async getDelta(): Promise { if (this.#previous === null) { - this.#previous = current; - return null; + return undefined; } + const current = await this.#takeSnapshot(); const delta = computeDelta(this.#previous, current); - this.#previous = current; - return delta ? formatDelta(delta) : null; + return delta ? formatDelta(delta) : undefined; + } + + public async takeSnapshot(): Promise { + this.#previous = await this.#takeSnapshot(); } } diff --git a/apps/claude-sdk-cli/src/entry/main.ts b/apps/claude-sdk-cli/src/entry/main.ts index 9a95f5a..7bbf80f 100644 --- a/apps/claude-sdk-cli/src/entry/main.ts +++ b/apps/claude-sdk-cli/src/entry/main.ts @@ -229,7 +229,6 @@ const main = async () => { while (true) { const prompt = await layout.waitForInput(); - const gitDelta = await gitMonitor.takeDelta(); const claudeMdContent = watcher.config.claudeMd.enabled ? await claudeMdLoader.getContent() : null; // Update durable config with current values before each query @@ -241,7 +240,9 @@ const main = async () => { layout.setModel(watcher.config.model); turnInProgress = true; - await runAgent(queryRunner, prompt, layout, channel.consumerPort, transformToolResult, abortController, gitDelta ?? undefined); + const gitDelta = await gitMonitor.getDelta(); + await runAgent(queryRunner, prompt, layout, channel.consumerPort, transformToolResult, abortController, gitDelta); + await gitMonitor.takeSnapshot(); turnInProgress = false; currentAbortController = null; diff --git a/apps/claude-sdk-cli/test/gitDelta.spec.ts b/apps/claude-sdk-cli/test/gitDelta.spec.ts index f528413..f6e2e6e 100644 --- a/apps/claude-sdk-cli/test/gitDelta.spec.ts +++ b/apps/claude-sdk-cli/test/gitDelta.spec.ts @@ -203,21 +203,19 @@ describe('formatDelta — multiple fields joined with pipe', () => { // --------------------------------------------------------------------------- describe('GitStateMonitor — first call', () => { - it('returns null on the first call (no baseline yet)', async () => { + it('returns undefined on the first call (no baseline yet)', async () => { const monitor = new GitStateMonitor(() => Promise.resolve({ ...base })); - const actual = await monitor.takeDelta(); - const expected = null; - expect(actual).toEqual(expected); + const actual = await monitor.getDelta(); + expect(actual).toBeUndefined(); }); }); describe('GitStateMonitor — no change between calls', () => { - it('returns null when snapshot is identical to baseline', async () => { + it('returns undefined when snapshot is identical to baseline', async () => { const monitor = new GitStateMonitor(() => Promise.resolve({ ...base })); - await monitor.takeDelta(); // establish baseline - const actual = await monitor.takeDelta(); - const expected = null; - expect(actual).toEqual(expected); + await monitor.takeSnapshot(); // establish baseline + const actual = await monitor.getDelta(); + expect(actual).toBeUndefined(); }); }); @@ -226,20 +224,21 @@ describe('GitStateMonitor — change between calls', () => { let call = 0; const snapshots: GitSnapshot[] = [base, { ...base, branch: 'feature/x' }]; const monitor = new GitStateMonitor(() => Promise.resolve({ ...(snapshots[call++] ?? base) })); - await monitor.takeDelta(); // baseline - const actual = await monitor.takeDelta(); + await monitor.takeSnapshot(); // baseline: snapshot[0] = base + const actual = await monitor.getDelta(); // diffs snapshot[1] against base const expected = '[git delta] branch: main \u2192 feature/x'; expect(actual).toEqual(expected); }); it('advances the baseline so next call diffs against the most recent snapshot', async () => { let call = 0; - const snapshots: GitSnapshot[] = [base, { ...base, branch: 'feature/x' }, { ...base, branch: 'feature/x' }]; + const featureX = { ...base, branch: 'feature/x' }; + const snapshots: GitSnapshot[] = [base, featureX, featureX, featureX]; const monitor = new GitStateMonitor(() => Promise.resolve({ ...(snapshots[call++] ?? base) })); - await monitor.takeDelta(); // baseline: main - await monitor.takeDelta(); // delta: main → feature/x - const actual = await monitor.takeDelta(); // no change: feature/x → feature/x - const expected = null; - expect(actual).toEqual(expected); + await monitor.takeSnapshot(); // baseline: snapshot[0] = base (main) + await monitor.getDelta(); // diffs snapshot[1] = feature/x against base — returns delta (not used) + await monitor.takeSnapshot(); // advance baseline: snapshot[2] = feature/x + const actual = await monitor.getDelta(); // diffs snapshot[3] = feature/x against feature/x — no change + expect(actual).toBeUndefined(); }); });