From ade0269cf0ef508ceb2c5ab2b72df658bfa7121f Mon Sep 17 00:00:00 2001 From: Stephen Hellicar Date: Sat, 11 Apr 2026 01:43:53 +1000 Subject: [PATCH 1/4] Fix GitStateMonitor reporting agent's own changes as human activity The old takeDelta() took a snapshot and diffed against the previous one in a single call, always called before runAgent(). This meant the baseline was set before the agent ran, so everything the agent did during its turn (file edits, commits) would show up in the next turn's delta as if the human had done it. Split into two separate operations: - getDelta(): called before runAgent(), diffs against the stored baseline to capture only what the human changed between turns. Does not update the baseline. - takeSnapshot(): called after runAgent() returns, captures the post-agent state as the new baseline for the next turn. This way the agent's own work is always behind the baseline and never appears in the delta the agent receives. --- apps/claude-sdk-cli/src/GitStateMonitor.ts | 20 ++++++++++++-------- apps/claude-sdk-cli/src/entry/main.ts | 3 ++- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/apps/claude-sdk-cli/src/GitStateMonitor.ts b/apps/claude-sdk-cli/src/GitStateMonitor.ts index 0e32f98..fbe71fd 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 null 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; } + const current = await this.#takeSnapshot(); const delta = computeDelta(this.#previous, current); - this.#previous = current; return delta ? formatDelta(delta) : null; } + + 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..5880e2c 100644 --- a/apps/claude-sdk-cli/src/entry/main.ts +++ b/apps/claude-sdk-cli/src/entry/main.ts @@ -229,7 +229,7 @@ const main = async () => { while (true) { const prompt = await layout.waitForInput(); - const gitDelta = await gitMonitor.takeDelta(); + const gitDelta = await gitMonitor.getDelta(); const claudeMdContent = watcher.config.claudeMd.enabled ? await claudeMdLoader.getContent() : null; // Update durable config with current values before each query @@ -242,6 +242,7 @@ const main = async () => { layout.setModel(watcher.config.model); turnInProgress = true; await runAgent(queryRunner, prompt, layout, channel.consumerPort, transformToolResult, abortController, gitDelta ?? undefined); + await gitMonitor.takeSnapshot(); turnInProgress = false; currentAbortController = null; From 484e66611f8a3d700b72c91e96c6420be2664f1c Mon Sep 17 00:00:00 2001 From: Stephen Hellicar Date: Sat, 11 Apr 2026 01:47:17 +1000 Subject: [PATCH 2/4] Return undefined instead of null from getDelta string | undefined is consistent with how the rest of the codebase signals "no value". The null return was unnecessary and forced a ?? undefined coercion at the call site. Also moved the getDelta() call to immediately before runAgent() to keep the delta capture as close to agent start as possible. --- apps/claude-sdk-cli/src/GitStateMonitor.ts | 6 +++--- apps/claude-sdk-cli/src/entry/main.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/claude-sdk-cli/src/GitStateMonitor.ts b/apps/claude-sdk-cli/src/GitStateMonitor.ts index fbe71fd..685d040 100644 --- a/apps/claude-sdk-cli/src/GitStateMonitor.ts +++ b/apps/claude-sdk-cli/src/GitStateMonitor.ts @@ -21,15 +21,15 @@ export class GitStateMonitor { this.#takeSnapshot = takeSnapshot; } - public async getDelta(): Promise { + public async getDelta(): Promise { if (this.#previous === null) { - return null; + return undefined; } const current = await this.#takeSnapshot(); const delta = computeDelta(this.#previous, current); - return delta ? formatDelta(delta) : null; + return delta ? formatDelta(delta) : undefined; } public async takeSnapshot(): Promise { diff --git a/apps/claude-sdk-cli/src/entry/main.ts b/apps/claude-sdk-cli/src/entry/main.ts index 5880e2c..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.getDelta(); const claudeMdContent = watcher.config.claudeMd.enabled ? await claudeMdLoader.getContent() : null; // Update durable config with current values before each query @@ -241,7 +240,8 @@ 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; From 67351af285722f056f6872af7b3de0055613ef4d Mon Sep 17 00:00:00 2001 From: Stephen Hellicar Date: Sat, 11 Apr 2026 01:48:18 +1000 Subject: [PATCH 3/4] Add changelog entry for GitStateMonitor fix --- apps/claude-sdk-cli/changes.jsonl | 1 + 1 file changed, 1 insertion(+) 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"} From b76ade37e92fad07e0693b4dc4d97b4b5146ad89 Mon Sep 17 00:00:00 2001 From: Stephen Hellicar Date: Sat, 11 Apr 2026 03:31:18 +1000 Subject: [PATCH 4/4] Update gitDelta tests to use getDelta/takeSnapshot API The GitStateMonitor was refactored to split takeDelta() into two separate operations, but the tests were not updated to match. They still called takeDelta() which no longer exists, causing four failures. Update the GitStateMonitor tests to use the new two-call pattern: - takeSnapshot() establishes / advances the baseline - getDelta() diffs current state against the baseline without mutating it Also align the return value expectations: getDelta() returns undefined (not null) when there is no baseline or no change, matching the implementation signature of Promise. --- apps/claude-sdk-cli/src/GitStateMonitor.ts | 2 +- apps/claude-sdk-cli/test/gitDelta.spec.ts | 33 +++++++++++----------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/apps/claude-sdk-cli/src/GitStateMonitor.ts b/apps/claude-sdk-cli/src/GitStateMonitor.ts index 685d040..b006967 100644 --- a/apps/claude-sdk-cli/src/GitStateMonitor.ts +++ b/apps/claude-sdk-cli/src/GitStateMonitor.ts @@ -9,7 +9,7 @@ export type SnapshotFn = () => Promise; * 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 null if no baseline exists yet (first turn, nothing to compare against). + * `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. */ 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(); }); });