Integrate canonical architecture: two-process backend + LiveKit agent
Promote all 12 staged canonical files from docs/generated/files/ to their
live paths, creating the full PodMan architecture:
Backend:
- server.ts: HTTP service (token mint, sync-PR, outcome recording, /health, WS relay)
- agent.ts: worker joining LiveKit room, grabbing screenshare frames at ~1fps
- agent/podman.ts: orchestrator loop (vision -> collision detection -> intervention)
- env.ts: flat env var accessors replacing nested stub
- vision/gemini.ts: JPEG -> Gemini vision -> EngineerContext (real implementation)
- collision/detector.ts: fused vision+GitHub collision detection (the moat)
- github/client.ts: Octokit wrapper with caching + sync PR creation
- memory/store.ts: extended with recordObservation/recordCollision/recordIntervention/recordOutcome helpers
- memory/vectors.ts: stub for Voyage+Atlas vector recall (Loop A)
- memory/policy.ts: stub for intervention policy gate (Loop B)
- voice/live.ts: stub for Gemini Live TTS voice output
Shared:
- messages.ts: LiveKit data-channel wire protocol (DataMessage, InterventionOutcome, TeamModel, LocalGitReport)
- index.ts: re-exports messages module
Frontend:
- livekit/useScreenPublish.ts: hook for joining pod and publishing screenshare
- livekit/useInterventions.ts: hook for receiving collision cards and responding
- lib/api.ts: fetchToken + postOutcome HTTP helpers
Database:
- database/init.ts: MongoDB Atlas collections + indexes + vector search index
Infra:
- infra/.do/app.yaml: DO App Platform spec (static_site + service + worker)
Retire stubs superseded by canonical decomposition:
- backend/src/index.ts (replaced by server.ts)
- backend/src/intervention/engine.ts (logic now in agent/podman.ts)
- backend/src/livekit/token.ts (token minting now in server.ts)
Install missing dependencies: @livekit/rtc-node, sharp, mongodb, ws, @types/ws
Type error fixes:
- vision/gemini.ts: use MediaResolution.MEDIA_RESOLUTION_LOW enum value (not string literal)
- agent/podman.ts: wrap SuggestedActionKind into { kind: action } SuggestedAction object
All packages pass pnpm -r typecheck and pnpm -r build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,50 +1,40 @@
|
||||
import type { Collision, EngineerContext, GithubStateSnapshot } from '@podman/shared';
|
||||
import type { EngineerContext, Collision, GithubStateSnapshot } from '@podman/shared';
|
||||
|
||||
function normalize(path?: string): string | undefined {
|
||||
if (!path) return undefined;
|
||||
return path.replace(/^\.?\/?(src\/)?/, 'src/').toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuse live engineer contexts (from vision) with GitHub state to find overlaps:
|
||||
* two or more engineers editing the same file — especially when unpushed.
|
||||
*
|
||||
* Pure function: easy to unit-test, no I/O. Callers supply the GitHub snapshot.
|
||||
*/
|
||||
export function detectCollisions(
|
||||
contexts: EngineerContext[],
|
||||
githubStateByFile: Record<string, GithubStateSnapshot> = {},
|
||||
now: string = new Date().toISOString(),
|
||||
github: GithubStateSnapshot,
|
||||
): Collision[] {
|
||||
const byFile = new Map<string, EngineerContext[]>();
|
||||
for (const ctx of contexts) {
|
||||
if (!ctx.currentFile) continue;
|
||||
const list = byFile.get(ctx.currentFile) ?? [];
|
||||
list.push(ctx);
|
||||
byFile.set(ctx.currentFile, list);
|
||||
for (const c of contexts) {
|
||||
const f = normalize(c.currentFile);
|
||||
if (!f) continue;
|
||||
(byFile.get(f) ?? byFile.set(f, []).get(f)!).push(c);
|
||||
}
|
||||
|
||||
const collisions: Collision[] = [];
|
||||
const out: Collision[] = [];
|
||||
for (const [file, group] of byFile) {
|
||||
if (group.length < 2) continue;
|
||||
|
||||
const engineers = [...new Set(group.map((g) => g.engineerId))];
|
||||
if (engineers.length < 2) continue;
|
||||
|
||||
const github = githubStateByFile[file];
|
||||
const unpushed = group.some((g) => g.hasUnpushedChanges) || github?.unpushed === true;
|
||||
const sharedSymbol = group.every(
|
||||
(g) => g.currentSymbol && g.currentSymbol === group[0]!.currentSymbol,
|
||||
)
|
||||
? group[0]!.currentSymbol
|
||||
: undefined;
|
||||
const anyUnpushed =
|
||||
group.some((g) => g.hasUnpushedChanges) || github.unpushed === true;
|
||||
if (!anyUnpushed) continue; // the crux GitHub alone cannot answer
|
||||
|
||||
collisions.push({
|
||||
id: `${group[0]!.podId}:${file}:${engineers.sort().join(',')}`,
|
||||
out.push({
|
||||
id: `col_${file}_${Date.now()}`,
|
||||
podId: group[0]!.podId,
|
||||
file,
|
||||
symbol: sharedSymbol,
|
||||
symbol: group.find((g) => g.currentSymbol)?.currentSymbol,
|
||||
engineers,
|
||||
severity: unpushed ? 'critical' : sharedSymbol ? 'warn' : 'info',
|
||||
githubState: github,
|
||||
detectedAt: now,
|
||||
severity: 'warn',
|
||||
githubState: { ...github, unpushed: anyUnpushed },
|
||||
detectedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return collisions;
|
||||
return out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user