feat(viz): kind-specific choreography and a finished mission you can read
ci / gates (push) Failing after 6s
ci / rust (push) Skipped
ci / frontend (push) Skipped
ci / e2e (push) Skipped
ci / publish (push) Skipped

Security: the pawns already orbited their destination, so homing them at
the security station gave circling for free. This adds the radial
press-and-retreat — an agent closing on the target and backing off reads
as probing it, where a fixed radius reads as waiting — and holds the
stochastic target release while probing, or the circling breaks up into
stray trips that look like distraction rather than a scan.

Findings are `mission_tasks` rows, one orb each, popped once. There is
deliberately no severity anywhere in the path: the scanner keeps
severity, file and line as substrings inside `title`, so a severity
parsed out of prose and rendered as an orb's RADIUS would be the picture
asserting a measurement the data never contained. Count only.

Benchmarks annotate the station, as text. `delta` has no schema —
compute_delta emits `{kind:"opaque"}` whenever the before/after metrics
were not structurally comparable, which is most drivers. The server
formats the shape it can parse and COUNTS the rest; an unparseable driver
reports "3 sample(s)" rather than an invented improvement, and an opaque
delta says nothing at all.

The finished map: the live label rule gates service/event nodes on
`heat > 0.12`, which is exactly backwards once everything has cooled — a
static map would be unlabelled dots. Frozen, the 25 most-touched nodes
label regardless of heat, phase stations carry a second line counting
what they produced, and the camera is released ONCE so it frames the
result even if the user panned during the run.

