mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-24 00:51:27 +08:00
bd1be0c1ce
- Line precision: parse git diff --unified=0 into per-file changed line ranges
(defaultWorkingSetFor), so two agents in the SAME file but DIFFERENT functions
no longer false-collide. Overlap channel now uses the overlap coefficient
(|A∩B|/min(|A|,|B|)) — high when one edit sits inside the other's region, low
for disjoint ranges; whole-file edit = 1. Docstring + design doc updated.
- Trigger firing: buildProximityTriggers() turns advisories into the concrete
messages — transmit-intent to both on a Traffic Advisory, steer-away to the
yielding agent + a hold notice on a Resolution Advisory. buildProximitySnapshot
now returns triggers; dispatchProximityTriggers(triggers, {sendMessage}) delivers
them through an injectable sink (the ECC messages table), best-effort.
- 12 new tests (line-range disjoint vs overlapping, parseDiffRanges, triggers,
dispatch). Full suite 2881/2881; lint green.
190 lines
6.2 KiB
JavaScript
190 lines
6.2 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Control-pane integration for the agent-space proximity metric.
|
|
*
|
|
* Turns live sessions into agent working sets (the files each session's worktree
|
|
* has changed), builds the dependency graph over those files, and runs the
|
|
* TCAS-style airspace scan — so the board can surface "two agents are converging"
|
|
* advisories and a 3D position per agent. See docs/design/agent-proximity.md.
|
|
*/
|
|
|
|
const path = require('path');
|
|
const { execFileSync } = require('child_process');
|
|
|
|
const { scanAirspace, buildProximityTriggers } = require('../agent-proximity');
|
|
const { buildDependencyGraph } = require('../agent-proximity/graph');
|
|
|
|
/**
|
|
* Parse `git diff --unified=0` output into per-file NEW-side line ranges. Hunk
|
|
* headers look like `@@ -a,b +c,d @@`; we keep the +c,d (new) side so the overlap
|
|
* channel can tell that two agents touch the *same file* but *different line
|
|
* ranges* (different functions) and not flag a false collision.
|
|
*
|
|
* @param {string} diff
|
|
* @returns {Map<string, Array<[number,number]>>}
|
|
*/
|
|
function parseDiffRanges(diff) {
|
|
const byFile = new Map();
|
|
let current = null;
|
|
for (const line of String(diff || '').split('\n')) {
|
|
const fileMatch = line.match(/^\+\+\+ b\/(.+)$/);
|
|
if (fileMatch) {
|
|
const name = fileMatch[1].trim();
|
|
current = name === '/dev/null' ? null : name;
|
|
if (current && !byFile.has(current)) byFile.set(current, []);
|
|
continue;
|
|
}
|
|
const hunk = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
|
|
if (hunk && current) {
|
|
const start = parseInt(hunk[1], 10);
|
|
const count = hunk[2] === undefined ? 1 : parseInt(hunk[2], 10);
|
|
if (count > 0) byFile.get(current).push([start, start + count - 1]);
|
|
}
|
|
}
|
|
return byFile;
|
|
}
|
|
|
|
function runGitDiff(worktreePath, base, extraArgs) {
|
|
return execFileSync('git', ['-C', worktreePath, 'diff', ...extraArgs, `${base}...HEAD`], {
|
|
encoding: 'utf8',
|
|
timeout: 5000,
|
|
maxBuffer: 8 * 1024 * 1024,
|
|
stdio: ['ignore', 'pipe', 'ignore']
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* proximity degrades gracefully (never throws into the snapshot path).
|
|
*/
|
|
function defaultWorkingSetFor(session) {
|
|
const wt = session && session.worktree;
|
|
if (!wt || !wt.path) return [];
|
|
const base = wt.base || 'HEAD';
|
|
try {
|
|
const ranges = parseDiffRanges(runGitDiff(wt.path, base, ['--unified=0']));
|
|
return [...ranges.entries()].map(([path, lines]) => (lines.length > 0 ? { path, lines } : { path }));
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Back-compat file-name-only source (no line ranges).
|
|
*/
|
|
function defaultChangedFilesFor(session) {
|
|
const wt = session && session.worktree;
|
|
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);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Map sessions to agent working sets. Only sessions with a worktree and at least
|
|
* one changed file participate (an agent with no edits cannot collide).
|
|
* Inject `workingSetFor` (returns [{path,lines?}]) or `changedFilesFor`
|
|
* (returns file-name strings) for tests.
|
|
*/
|
|
function sessionsToAgents(sessions, deps = {}) {
|
|
const workingSetFor = deps.workingSetFor || (deps.changedFilesFor ? session => deps.changedFilesFor(session).map(p => ({ path: p })) : defaultWorkingSetFor);
|
|
const agents = [];
|
|
for (const session of sessions || []) {
|
|
const files = workingSetFor(session).map(f => ({ weight: 1, ...f }));
|
|
if (files.length === 0) continue;
|
|
agents.push({
|
|
agentId: session.id,
|
|
label: session.task || session.id,
|
|
startedAt: session.createdAt || null,
|
|
files
|
|
});
|
|
}
|
|
return agents;
|
|
}
|
|
|
|
/**
|
|
* Compute the proximity snapshot from the control-pane sessions.
|
|
*
|
|
* @param {Array} sessions normalized control-pane sessions
|
|
* @param {object} [options] { repoRoot, changedFilesFor, ...scanOptions }
|
|
* @returns {{ enabled, advisories, positions, links, counts }}
|
|
*/
|
|
function buildProximitySnapshot(sessions, options = {}) {
|
|
const repoRoot = path.resolve(options.repoRoot || path.join(__dirname, '..', '..', '..'));
|
|
const agents = sessionsToAgents(sessions, options);
|
|
|
|
// Need at least two participating agents for a collision to be possible.
|
|
if (agents.length < 2) {
|
|
return {
|
|
enabled: true,
|
|
advisories: [],
|
|
positions: agents.map(a => ({ agentId: a.agentId, position: [0, 0, 0], fileCount: a.files.length })),
|
|
links: [],
|
|
counts: { agents: agents.length, advisories: 0, resolutions: 0 }
|
|
};
|
|
}
|
|
|
|
const touched = [...new Set(agents.flatMap(a => a.files.map(f => f.path)))];
|
|
let graph = { adjacency: {}, files: [] };
|
|
try {
|
|
graph = options.graph || buildDependencyGraph(repoRoot, touched, options.graphDeps || {});
|
|
} catch {
|
|
graph = { adjacency: {}, files: [] };
|
|
}
|
|
|
|
const scan = scanAirspace(agents, graph, options);
|
|
const labels = new Map(agents.map(a => [a.agentId, a.label]));
|
|
const advisories = scan.advisories.map(adv => ({
|
|
...adv,
|
|
aLabel: labels.get(adv.a) || adv.a,
|
|
bLabel: labels.get(adv.b) || adv.b
|
|
}));
|
|
return {
|
|
enabled: true,
|
|
advisories,
|
|
triggers: buildProximityTriggers(scan.advisories),
|
|
positions: scan.positions,
|
|
links: scan.links,
|
|
counts: scan.counts
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Deliver proximity triggers via an injected message sink. The sink is
|
|
* `sendMessage({ fromSession, toSession, content, msgType })` — e.g. a writer
|
|
* for the ECC `messages` table the control pane already reads. Best-effort:
|
|
* a failing send is skipped, never thrown. Returns the dispatched count.
|
|
*/
|
|
function dispatchProximityTriggers(triggers, deps = {}) {
|
|
const send = deps.sendMessage;
|
|
if (typeof send !== 'function') return { dispatched: 0, skipped: (triggers || []).length };
|
|
let dispatched = 0;
|
|
let skipped = 0;
|
|
for (const t of triggers || []) {
|
|
try {
|
|
send({ fromSession: t.from, toSession: t.to, content: t.content, msgType: t.type });
|
|
dispatched += 1;
|
|
} catch {
|
|
skipped += 1;
|
|
}
|
|
}
|
|
return { dispatched, skipped };
|
|
}
|
|
|
|
module.exports = {
|
|
buildProximitySnapshot,
|
|
sessionsToAgents,
|
|
defaultWorkingSetFor,
|
|
defaultChangedFilesFor,
|
|
parseDiffRanges,
|
|
dispatchProximityTriggers
|
|
};
|