mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-19 06:23:46 +02:00
fix(proximity): observe uncommitted worktree changes
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
|
||||
Reference in New Issue
Block a user