Every count in a caption is read off the drawn scene rather than a
parallel tally, so the words and the picture cannot disagree.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-11 09:22:16 -07:00
co-authored by Claude Opus 5
parent 9e61e3ba35
commit f8438c32ea
4 changed files with 433 additions and 8 deletions
+214
View File
@@ -283,6 +283,137 @@ async fn world_acts(pool: &PgPool, ws: WorkspaceId, only: Option<Uuid>, after: i
.collect() .collect()
} }
/// One security finding, as the scanner recorded it.
struct FindingRow {
mission_id: String,
phase_id: String,
task_id: String,
title: String,
}
/// Findings raised by a security phase.
///
/// They are `mission_tasks` rows — the scanner's own store — not a new table.
/// There is deliberately NO severity field here: severity, file and line are
/// substrings inside `title` (see `security_scan.rs`), and a severity parsed
/// out of prose and then encoded as an orb's RADIUS would be an invented fact
/// rendered as a measurement. The title is shown as written.
async fn world_findings(pool: &PgPool, ws: WorkspaceId, only: Option<Uuid>) -> Vec<FindingRow> {
let rows = sqlx::query(
"SELECT t.mission_id::text AS mission_id,
t.phase_id::text AS phase_id,
t.id::text AS task_id,
t.title
FROM mission_tasks t
JOIN mission_phases p ON p.id = t.phase_id
JOIN missions m ON m.id = t.mission_id
WHERE m.workspace_id = $1
AND p.kind = 'security_scan'
AND ( m.status = 'running'
OR ( m.status IN ('completed', 'failed')
AND m.completed_at > now() - interval '24 hours' ) )
AND ($2::uuid IS NULL OR m.id = $2)
LIMIT 300",
)
.bind(ws.as_uuid())
.bind(only)
.fetch_all(pool)
.await
.unwrap_or_default();
rows.into_iter()
.map(|r| FindingRow {
mission_id: r.get::<String, _>("mission_id"),
phase_id: r.get::<String, _>("phase_id"),
task_id: r.get::<String, _>("task_id"),
title: r.get::<String, _>("title"),
})
.collect()
}
/// A benchmark phase's result, summarised.
struct BenchRow {
mission_id: String,
phase_id: String,
note: String,
}
/// Benchmark deltas, as an annotation on the phase — not as nodes.
///
/// `delta` is a `Record<string, unknown>` with no schema: `compute_delta`
/// produces `{kind:"bencher_diff", samples:[…]}` when the before/after shapes
/// are structurally comparable, and `{kind:"opaque"}` when they are not. Only
/// the shape that can be parsed is formatted; the rest is COUNTED, never
/// guessed at, so a driver whose output we do not understand reports "3
/// samples" rather than an invented improvement.
async fn world_benchmarks(pool: &PgPool, ws: WorkspaceId, only: Option<Uuid>) -> Vec<BenchRow> {
let rows = sqlx::query(
"SELECT DISTINCT ON (b.phase_id)
b.mission_id::text AS mission_id,
b.phase_id::text AS phase_id,
b.delta
FROM benchmark_snapshots b
JOIN missions m ON m.id = b.mission_id
WHERE m.workspace_id = $1
AND b.delta IS NOT NULL
AND ( m.status = 'running'
OR ( m.status IN ('completed', 'failed')
AND m.completed_at > now() - interval '24 hours' ) )
AND ($2::uuid IS NULL OR m.id = $2)
ORDER BY b.phase_id, b.iteration DESC",
)
.bind(ws.as_uuid())
.bind(only)
.fetch_all(pool)
.await
.unwrap_or_default();
rows.into_iter()
.filter_map(|r| {
let note = benchmark_note(&r.get::<serde_json::Value, _>("delta"))?;
Some(BenchRow {
mission_id: r.get::<String, _>("mission_id"),
phase_id: r.get::<String, _>("phase_id"),
note,
})
})
.collect()
}
/// One line describing a benchmark delta, or `None` if there is nothing
/// truthful to say about it.
fn benchmark_note(delta: &serde_json::Value) -> Option<String> {
let samples = delta.get("samples").and_then(|s| s.as_array())?;
if samples.is_empty() {
return None;
}
let mut improved = 0usize;
let mut regressed = 0usize;
// Best (most negative) percent change, since that is the one claim the
// shape actually supports.
let mut best: Option<f64> = None;
for s in samples {
match s.get("direction").and_then(|d| d.as_str()) {
Some("improved") => improved += 1,
Some("regressed") => regressed += 1,
// Counted in the total but claimed for neither side.
_ => {}
}
if let Some(pct) = s.get("delta_pct").and_then(serde_json::Value::as_f64) {
best = Some(best.map_or(pct, |b: f64| b.min(pct)));
}
}
let mut parts = vec![format!("{} sample(s)", samples.len())];
if improved > 0 {
parts.push(format!("{improved} faster"));
}
if regressed > 0 {
parts.push(format!("{regressed} slower"));
}
if let Some(b) = best.filter(|b| *b < 0.0) {
parts.push(format!("best {:.1}%", b));
}
Some(parts.join(", "))
}
/// Agents that execute a phase of this kind, via the purposes the phase runner /// Agents that execute a phase of this kind, via the purposes the phase runner
/// itself uses. Returns empty for a teamless (microVM) mission. /// itself uses. Returns empty for a teamless (microVM) mission.
async fn phase_agents( async fn phase_agents(
@@ -612,6 +743,11 @@ pub async fn world_live(
// opening a finished mission would replay an hour of tool calls as a // opening a finished mission would replay an hour of tool calls as a
// burst storm and read as a mission that just did all of it at once. // burst storm and read as a mission that just did all of it at once.
let mut event_cursor: i64 = -1; let mut event_cursor: i64 = -1;
// Findings already announced. A finding is raised once.
let mut last_finding: HashSet<String> = HashSet::new();
// Last benchmark summary per phase; a looping phase produces a new one.
let mut last_bench: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
// Audit-log cursor for edge-initiated inter-agent events (delegation, // Audit-log cursor for edge-initiated inter-agent events (delegation,
// A2A) that bypass the run loop. -1 until seeded on the first pass. // A2A) that bypass the run loop. -1 until seeded on the first pass.
let mut audit_cursor: i64 = -1; let mut audit_cursor: i64 = -1;
@@ -792,6 +928,42 @@ pub async fn world_live(
); );
} }
// Benchmark results, as an annotation on the station. Re-emitted
// only when the summary changes: a phase that loops produces a new
// snapshot per iteration, and that IS news.
for b in world_benchmarks(&pool, ws, only_mission).await {
if last_bench.get(&b.phase_id).map(|n| n == &b.note).unwrap_or(false) {
continue;
}
last_bench.insert(b.phase_id.clone(), b.note.clone());
yield sse(
"mission.benchmark",
json!({
"missionId": b.mission_id,
"phaseId": b.phase_id,
"note": b.note,
}),
);
}
// Findings, once each, hanging off the security station. Same rule
// as files: a finding is a fact that was raised once, and
// re-sending it would pop the orb again on every poll.
for f in world_findings(&pool, ws, only_mission).await {
if !last_finding.insert(f.task_id.clone()) {
continue;
}
yield sse(
"mission.finding",
json!({
"missionId": f.mission_id,
"phaseId": f.phase_id,
"findingId": f.task_id,
"title": f.title,
}),
);
}
// Structured activity — the motion channel. Tool calls and file // Structured activity — the motion channel. Tool calls and file
// touches recorded at the source by the container tap and the // touches recorded at the source by the container tap and the
// microVM `PostToolUse` hook. Never parsed from prose: a tool name // microVM `PostToolUse` hook. Never parsed from prose: a tool name
@@ -1110,6 +1282,48 @@ mod mission_feed_tests {
); );
} }
/// A benchmark delta is described only as far as its shape allows.
///
/// `compute_delta` emits `{kind:"opaque"}` whenever the before/after
/// metrics were not structurally comparable — which is most drivers. The
/// temptation is to say something anyway; the result would be a performance
/// claim the picture makes and the data does not support.
#[test]
fn a_benchmark_is_described_only_as_far_as_its_shape_allows() {
use serde_json::json;
assert_eq!(
super::benchmark_note(&json!({
"kind": "bencher_diff",
"samples": [
{"name": "a", "delta_pct": -12.5, "direction": "improved"},
{"name": "b", "delta_pct": 3.0, "direction": "regressed"},
// No direction and no percentage: counted, claimed for
// neither side.
{"name": "c"},
],
}))
.as_deref(),
Some("3 sample(s), 1 faster, 1 slower, best -12.5%")
);
// Nothing comparable happened, so nothing is said.
assert_eq!(
super::benchmark_note(&json!({ "kind": "opaque", "note": "not comparable" })),
None
);
assert_eq!(
super::benchmark_note(&json!({ "kind": "bencher_diff", "samples": [] })),
None
);
// Everything got slower: no "best" claim at all.
assert_eq!(
super::benchmark_note(&json!({
"samples": [{"name": "a", "delta_pct": 8.0, "direction": "regressed"}],
}))
.as_deref(),
Some("1 sample(s), 1 slower")
);
}
/// Agent→phase attribution must come from the runner's own mapping. /// Agent→phase attribution must come from the runner's own mapping.
#[test] #[test]
fn phase_attribution_reuses_the_runners_mapping() { fn phase_attribution_reuses_the_runners_mapping() {
+128 -5
View File
@@ -102,6 +102,41 @@ const mono = "'Geist Mono', ui-monospace, monospace";
/// Phase kind → what a person calls it. Mirrors the mission list so the two /// Phase kind → what a person calls it. Mirrors the mission list so the two
/// surfaces do not name the same phase differently. /// surfaces do not name the same phase differently.
/// What a station produced, counted from the scene the viewer is looking at.
///
/// Derived from the engine's own nodes rather than from a parallel tally: a
/// second count is free to disagree with the orbs on screen, and the caption
/// would then be confidently wrong about a picture the reader can see.
function phaseSubLabel(engine: WorldEngine, phaseId: string): string {
let files = 0;
let findings = 0;
for (const n of engine.nodes.values()) {
if (n.id.startsWith("finding:")) {
// Findings hang directly off the station that raised them.
if (n.parentId === phaseId) findings += 1;
continue;
}
if (!n.id.startsWith("file:")) continue;
// Files nest through a directory chain, so walk up to the station.
let cur: string | null = n.parentId;
for (let hop = 0; cur && hop < 12; hop++) {
if (cur === phaseId) {
files += 1;
break;
}
cur = engine.nodes.get(cur)?.parentId ?? null;
}
}
const parts: string[] = [];
const note = engine.nodes.get(phaseId)?.note;
if (note) parts.push(note);
if (files) parts.push(`${files} file${files === 1 ? "" : "s"}`);
// Count only. Severity lives inside the finding's title as prose, so a
// breakdown here would be invented — see the `mission.finding` contract.
if (findings) parts.push(`${findings} finding${findings === 1 ? "" : "s"}`);
return parts.join(", ");
}
const PHASE_LABEL: Record<string, string> = { const PHASE_LABEL: Record<string, string> = {
research: "Research", research: "Research",
coding: "Coding", coding: "Coding",
@@ -130,7 +165,16 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
const [speedMult, setSpeedMult] = useState(1); const [speedMult, setSpeedMult] = useState(1);
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
const replayHours = 24; const replayHours = 24;
const [hud, setHud] = useState({ agents: 0, active: 0, phases: 0, done: 0, stale: 0 }); const [hud, setHud] = useState({
agents: 0,
active: 0,
phases: 0,
done: 0,
stale: 0,
files: 0,
findings: 0,
finished: false,
});
const telemetry = useLiveState<"telemetry", { tokensPerMin?: number; doorsPending?: number; loops?: number }>( const telemetry = useLiveState<"telemetry", { tokensPerMin?: number; doorsPending?: number; loops?: number }>(
"telemetry", "telemetry",
(d) => d, (d) => d,
@@ -148,6 +192,8 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
/// Accumulated mission plan. Kept in a ref rather than state: it is written /// Accumulated mission plan. Kept in a ref rather than state: it is written
/// from SSE handlers many times a second and only ever read to push into the /// from SSE handlers many times a second and only ever read to push into the
/// engine, so re-rendering on every phase update would be pure cost. /// engine, so re-rendering on every phase update would be pure cost.
/// Whether the camera has already been released once for the finished map.
const reframedRef = useRef(false);
const planRef = useRef<{ const planRef = useRef<{
missionId: string | null; missionId: string | null;
title: string; title: string;
@@ -210,7 +256,24 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
// is working" and "this mission stopped and nobody noticed". // is working" and "this mission stopped and nobody noticed".
if ((n.heatFloor ?? 0) > 0 && (n.heatFloor ?? 0) <= 0.16) stale += 1; if ((n.heatFloor ?? 0) > 0 && (n.heatFloor ?? 0) <= 0.16) stale += 1;
} }
setHud({ agents: e.pawns.size, active, phases, done, stale }); // The finished-map summary. Counted from the drawn scene so the caption
// and the picture cannot disagree.
let files = 0;
let findings = 0;
for (const n of e.nodes.values()) {
if (n.id.startsWith("file:")) files += 1;
else if (n.id.startsWith("finding:")) findings += 1;
}
setHud({
agents: e.pawns.size,
active,
phases,
done,
stale,
files,
findings,
finished: e.frozen,
});
}, 600); }, 600);
return () => clearInterval(id); return () => clearInterval(id);
}, []); }, []);
@@ -302,6 +365,9 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
// pawn homes to the mission centre and the scene is a clump no matter // pawn homes to the mission centre and the scene is a clump no matter
// how many stations it has. // how many stations it has.
for (const a of d.agentIds ?? []) e.setHome(a, `phase:${d.phaseId}`); for (const a of d.agentIds ?? []) e.setHome(a, `phase:${d.phaseId}`);
// A security station is probed, not rested at — the pawns circle it and
// press in. Only while it is running: a finished scan is a map.
e.setProbe(`phase:${d.phaseId}`, d.kind === "security_scan" && d.status === "running");
pushPlan(); pushPlan();
}), }),
// The files a phase actually touched. Scoped by mission id, not by // The files a phase actually touched. Scoped by mission id, not by
@@ -311,6 +377,14 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
if (focusMissionId && d.missionId !== focusMissionId) return; if (focusMissionId && d.missionId !== focusMissionId) return;
e.onMissionFile(d); e.onMissionFile(d);
}), }),
live.on("mission.finding", (d) => {
if (focusMissionId && d.missionId !== focusMissionId) return;
e.onMissionFinding(d);
}),
live.on("mission.benchmark", (d) => {
if (focusMissionId && d.missionId !== focusMissionId) return;
e.onMissionBenchmark(d);
}),
live.on("room.message", (d) => { live.on("room.message", (d) => {
if (!d.fromAgentId || !inFocus(d.fromAgentId)) return; if (!d.fromAgentId || !inFocus(d.fromAgentId)) return;
for (const pid of d.participantIds ?? []) { for (const pid of d.participantIds ?? []) {
@@ -1090,6 +1164,14 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
updateParticles(dt); updateParticles(dt);
// The moment the mission finishes, the scene stops moving and becomes a
// thing to look at — so frame it once, even if the user had panned during
// the run. Once only: re-releasing the camera every frame would fight
// them for control of a map they are trying to read.
if (engine.frozen && !reframedRef.current) {
reframedRef.current = true;
userInteracted = false;
}
// camera: auto-frame until the user grabs it (then OrbitControls owns it) // camera: auto-frame until the user grabs it (then OrbitControls owns it)
if (!userInteracted && minX < maxX) { if (!userInteracted && minX < maxX) {
const cx = (minX + maxX) / 2; const cx = (minX + maxX) / 2;
@@ -1104,6 +1186,21 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
// labels (project to screen). In repo focus mode we skip labels for // labels (project to screen). In repo focus mode we skip labels for
// hidden nodes so they don't leak past the culling. // hidden nodes so they don't leak past the culling.
const wantLabels = new Map<string, { x: number; y: number; text: string; color: string; big: boolean }>(); const wantLabels = new Map<string, { x: number; y: number; text: string; color: string; big: boolean }>();
// A finished mission is a map to READ. The live rule below labels a
// service/event node only while `heat > 0.12`, which is exactly backwards
// for a static map: every orb has cooled, so the map would be unlabelled
// dots. Frozen, the most-touched nodes are labelled regardless of heat —
// capped, because a coding phase can leave hundreds of file orbs and all
// of them labelled is not a map either.
const settledLabels = engine.frozen
? new Set(
[...engine.nodes.values()]
.filter((n) => n.tier === "service" || n.tier === "event")
.sort((a, b) => (b.touchCount ?? 0) - (a.touchCount ?? 0))
.slice(0, 25)
.map((n) => n.id),
)
: null;
for (const n of engine.nodes.values()) { for (const n of engine.nodes.values()) {
if (n.tier === "root") continue; if (n.tier === "root") continue;
if (visibleNodes && !visibleNodes.has(n.id)) continue; if (visibleNodes && !visibleNodes.has(n.id)) continue;
@@ -1117,9 +1214,22 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
n.tier === "team" || n.tier === "team" ||
n.tier === "mission" || n.tier === "mission" ||
n.tier === "phase"; n.tier === "phase";
const hot = (n.tier === "service" || n.tier === "event") && (n.heat > 0.12 || n.id === selectedRef.current); const hot =
if (struct || hot) (n.tier === "service" || n.tier === "event") &&
wantLabels.set(n.id, { x: n.x, y: n.y - n.r - 6, text: n.label, color: n.color, big: n.tier === "org" }); (n.heat > 0.12 || n.id === selectedRef.current || !!settledLabels?.has(n.id));
if (struct || hot) {
// Phase stations carry a second line: what the station produced. Read
// off the scene itself rather than a parallel count, so it cannot
// disagree with what is drawn.
const sub = n.tier === "phase" ? phaseSubLabel(engine, n.id) : "";
wantLabels.set(n.id, {
x: n.x,
y: n.y - n.r - 6,
text: sub ? `${n.label} · ${sub}` : n.label,
color: n.color,
big: n.tier === "org",
});
}
} }
if (showPawns) if (showPawns)
for (const p of engine.pawns.values()) for (const p of engine.pawns.values())
@@ -1372,6 +1482,19 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
{hud.stale} quiet {hud.stale} quiet
</span> </span>
) : null} ) : null}
{hud.files ? (
<span title="files this mission touched">
{hud.files} file{hud.files === 1 ? "" : "s"}
</span>
) : null}
{/* Count only — the scanner keeps severity as prose inside the
finding's title, so a severity breakdown here would be invented. */}
{hud.findings ? (
<span style={{ color: "#ff5f57" }}>
{hud.findings} finding{hud.findings === 1 ? "" : "s"}
</span>
) : null}
{hud.finished ? <span style={{ color: "#5fd08a" }}>finished</span> : null}
{telemetry.tokensPerMin ? <span>{Math.round(telemetry.tokensPerMin / 1000)}k tok/m</span> : null} {telemetry.tokensPerMin ? <span>{Math.round(telemetry.tokensPerMin / 1000)}k tok/m</span> : null}
{telemetry.doorsPending ? ( {telemetry.doorsPending ? (
<span style={{ color: "#e8b465" }}> <span style={{ color: "#e8b465" }}>
+65 -3
View File
@@ -54,6 +54,9 @@ export interface GNode {
* persistent "heat map" tint so files touched many times stay visibly * persistent "heat map" tint so files touched many times stay visibly
* warmer than virgin files even at heat=0. */ * warmer than virgin files even at heat=0. */
touchCount: number; touchCount: number;
/** A one-line annotation drawn beside this node's label — currently a
* benchmark station's result. Text only, on purpose. */
note?: string;
} }
export interface GPawn { export interface GPawn {
@@ -429,6 +432,54 @@ export class WorldEngine {
} }
} }
/// A security finding, hanging off the station that raised it.
///
/// Rendered as one orb per finding and NOTHING else. The scanner keeps
/// severity, file and line as substrings inside `title`, so there is no
/// severity to encode — and a radius scaled by a severity parsed out of prose
/// would be the picture asserting a measurement the data never contained.
onMissionFinding(e: TaxonomyEvents["mission.finding"]) {
const id = `finding:${e.findingId}`;
const existed = this.nodes.has(id);
const parent = this.nodes.has(`phase:${e.phaseId}`) ? `phase:${e.phaseId}` : ROOT;
const depth = (this.nodes.get(parent)?.depth ?? 0) + 1;
const n = this.ensureNode(id, "event", e.title, parent, depth);
n.label = e.title;
n.lastSeen = this.now;
if (!existed) {
// The explosive pop, once. A finding is raised once; re-popping it on a
// reconnect would read as the scanner finding it again.
n.burst = 1;
n.heat = 1;
n.lastActivityAt = this.now;
}
}
/// A benchmark station's result, as an annotation on the orb.
///
/// Text, not geometry. Encoding a speedup as radius or colour would make the
/// picture assert a magnitude, and the underlying `delta` has no schema that
/// supports one — see the `mission.benchmark` contract.
onMissionBenchmark(e: TaxonomyEvents["mission.benchmark"]) {
const n = this.nodes.get(`phase:${e.phaseId}`);
if (n) n.note = e.note;
}
/// Stations whose agents circle and press in rather than resting.
///
/// Security is the one kind whose work IS the choreography: probes at a
/// target, over and over. The pawns already orbit their destination, so
/// homing them here buys the circling for free; all this adds is the radial
/// press-and-retreat and a hold on the stochastic target release, so a
/// probing agent does not wander off mid-pass.
private probing = new Set<string>();
/// Mark a phase node as a probe target (or not).
setProbe(nodeId: string, on: boolean) {
if (on) this.probing.add(nodeId);
else this.probing.delete(nodeId);
}
/// Where a pawn rests when it is not touching anything. /// Where a pawn rests when it is not touching anything.
/// ///
/// This is what dissolves the clump. `homes` was written only by `seed()`, so /// This is what dissolves the clump. `homes` was written only by `seed()`, so
@@ -764,8 +815,16 @@ export class WorldEngine {
// Frozen: pawns settle at fixed angles around their last station rather // Frozen: pawns settle at fixed angles around their last station rather
// than orbiting forever, so a finished map is still rather than restless. // than orbiting forever, so a finished map is still rather than restless.
const ang = (this.frozen ? 0 : this.now * 1.4) + (p.id.charCodeAt(0) || 0); const ang = (this.frozen ? 0 : this.now * 1.4) + (p.id.charCodeAt(0) || 0);
const ox = dest.x + Math.cos(ang) * (dest.r + 22); // Probing: press in and pull back. A security agent circling at a fixed
const oy = dest.y + Math.sin(ang) * (dest.r + 22); // radius reads as waiting; the same agent closing on the target and
// backing off reads as probing it, which is what it is doing.
const probe =
!this.frozen && this.probing.has(dest.id)
? Math.sin(this.now * 2.2 + (p.id.charCodeAt(1) || 0)) * 16
: 0;
const radius = dest.r + 22 - probe;
const ox = dest.x + Math.cos(ang) * radius;
const oy = dest.y + Math.sin(ang) * radius;
p.vx += (ox - p.x) * dt * 3; p.vx += (ox - p.x) * dt * 3;
p.vy += (oy - p.y) * dt * 3; p.vy += (oy - p.y) * dt * 3;
for (const q of pawns) { for (const q of pawns) {
@@ -790,7 +849,10 @@ export class WorldEngine {
target.heat = Math.min(1, target.heat + dt * 1.5); target.heat = Math.min(1, target.heat + dt * 1.5);
if (Math.random() < dt * 5) if (Math.random() < dt * 5)
this.beams.push({ x1: p.x, y1: p.y, x2: target.x, y2: target.y, color: p.color, life: 1 }); this.beams.push({ x1: p.x, y1: p.y, x2: target.x, y2: target.y, color: p.color, life: 1 });
if (Math.random() < dt * 0.7) p.targetId = null; // arrived; release // Arrived; release — UNLESS this is a probe. Releasing there would
// send the agent home mid-pass and the circling would break up into
// stray trips, which looks like distraction rather than a scan.
if (!this.probing.has(target.id) && Math.random() < dt * 0.7) p.targetId = null;
} }
} }
p.idle += dt; p.idle += dt;
+26
View File
@@ -107,6 +107,27 @@ export interface TaxonomyEvents {
status?: string; status?: string;
source?: "diff" | "tool"; source?: "diff" | "tool";
}; };
/** World → one orb per security finding, hanging off the security station.
*
* There is deliberately no `severity`. The scanner writes severity, file and
* line as substrings INSIDE `title`, so a severity here would be parsed out
* of prose and then rendered as a measurement — a number the picture asserts
* and the data never contained. Structured severity needs a column written
* at the source. Ephemeral: raised once, and the engine keeps the orb. */
"mission.finding": {
missionId: string;
phaseId: string;
findingId: string;
title: string;
};
/** World → a one-line summary annotated on a benchmark station.
*
* A summary, not the delta: `delta` is a `Record<string, unknown>` with no
* schema, and only the `bencher_diff` shape can be read at all. The server
* formats what it can parse and COUNTS the rest, so an unparseable driver
* reports "3 samples" rather than an invented improvement. Stateful per
* phase — a late subscriber must see the result the station already has. */
"mission.benchmark": { missionId: string; phaseId: string; note: string };
/** World → re-layout (add/remove org units, agents, projects). */ /** World → re-layout (add/remove org units, agents, projects). */
"topology.update": { "topology.update": {
formation: "hierarchy" | "flat" | "live"; formation: "hierarchy" | "flat" | "live";
@@ -152,6 +173,8 @@ export const TAXONOMY_TYPES: TaxonomyType[] = [
"mission.update", "mission.update",
"mission.phase", "mission.phase",
"mission.file", "mission.file",
"mission.finding",
"mission.benchmark",
"topology.update", "topology.update",
"telemetry", "telemetry",
"routine.update", "routine.update",
@@ -167,6 +190,7 @@ export const STATEFUL_TYPES = new Set<TaxonomyType>([
"node.activity", "node.activity",
"mission.update", "mission.update",
"mission.phase", "mission.phase",
"mission.benchmark",
"telemetry", "telemetry",
"topology.update", "topology.update",
"routine.update", "routine.update",
@@ -183,6 +207,8 @@ export function stateKey<T extends TaxonomyType>(type: T, d: TaxonomyPayload<T>)
// moment it is pinned rather than on the next phase transition. // moment it is pinned rather than on the next phase transition.
if (type === "mission.update") return `${type}:${(d as TaxonomyEvents["mission.update"]).missionId}`; if (type === "mission.update") return `${type}:${(d as TaxonomyEvents["mission.update"]).missionId}`;
if (type === "mission.phase") return `${type}:${(d as TaxonomyEvents["mission.phase"]).phaseId}`; if (type === "mission.phase") return `${type}:${(d as TaxonomyEvents["mission.phase"]).phaseId}`;
if (type === "mission.benchmark")
return `${type}:${(d as TaxonomyEvents["mission.benchmark"]).phaseId}`;
if (type === "telemetry") { if (type === "telemetry") {
const a = (d as TaxonomyEvents["telemetry"]).agentId; const a = (d as TaxonomyEvents["telemetry"]).agentId;
return a ? `telemetry:${a}` : "telemetry"; // per-agent slice vs workspace-wide return a ? `telemetry:${a}` : "telemetry"; // per-agent slice vs workspace-wide