fix(proximity): observe uncommitted worktree changes

This commit is contained in:
Huanyi Xie
2026-10-06 19:39:22 +03:00
parent ef648e0189
commit 72d738de37
3 changed files with 67 additions and 6 deletions
+5
View File
@@ -149,3 +149,8 @@ the renderer draws), and pairwise `links` with risk (the edges to color).
escalation wired to live session diffs.
- v2: cross-machine airspace over Tailscale (teammate agents enter the same
space); the recorded "N agents, M humans, zero merge conflicts" demo.
The control pane compares each worktree against the merge base of its declared base
and HEAD. Committed, staged, and unstaged tracked edits carry changed line ranges.
Untracked files that Git does not ignore participate as whole-file working sets.
Changes made only on the base branch after the worktree diverged do not participate.
+13 -6
View File
@@ -45,8 +45,8 @@ function parseDiffRanges(diff) {
return byFile;
}
function runGitDiff(worktreePath, base, extraArgs) {
return execFileSync('git', ['-C', worktreePath, 'diff', ...extraArgs, `${base}...HEAD`], {
function runGit(worktreePath, args) {
return execFileSync('git', ['-C', worktreePath, ...args], {
encoding: 'utf8',
timeout: 5000,
maxBuffer: 8 * 1024 * 1024,
@@ -54,6 +54,15 @@ function runGitDiff(worktreePath, base, extraArgs) {
});
}
function runGitDiff(worktreePath, base, extraArgs) {
const mergeBase = runGit(worktreePath, ['merge-base', '--', base, 'HEAD']).trim();
return runGit(worktreePath, ['diff', ...extraArgs, mergeBase, '--']);
}
function untrackedFiles(worktreePath) {
return runGit(worktreePath, ['ls-files', '--others', '--exclude-standard', '-z']).split('\0').filter(Boolean);
}
/**
* Default working-set source: a session's worktree diff against its base, with
* per-file changed line ranges. Returns [{ path, lines? }], or [] on failure so
@@ -65,6 +74,7 @@ function defaultWorkingSetFor(session) {
const base = wt.base || 'HEAD';
try {
const ranges = parseDiffRanges(runGitDiff(wt.path, base, ['--unified=0']));
for (const file of untrackedFiles(wt.path)) ranges.set(file, []);
return [...ranges.entries()].map(([path, lines]) => (lines.length > 0 ? { path, lines } : { path }));
} catch {
return [];
@@ -79,10 +89,7 @@ function defaultChangedFilesFor(session) {
if (!wt || !wt.path) return [];
const base = wt.base || 'HEAD';
try {
return runGitDiff(wt.path, base, ['--name-only'])
.split('\n')
.map(s => s.trim())
.filter(Boolean);
return [...new Set([...runGitDiff(wt.path, base, ['--name-only', '-z']).split('\0').filter(Boolean), ...untrackedFiles(wt.path)])];
} catch {
return [];
}
+49
View File
@@ -54,6 +54,55 @@ const changedFilesFor = session =>
'no-worktree': []
})[session.id] || [];
test('real worktrees expose committed, staged, unstaged and untracked work before commit', () => {
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync } = require('child_process');
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-proximity-worktree-'));
const repo = path.join(dir, 'repo');
const git = (cwd, ...args) => execFileSync('git', ['-C', cwd, ...args], { stdio: 'pipe' });
try {
fs.mkdirSync(repo);
git(repo, 'init', '-q', '-b', 'main');
git(repo, 'config', 'user.name', 'proximity-test');
git(repo, 'config', 'user.email', 'proximity-test@example.com');
fs.writeFileSync(path.join(repo, 'shared.js'), 'const value = 1;\n');
fs.writeFileSync(path.join(repo, '.gitignore'), 'ignored.js\n');
git(repo, 'add', '.');
git(repo, 'commit', '-qm', 'base');
const worktrees = ['a', 'b'].map(name => {
const wt = path.join(dir, name);
git(repo, 'worktree', 'add', '-q', '-b', name, wt, 'main');
fs.writeFileSync(path.join(wt, 'shared.js'), `const value = '${name}';\n`);
fs.writeFileSync(path.join(wt, 'new.js'), `export const name = '${name}';\n`);
fs.writeFileSync(path.join(wt, 'ignored.js'), 'ignored\n');
return { id: name, worktree: { path: wt, base: 'main' } };
});
git(worktrees[0].worktree.path, 'add', 'shared.js');
fs.writeFileSync(path.join(repo, 'upstream.js'), 'upstream only\n');
git(repo, 'add', '.');
git(repo, 'commit', '-qm', 'advance base');
const assertWorkingSets = () => {
const agents = sessionsToAgents(worktrees);
assert.strictEqual(agents.length, 2);
for (const agent of agents) {
assert.deepStrictEqual(agent.files.map(f => f.path).sort(), ['new.js', 'shared.js']);
assert.deepStrictEqual(agent.files.find(f => f.path === 'shared.js').lines, [[1, 1]]);
assert.strictEqual(agent.files.find(f => f.path === 'new.js').lines, undefined);
}
const snapshot = buildProximitySnapshot(worktrees, { repoRoot: repo, graph: { adjacency: {} } });
assert.strictEqual(snapshot.counts.agents, 2);
assert.ok(snapshot.advisories.some(a => a.level === 'resolution'));
};
assertWorkingSets();
git(worktrees[0].worktree.path, 'commit', '-qm', 'commit staged edit');
assertWorkingSets();
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
test('sessionsToAgents: only worktree sessions with edits participate', () => {
const agents = sessionsToAgents(sessions, { changedFilesFor });
assert.deepStrictEqual(agents.map(a => a.agentId).sort(), ['docs-bot', 'lead-hermes', 'worker-kb']);