- docs/plans/research-overlap.md: self-contained build spec for code-edit vs research cross-channel overlap nudge (vision classifies editing vs research, semantic embedding match, collaboration nudge). Ready for an implementing agent. - CLAUDE.md: replace branch-first habit with push-directly-to-main policy plus mandatory pull --rebase before push for concurrent teammates. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LuV8W8oNYRsDWKoqK8Mkqc
8.8 KiB
Build spec: cross-channel overlap (code-edit ↔ research nudge)
Self-contained spec for an implementing agent. Everything needed to build is here: current-state anchors, exact edits, code sketches, verification. Read the referenced files before editing.
Context / why
Today PodMan only fires when two engineers touch the same file (basename
match) with unpushed changes — backend/src/collision/detector.ts:36. That makes
the system feel like a one-file trick. The wow we want: detect a code↔research
overlap — one teammate is editing livekit.py while another is researching
the same topic in a browser (LiveKit docs/SDK). PodMan nudges:
🤝 bob is deep in LiveKit docs while you edit livekit.py — sync up before duplicating effort.
This reframes PodMan from merge-conflict detector to a team-coordination agent that catches duplicated effort / knowledge overlap — stronger continual-learning story, distinct demo beat.
Locked decisions
- Signal capture: Gemini vision on the existing LiveKit screenshare. When a
teammate shares a browser/docs window, vision classifies it
researchand extracts{researchTopic, researchSource}. No browser extension, no new client surface. Reusesbackend/src/agent.ts+backend/src/vision/gemini.ts. - Matching: semantic embeddings (reuse
embed()+cosine()inbackend/src/memory/vectors.ts). Deterministic stem/keyword fallback fires when an embed call returnsnull, so the demo path never depends on a live vector call. - Framing: collaboration nudge (
ping_teammate), spoken once for the beat.
Current-state anchors (read these first)
backend/src/collision/detector.ts:14-20—fileKey()stem/basename logic to mirror.backend/src/collision/detector.ts:36-89—detectCollisions()(same-file path; leave unchanged).backend/src/agent/podman.ts:85-116—onScreenFrame()orchestration;:124-126conflictKey();:128-183handle().backend/src/agent/podman.ts:38-46—engineersOverlapOnFile()(git ground-truth; research must skip it).backend/src/vision/gemini.ts:7-70—SCHEMA, prompt, andEngineerContextmapping.backend/src/memory/vectors.ts:52-66cosine(),:68-70privateembed().backend/src/action/hermes.ts:46-62—publishHermesIntervention()(speaks only ifvoiceLinepassed).frontend/src/livekit/useInterventions.ts:36-48— data-channel handler (rendersintervention.messageverbatim).shared/src/engineer.ts:6-23EngineerContext;shared/src/collision.ts:7-28Collision.
Off-spec gate: nothing in docs/ covers cross-channel overlap. Update specs
in step 1 before code (repo documentation-first rule).
Concurrency / safety (several people build at once)
- Shared-contract edits are additive optional fields only — no signature changes. Safe.
backend/src/agent/podman.tsis hot: two in-place edits (conflictKey,handlebranch).git pull --rebasebefore pushing.- No required frontend change — nudge text rides in
intervention.message, already rendered byfrontend/src/components/PodView.tsx.
Implementation steps
1. Specs first
docs/gemini.md— vision classifieseditingvsresearch; extractsresearchTopic/researchSource.docs/hermes.md— new intervention type research overlap (collaboration nudge,ping_teammate, spoken once); explicitly NOT a merge conflict.docs/mongodb.md— new optionalCollisionfields.docs/demo.md— insert ~30s beat after the same-file collision.
2. Shared contract (additive, optional)
shared/src/engineer.ts — add to EngineerContext:
mode?: 'editing' | 'research';
researchTopic?: string;
researchSource?: string; // domain, e.g. "docs.livekit.io"
shared/src/collision.ts — add to Collision:
overlapKind?: 'file' | 'research'; // undefined = file (preserves current behavior)
researchTopic?: string;
researchSource?: string;
researcher?: string; // engineer doing research
editor?: string; // engineer editing the file
3. Vision: classify editing vs research
backend/src/vision/gemini.ts — extend SCHEMA with mode, researchTopic,
researchSource (+ propertyOrdering). Update the prompt so a browser/docs/SDK
frame returns mode:'research' + topic + source domain, else mode:'editing'
with existing IDE fields. Map new fields into the returned EngineerContext.
Keep thinkingConfig.thinkingBudget:0 and MEDIA_RESOLUTION_LOW.
4. Semantic matcher helper (reuse embed/cosine)
backend/src/memory/vectors.ts — add and export:
export async function semanticSimilarity(a: string, b: string): Promise<number | null> {
const [va, vb] = await Promise.all([embed(a, 'query'), embed(b, 'document')]);
if (!va || !vb) return null;
return cosine(va, vb);
}
5. New detector (additive file)
backend/src/env.ts — add RESEARCH_OVERLAP_THRESHOLD (default 0.6).
backend/src/collision/research.ts (new):
export interface ResearchOpts { similarity?: (a: string, b: string) => Promise<number | null>; threshold?: number; }
export async function detectResearchOverlaps(
contexts: EngineerContext[],
gitStates: Map<string, GitState> | undefined,
opts: ResearchOpts = {},
): Promise<Collision[]>;
Logic:
- researchers = contexts with
mode==='research'&&researchTopic. - editor files = ground truth from
gitStates[*].changedFiles(reliable) plus anymode==='editing'currentFile; each tagged with its engineer. - For each distinct (researcher, editor) pair on a file, score
similarity("${topic} ${source}", "<file stem words> <symbol> <activity>")(defaultsimilarity = semanticSimilarity). Fire whenscore >= threshold(defaultRESEARCH_OVERLAP_THRESHOLD). - Fallback: if
score === null, deterministic stem/token overlap (mirrorfileKey()stemming) — guaranteeslivekit↔livekit.pyfires offline. - Dedupe to best-scoring file per researcher; require distinct engineers.
- Emit
Collision:id: col_research_<stem>_<Date.now()>,engineers:[editor, researcher],file:<editor file>,severity:'warn',overlapKind:'research', plusresearcher,editor,researchTopic,researchSource.
6. Wire into orchestrator — backend/src/agent/podman.ts
- In
onScreenFrame, afterdetectCollisions(...)(line ~100):(concat before the re-arm + handle loops atconst research = await detectResearchOverlaps([...this.contexts.values()], gitStates); const collisions = [...fileCollisions, ...research];:104/:111/:115). conflictKey()(:124) — namespace by overlap kind so research and file overlaps on the same file don't share an edge-trigger key:return `${collision.overlapKind ?? 'file'}:${comparableBasename(collision.file)}`;gitOverlaploop (:111-113) — guard: only callengineersOverlapOnFilewhencollision.overlapKind !== 'research'(researcher won't have the file dirty; leavegitOverlapundefined for research).handle()(:128) — branch onoverlapKind === 'research':message:`🤝 ${researcher} is researching ${researchTopic}` + (researchSource ? ` (${researchSource})` : '') + ` while ${editor} edits ${shortFile} — sync up before duplicating effort.`voiceLine:`${researcher} is researching ${researchTopic} while ${editor} works on ${shortFile}. Worth a quick sync.`suggestedAction.kind = 'ping_teammate'.- Pass
voiceLinetopublishHermesInterventionregardless of severity so the beat is spoken once. (For file collisions keep existingseverity === 'critical' ? voiceLine : undefined.) - Existing
recallSimilar/shouldIntervenegate stays unchanged.
7. Frontend (nice-to-have, cut if behind)
frontend/src/livekit/useInterventions.ts — capture msg.collision.overlapKind;
show a 🤝 badge on the card in PodView.tsx. Core path needs nothing.
Verification
- Build:
pnpm -r build(shared builds first — order matters). - Detector (offline, deterministic): harness calling
detectResearchOverlapswith stubbedopts.similarity:- researcher
{mode:'research', researchTopic:'LiveKit agent init', researchSource:'docs.livekit.io'}- gitState with
livekit.pydirty for another engineer → exactly oneoverlapKind:'research'collision naming both.
- gitState with
- single engineer both sides → no overlap.
similarityreturnsnull→ keyword fallback still fires onlivekit.
- researcher
- Live: run the agent (systemd on the box, or local); one participant shares a
browser tab on LiveKit docs, another keeps
livekit.pydirty (git sidecar running) → 🤝 nudge card + one-time spoken cue. Fires once (edge-triggered), re-arms after the browser closes. - Regression: same-file collision still fires, unaffected (separate
conflictKeynamespace).