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
1 change: 1 addition & 0 deletions apps/claude-sdk-cli/changes.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"description":"Fix `GitStateMonitor` reporting the agent's own file edits and commits as human activity between turns","category":"fixed"}
24 changes: 14 additions & 10 deletions apps/claude-sdk-cli/src/GitStateMonitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ export type SnapshotFn = () => Promise<GitSnapshot>;
/**
* 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;
Expand All @@ -18,17 +21,18 @@ export class GitStateMonitor {
this.#takeSnapshot = takeSnapshot;
}

public async takeDelta(): Promise<string | null> {
const current = await this.#takeSnapshot();

public async getDelta(): Promise<string | undefined> {
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<void> {
this.#previous = await this.#takeSnapshot();
}
}
5 changes: 3 additions & 2 deletions apps/claude-sdk-cli/src/entry/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down
33 changes: 16 additions & 17 deletions apps/claude-sdk-cli/test/gitDelta.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});

Expand All @@ -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();
});
});
Loading