mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-24 14:19:32 +08:00
feat(control-pane): wire agent-proximity into the snapshot (opt-in, live)
Turns live sessions into the airspace scan: each worktree session's git diff becomes its working set, the dependency graph is built over the touched files, and scanAirspace() produces the TCAS advisories + 3D positions. - scripts/lib/control-pane/proximity.js: sessionsToAgents() + buildProximitySnapshot(); default working-set source shells `git diff --name-only <base>...HEAD` per worktree (injectable for tests, fails closed to []). - state.js: opt-in `proximity` field on the snapshot (includeProximity flag) so the default hot path stays fast (git diffs only run when requested). - 4 integration tests (same-file editors -> resolution, later agent steers, <2 participants -> no advisories, labels). Full suite 2873/2873; lint green.
This commit is contained in:
parent
726972d735
commit
7df803935a
111
scripts/lib/control-pane/proximity.js
Normal file
111
scripts/lib/control-pane/proximity.js
Normal file
@ -0,0 +1,111 @@
|
||||
'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 } = require('../agent-proximity');
|
||||
const { buildDependencyGraph } = require('../agent-proximity/graph');
|
||||
|
||||
/**
|
||||
* Default working-set source: `git diff --name-only <base>` inside a session's
|
||||
* worktree, returning repo-relative changed files. Returns [] on any failure so
|
||||
* proximity degrades gracefully (never throws into the snapshot path).
|
||||
*/
|
||||
function defaultChangedFilesFor(session) {
|
||||
const wt = session && session.worktree;
|
||||
if (!wt || !wt.path) return [];
|
||||
const base = wt.base || 'HEAD';
|
||||
try {
|
||||
const out = execFileSync('git', ['-C', wt.path, 'diff', '--name-only', `${base}...HEAD`], {
|
||||
encoding: 'utf8',
|
||||
timeout: 4000,
|
||||
stdio: ['ignore', 'pipe', 'ignore']
|
||||
});
|
||||
return out
|
||||
.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).
|
||||
*/
|
||||
function sessionsToAgents(sessions, deps = {}) {
|
||||
const changedFilesFor = deps.changedFilesFor || defaultChangedFilesFor;
|
||||
const agents = [];
|
||||
for (const session of sessions || []) {
|
||||
const files = changedFilesFor(session).map(p => ({ path: p, weight: 1 }));
|
||||
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]));
|
||||
return {
|
||||
enabled: true,
|
||||
advisories: scan.advisories.map(adv => ({
|
||||
...adv,
|
||||
aLabel: labels.get(adv.a) || adv.a,
|
||||
bLabel: labels.get(adv.b) || adv.b
|
||||
})),
|
||||
positions: scan.positions,
|
||||
links: scan.links,
|
||||
counts: scan.counts
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildProximitySnapshot,
|
||||
sessionsToAgents,
|
||||
defaultChangedFilesFor
|
||||
};
|
||||
@ -633,6 +633,14 @@ async function buildControlPaneSnapshot(options = {}) {
|
||||
const entities = readEntities(db);
|
||||
const observations = readObservations(db);
|
||||
const relationCounts = readRelationCounts(db);
|
||||
// Proximity (agent-space collision avoidance) is opt-in: it shells `git diff`
|
||||
// per worktree, so we only compute it when explicitly requested to keep the
|
||||
// default snapshot fast.
|
||||
let proximity = null;
|
||||
if (options.includeProximity) {
|
||||
const { buildProximitySnapshot } = require('./proximity');
|
||||
proximity = buildProximitySnapshot(sessions, { repoRoot, ...(options.proximityOptions || {}) });
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
summary: summarizeSessions(sessions),
|
||||
@ -649,7 +657,8 @@ async function buildControlPaneSnapshot(options = {}) {
|
||||
limit
|
||||
})
|
||||
},
|
||||
connectors: connectorStatus(config, db)
|
||||
connectors: connectorStatus(config, db),
|
||||
proximity
|
||||
};
|
||||
} finally {
|
||||
db.close();
|
||||
|
||||
91
tests/lib/control-pane-proximity.test.js
Normal file
91
tests/lib/control-pane-proximity.test.js
Normal file
@ -0,0 +1,91 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Tests for the control-pane proximity integration (sessions -> airspace scan).
|
||||
*/
|
||||
|
||||
const assert = require('assert');
|
||||
|
||||
const { buildProximitySnapshot, sessionsToAgents } = require('../../scripts/lib/control-pane/proximity');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` PASS ${name}`);
|
||||
passed += 1;
|
||||
} catch (e) {
|
||||
console.log(` FAIL ${name}`);
|
||||
console.log(` ${e.message}`);
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const sessions = [
|
||||
{
|
||||
id: 'lead-hermes',
|
||||
task: 'Build the API',
|
||||
createdAt: '2026-06-19T10:00:00Z',
|
||||
worktree: { path: '/wt/lead', base: 'main' }
|
||||
},
|
||||
{
|
||||
id: 'worker-kb',
|
||||
task: 'Also touch the API',
|
||||
createdAt: '2026-06-19T10:05:00Z',
|
||||
worktree: { path: '/wt/worker', base: 'main' }
|
||||
},
|
||||
{
|
||||
id: 'docs-bot',
|
||||
task: 'Write docs',
|
||||
createdAt: '2026-06-19T10:06:00Z',
|
||||
worktree: { path: '/wt/docs', base: 'main' }
|
||||
},
|
||||
{ id: 'no-worktree', task: 'idle', createdAt: '2026-06-19T10:07:00Z', worktree: null }
|
||||
];
|
||||
|
||||
// Injected working sets: lead + worker both edit the same API file (collision);
|
||||
// docs-bot edits an unrelated file (clear).
|
||||
const changedFilesFor = session =>
|
||||
({
|
||||
'lead-hermes': ['src/api/users.js'],
|
||||
'worker-kb': ['src/api/users.js'],
|
||||
'docs-bot': ['docs/guide.md'],
|
||||
'no-worktree': []
|
||||
})[session.id] || [];
|
||||
|
||||
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']);
|
||||
assert.strictEqual(agents.find(a => a.agentId === 'lead-hermes').files[0].path, 'src/api/users.js');
|
||||
});
|
||||
|
||||
test('buildProximitySnapshot: same-file editors get a resolution; the later one steers', () => {
|
||||
const prox = buildProximitySnapshot(sessions, { changedFilesFor, graph: { adjacency: {} } });
|
||||
assert.strictEqual(prox.enabled, true);
|
||||
assert.strictEqual(prox.counts.agents, 3);
|
||||
const collision = prox.advisories.find(a => [a.a, a.b].includes('lead-hermes') && [a.a, a.b].includes('worker-kb'));
|
||||
assert.ok(collision, 'lead/worker should produce an advisory');
|
||||
assert.strictEqual(collision.level, 'resolution', `level ${collision.level} risk ${collision.risk}`);
|
||||
// lead started earlier ⇒ holds; worker steers.
|
||||
assert.strictEqual(collision.steer, 'worker-kb');
|
||||
assert.strictEqual(collision.hold, 'lead-hermes');
|
||||
// docs-bot is clear of both.
|
||||
assert.ok(!prox.advisories.some(a => a.a === 'docs-bot' || a.b === 'docs-bot'));
|
||||
// every participating agent gets a 3D position.
|
||||
assert.strictEqual(prox.positions.length, 3);
|
||||
});
|
||||
|
||||
test('buildProximitySnapshot: fewer than two participants ⇒ no advisories', () => {
|
||||
const single = buildProximitySnapshot([sessions[0]], { changedFilesFor });
|
||||
assert.strictEqual(single.counts.agents, 1);
|
||||
assert.strictEqual(single.advisories.length, 0);
|
||||
});
|
||||
|
||||
test('buildProximitySnapshot: advisories carry human-readable labels', () => {
|
||||
const prox = buildProximitySnapshot(sessions, { changedFilesFor, graph: { adjacency: {} } });
|
||||
const collision = prox.advisories[0];
|
||||
assert.ok(collision.aLabel && collision.bLabel, 'labels present');
|
||||
});
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
if (failed > 0) process.exit(1);
|
||||
Loading…
x
Reference in New Issue
Block a user