chore: format repo and fix lint config

This commit is contained in:
Yahya Alhinai
2026-06-28 00:09:48 +00:00
parent 1294b8428d
commit b1b0761973
23 changed files with 198 additions and 104 deletions
+49 -16
View File
@@ -1,10 +1,12 @@
# CLAUDE.md
## Mission
You are the execution copilot for a 24-hour hackathon project with **12 hours max effective build time left**.
Your job is not to be creative for creativitys sake. Your job is to help the team **ship one technically impressive, demo-stable, judge-friendly project** under extreme time pressure.
Optimize for:
- fast execution
- technical depth that is visible in a 3-minute demo
- originality beyond generic AI wrappers
@@ -13,9 +15,10 @@ Optimize for:
Do not encourage side quests, overengineering, or speculative future work.
***
---
## Hackathon context
Use these facts as hard constraints:
- The project is for a hackathon focused on **self-improving AI, continual learning, agent infrastructure, and recursive intelligence**.
@@ -32,9 +35,10 @@ Use these facts as hard constraints:
- generic analyzers/coaches in banned categories
- The project should feel like a **real agent system that improves from usage**, not a prompt demo.
***
---
## Required stack for this workspace
Assume the team is using:
- **DigitalOcean** for hosting / deployment
@@ -45,6 +49,7 @@ Assume the team is using:
Prefer solutions that make these technologies central, not decorative.
### Stack roles
Use this default mapping unless explicitly changed:
- **Gemini**
@@ -70,9 +75,10 @@ Use this default mapping unless explicitly changed:
If proposing architecture, keep it aligned with this stack.
***
---
## Project strategy rules
Always reason from these principles:
1. **One killer workflow beats five weak ones.**
@@ -84,6 +90,7 @@ Always reason from these principles:
7. **Do not optimize for completeness. Optimize for a convincing end-to-end loop.**
When asked for ideas, prefer:
- B2B or developer tooling
- agent infrastructure
- self-improving workflows
@@ -92,18 +99,21 @@ When asked for ideas, prefer:
- strong before/after demo moments
Reject ideas that are:
- broad consumer apps
- generic copilots
- “chat with your data” tools
- mostly static dashboards
- impossible to build in 12 hours
***
---
## Execution mode
When helping in this workspace, be brutally practical.
### Always do these
- force prioritization
- identify the shortest demoable path
- separate **must-have**, **nice-to-have**, and **cut**
@@ -112,39 +122,47 @@ When helping in this workspace, be brutally practical.
- prefer mocked or constrained environments over flaky real-world integrations if demo reliability improves
### Never do these
- suggest large refactors unless absolutely necessary
- encourage adding multiple product surfaces
- recommend training a meaningful large model from scratch
- pretend a weak feature is impressive
- propose “future work” as if it helps judging
***
---
## Time constraint protocol
Assume **12 effective build hours remain** unless told otherwise.
Every recommendation must pass this filter:
### Must-have test
Can this be built and demo-polished in <= 12 hours?
If not, simplify or kill it.
### Priority buckets
Whenever planning features, organize into:
#### Must-have demo path
The minimum end-to-end flow required for a winning demo.
#### Nice-to-have
Useful only if the core path is done early.
#### Cut immediately
Features that sound good but jeopardize shipping.
Default behavior: if uncertain, put it in **Cut immediately**.
***
---
## Preferred architecture pattern
Unless there is a strong reason not to, bias toward this system shape:
1. **User provides a task / goal**
@@ -158,6 +176,7 @@ Unless there is a strong reason not to, bias toward this system shape:
This is the default “self-improving loop.”
### Good examples of improvement signals
- task success/failure
- test pass rate
- latency or step count
@@ -166,7 +185,9 @@ This is the default “self-improving loop.”
- human thumbs up/down only if necessary, but prefer automatic verification
### Best demo structure
A strong demo usually shows:
- Run 1 fails or is mediocre
- System inspects memory / trace
- System updates strategy
@@ -174,12 +195,14 @@ A strong demo usually shows:
That is more convincing than a static success case.
***
---
## Working style for code help
When asked to help code, optimize for momentum.
### Code generation rules
- Write small, composable files.
- Prefer boring reliable frameworks over fancy ones.
- Minimize moving parts.
@@ -188,12 +211,14 @@ When asked to help code, optimize for momentum.
- Add logging for every agent step, tool call, verifier result, and memory write.
### Debugging rules
- Start from the smallest failing surface.
- Propose the most likely root cause first.
- Give concrete fixes, not broad theories.
- Prefer copy-pasteable commands and patches.
### UI rules
- UI exists to support the demo.
- It should clearly show:
- current task
@@ -204,9 +229,10 @@ When asked to help code, optimize for momentum.
- Do not build a dashboard-heavy product shell.
- Avoid Streamlit.
***
---
## How to evaluate proposals
If asked whether something is a good idea, score it explicitly on:
- **Problem Quality** — is the pain real and costly?
@@ -218,9 +244,10 @@ If asked whether something is a good idea, score it explicitly on:
If an idea is weak, say so directly and suggest a tighter version.
***
---
## Strong defaults for this workspace
Unless told otherwise, assume the team should build something in this family:
- self-improving agent workflow
@@ -231,9 +258,10 @@ Unless told otherwise, assume the team should build something in this family:
Do not drift into generic SaaS CRUD apps.
***
---
## Demo-first development order
If asked what to build next, default to this sequence:
1. Define one narrow workflow
@@ -247,9 +275,10 @@ If asked what to build next, default to this sequence:
If something threatens steps 1-6, cut it.
***
---
## Communication style
Be direct, concise, and critical.
Do not be sycophantic.
Do not praise mediocre ideas.
@@ -258,14 +287,17 @@ When there is risk, say exactly what the risk is.
When there is a simpler path, recommend it.
Preferred response pattern:
1. verdict
2. why
3. what to do now
***
---
## If asked to choose between options
Default decision criteria:
- faster to demo
- easier to explain
- more visually impressive
@@ -274,14 +306,15 @@ Default decision criteria:
If two options are close, choose the one with the **better live demo**.
***
---
## Non-negotiable reminder
The team does **not** have time to build a platform.
The team has time to build **one sharp, memorable, end-to-end loop**.
Everything should serve that outcome.
***
---
## Documentation-first enforcement — HARD RULE
@@ -299,7 +332,7 @@ This is not a guideline. This is a gate.
**Do not write the code.** Instead:
1. Say explicitly: *"This isn't in the current plan. Let me understand what you're trying to do."*
1. Say explicitly: _"This isn't in the current plan. Let me understand what you're trying to do."_
2. Ask what problem they're solving and whether it's required for the demo path.
3. Evaluate whether it fits within scope or replaces something planned.
4. If it's valid: **update `docs/PLAN.md` and the relevant spec first**, then proceed to code.
@@ -317,7 +350,7 @@ This is not a guideline. This is a gate.
4 engineers are building simultaneously. If one person deviates from the plan, others build against wrong assumptions. An unplanned change to `backend/src/index.ts` can silently break another engineer's work mid-build. The docs are the contract between teammates — Claude's job is to enforce them, not work around them.
***
---
## Team context — 4 people working simultaneously
+2 -1
View File
@@ -43,7 +43,8 @@ async function main() {
room.on(
RoomEvent.TrackSubscribed,
(track: RemoteTrack, pub: RemoteTrackPublication, participant: RemoteParticipant) => {
if (track.kind !== TrackKind.KIND_VIDEO || pub.source !== TrackSource.SOURCE_SCREENSHARE) return;
if (track.kind !== TrackKind.KIND_VIDEO || pub.source !== TrackSource.SOURCE_SCREENSHARE)
return;
const id = participant.identity;
const stream = new VideoStream(track);
void (async () => {
+11 -7
View File
@@ -26,9 +26,12 @@ export class PodMan {
const msg = JSON.parse(new TextDecoder().decode(payload)) as DataMessage;
if (msg.type === 'GIT_REPORT') {
const c = this.contexts.get(msg.report.engineerId);
if (c) c.hasUnpushedChanges = msg.report.unpushedCount > 0 || msg.report.dirtyFiles.length > 0;
if (c)
c.hasUnpushedChanges = msg.report.unpushedCount > 0 || msg.report.dirtyFiles.length > 0;
}
} catch { /* ignore malformed */ }
} catch {
/* ignore malformed */
}
});
}
@@ -58,7 +61,8 @@ export class PodMan {
await recordCollision(collision);
const action = preferredAction(collision, prior);
const names = collision.engineers.join(' and ');
const message = `${names} are both editing ${collision.file}` +
const message =
`${names} are both editing ${collision.file}` +
(collision.githubState?.unpushed ? ' and one has unpushed changes.' : '.') +
(prior ? ` I've seen this conflict pattern before.` : '');
@@ -75,10 +79,10 @@ export class PodMan {
await recordIntervention(intervention);
const data: DataMessage = { type: 'COLLISION', collision, intervention };
await this.room.localParticipant?.publishData(
this.encoder.encode(JSON.stringify(data)),
{ reliable: true, topic: DATA_TOPIC },
);
await this.room.localParticipant?.publishData(this.encoder.encode(JSON.stringify(data)), {
reliable: true,
topic: DATA_TOPIC,
});
await speak(this.room, message); // gemini-3.1-flash-live voice into the room
}
}
+1 -2
View File
@@ -21,8 +21,7 @@ export function detectCollisions(
const engineers = [...new Set(group.map((g) => g.engineerId))];
if (engineers.length < 2) continue;
const anyUnpushed =
group.some((g) => g.hasUnpushedChanges) || github.unpushed === true;
const anyUnpushed = group.some((g) => g.hasUnpushedChanges) || github.unpushed === true;
if (!anyUnpushed) continue; // the crux GitHub alone cannot answer
out.push({
+6 -1
View File
@@ -33,7 +33,12 @@ export async function createSyncPr(input: { headBranch: string; file: string; su
const { owner, repo } = repoParts();
const { data: mainRef } = await gh.rest.git.getRef({ owner, repo, ref: 'heads/main' });
const branch = `podman-sync-${Date.now()}`;
await gh.rest.git.createRef({ owner, repo, ref: `refs/heads/${branch}`, sha: mainRef.object.sha });
await gh.rest.git.createRef({
owner,
repo,
ref: `refs/heads/${branch}`,
sha: mainRef.object.sha,
});
const { data: pr } = await gh.rest.pulls.create({
owner,
repo,
+2 -8
View File
@@ -4,10 +4,7 @@ import type { Collision, SuggestedActionKind } from '@podman/shared';
* Policy gate: decides whether PodMan should intervene.
* Stub: always intervene on warn/critical.
*/
export function shouldIntervene(
collision: Collision,
_prior: unknown,
): boolean {
export function shouldIntervene(collision: Collision, _prior: unknown): boolean {
return collision.severity !== 'info';
}
@@ -15,9 +12,6 @@ export function shouldIntervene(
* Preferred action selection based on collision + prior history.
* Stub: open sync PR for critical, ping teammate otherwise.
*/
export function preferredAction(
collision: Collision,
_prior: unknown,
): SuggestedActionKind {
export function preferredAction(collision: Collision, _prior: unknown): SuggestedActionKind {
return collision.severity === 'critical' ? 'open_sync_pr' : 'ping_teammate';
}
+6 -2
View File
@@ -15,11 +15,15 @@ async function persist(name: string, fn: () => Promise<unknown>): Promise<void>
}
export async function recordObservation(ctx: EngineerContext): Promise<void> {
await persist('observation', async () => (await collections()).observations.insertOne({ ...ctx }));
await persist('observation', async () =>
(await collections()).observations.insertOne({ ...ctx }),
);
}
export async function recordCollision(collision: Collision): Promise<void> {
await persist('collision', async () => (await collections()).collisions.insertOne({ ...collision }));
await persist('collision', async () =>
(await collections()).collisions.insertOne({ ...collision }),
);
}
export async function recordIntervention(intervention: Intervention): Promise<void> {
+2 -1
View File
@@ -39,7 +39,8 @@ function cleanMembers(members: unknown): string[] {
for (const raw of members) {
if (typeof raw !== 'string') throw new Error('member names must be strings');
const name = raw.trim();
if (name.length > MAX_MEMBER_LEN) throw new Error(`member name too long (max ${MAX_MEMBER_LEN})`);
if (name.length > MAX_MEMBER_LEN)
throw new Error(`member name too long (max ${MAX_MEMBER_LEN})`);
const key = name.toLowerCase();
if (name && !seen.has(key)) {
seen.add(key);
+22 -5
View File
@@ -7,13 +7,28 @@ const ai = new GoogleGenAI({ apiKey: env.GEMINI_API_KEY });
const SCHEMA = {
type: Type.OBJECT,
properties: {
currentFile: { type: Type.STRING, description: 'open file path if visible, e.g. src/auth/session.ts' },
currentFile: {
type: Type.STRING,
description: 'open file path if visible, e.g. src/auth/session.ts',
},
currentSymbol: { type: Type.STRING, description: 'function/class under the cursor' },
activity: { type: Type.STRING, description: 'editing | reading | debugging | terminal | PR review' },
hasUnpushedChanges: { type: Type.BOOLEAN, description: 'dirty git gutter / modified markers visible' },
activity: {
type: Type.STRING,
description: 'editing | reading | debugging | terminal | PR review',
},
hasUnpushedChanges: {
type: Type.BOOLEAN,
description: 'dirty git gutter / modified markers visible',
},
confidence: { type: Type.NUMBER, description: '0..1 confidence in this read' },
},
propertyOrdering: ['currentFile', 'currentSymbol', 'activity', 'hasUnpushedChanges', 'confidence'],
propertyOrdering: [
'currentFile',
'currentSymbol',
'activity',
'hasUnpushedChanges',
'confidence',
],
} as const;
export async function analyzeFrame(
@@ -27,7 +42,9 @@ export async function analyzeFrame(
{
role: 'user',
parts: [
{ text: "You are PodMan watching an engineer's screen. Identify what file/symbol they are working on and whether there are uncommitted edits. JSON only." },
{
text: "You are PodMan watching an engineer's screen. Identify what file/symbol they are working on and whether there are uncommitted edits. JSON only.",
},
{ inlineData: { mimeType: 'image/jpeg', data: jpeg.toString('base64') } },
],
},
+6 -6
View File
@@ -6,12 +6,12 @@ This is what makes PodMan "more useful the more you use it" — the ownership ma
## Collections
| Collection | Holds | Notes |
|---|---|---|
| `engineer_states` | Latest `EngineerContext` per engineer | Upserted on every `/ingest` call |
| `ownership_map` | File → primary owner + contributors | Persists across sessions — the continual learning artifact |
| `events` | Detected coordination events (DEPENDENCY_READY, BLOCKER_DETECTED, DUPLICATE_WORK) | Append-only |
| `nudges` | Every voice nudge sent to the room | Used for cooldown checks (3 min between nudges) |
| Collection | Holds | Notes |
| ----------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `engineer_states` | Latest `EngineerContext` per engineer | Upserted on every `/ingest` call |
| `ownership_map` | File → primary owner + contributors | Persists across sessions — the continual learning artifact |
| `events` | Detected coordination events (DEPENDENCY_READY, BLOCKER_DETECTED, DUPLICATE_WORK) | Append-only |
| `nudges` | Every voice nudge sent to the room | Used for cooldown checks (3 min between nudges) |
Full schemas with indexes in [`docs/mongodb.md`](../docs/mongodb.md).
+7 -2
View File
@@ -26,7 +26,9 @@ async function main() {
name: 'vector_index',
type: 'vectorSearch',
definition: {
fields: [{ type: 'vector', path: 'embedding', numDimensions: 1024, similarity: 'cosine' }],
fields: [
{ type: 'vector', path: 'embedding', numDimensions: 1024, similarity: 'cosine' },
],
},
},
],
@@ -38,4 +40,7 @@ async function main() {
console.log('PodMan DB initialized.');
await client.close();
}
main().catch((e) => { console.error(e); process.exit(1); });
main().catch((e) => {
console.error(e);
process.exit(1);
});
+18 -12
View File
@@ -18,18 +18,21 @@ Pre-stage checklist for the 3-minute live demo. Do this on all 3 laptops before
## Laptop setup (all 3 machines)
### Editor settings
- Font size: **18pt or larger** — Gemini Vision must read file names and code
- Single editor window — no split panes, no overlapping terminals
- File tab visible with full file name shown (not truncated)
- Light or dark theme is fine — avoid low-contrast themes
### Browser
- Chrome (best `getDisplayMedia` support)
- PWA tab open and joined to `demo-pod`
- Earbuds / headphones plugged in and tested
- Volume: medium — PodMan voice should be clearly audible but not startle
### Screen layout
- Editor takes 2/3 of screen
- Terminal takes bottom 1/3 (always visible)
- No other windows on top
@@ -41,14 +44,17 @@ Pre-stage checklist for the 3-minute live demo. Do this on all 3 laptops before
Pre-create these files in the demo repo before the demo:
**Alice's machine:**
- Open `auth/middleware.ts` — has visible function stubs
- Terminal shows nothing running initially, then `Server running on :3001` at the right moment
**Bob's machine:**
- Open `frontend/login.tsx` — has visible form component code
- Terminal idle
**Carol's machine:**
- Open `frontend/integration.ts` or similar
- Terminal shows: `curl http://localhost:3001/auth``curl: (7) Failed to connect`
@@ -56,18 +62,18 @@ Pre-create these files in the demo repo before the demo:
## Demo script timing
| Time | Action | Who |
|---|---|---|
| 0:00 | All three join `demo-pod` | All |
| 0:05 | PodMan greets by voice | Hermes auto |
| 0:20 | Alice opens `auth/middleware.ts`, starts typing | Alice |
| 0:45 | Bob opens `frontend/login.tsx` | Bob |
| 0:50 | Carol runs `curl` command, sees error | Carol |
| ~1:20 | BLOCKER_DETECTED nudge fires | Hermes auto |
| 1:50 | Alice starts her server (`node server.js`) | Alice |
| ~2:00 | DEPENDENCY_READY nudge fires | Hermes auto |
| 2:20 | Optional: show session 2 ownership warm-start | Presenter |
| 2:45 | Close | Presenter |
| Time | Action | Who |
| ----- | ----------------------------------------------- | ----------- |
| 0:00 | All three join `demo-pod` | All |
| 0:05 | PodMan greets by voice | Hermes auto |
| 0:20 | Alice opens `auth/middleware.ts`, starts typing | Alice |
| 0:45 | Bob opens `frontend/login.tsx` | Bob |
| 0:50 | Carol runs `curl` command, sees error | Carol |
| ~1:20 | BLOCKER_DETECTED nudge fires | Hermes auto |
| 1:50 | Alice starts her server (`node server.js`) | Alice |
| ~2:00 | DEPENDENCY_READY nudge fires | Hermes auto |
| 2:20 | Optional: show session 2 ownership warm-start | Presenter |
| 2:45 | Close | Presenter |
---
+1
View File
@@ -114,6 +114,7 @@ static_sites:
## Fallback plan (if App Platform deploy fails on stage)
Run Hermes locally:
```bash
cd backend && pnpm dev
```
+3
View File
@@ -104,6 +104,7 @@ Respond with the message text only.
```
**Example output:**
> "Carol — Alice just got the auth endpoint running. You're clear to integrate."
---
@@ -115,6 +116,7 @@ Respond with the message text only.
**Integration:** LiveKit Agents framework — Hermes runs as a LiveKit Agent with Gemini Live 2.5 as the voice provider
**Flow:**
1. Nudge message text generated (step 3)
2. Hermes passes text to Gemini Live via LiveKit Agents
3. Gemini Live streams audio back in real-time
@@ -122,6 +124,7 @@ Respond with the message text only.
5. All participants hear it through their audio output
**Why Gemini Live (not plain TTS):**
- Streams audio directly — no intermediate WAV file conversion
- Latency ~300500ms from text to first audio packet
- Natural-sounding voice
+5 -5
View File
@@ -86,8 +86,8 @@ Each engineer opens a browser PWA on their laptop. The PWA captures a screen fra
## Prize alignment
| Prize | How PodMan earns it |
|---|---|
| Best Gemini 3.5 / 2.5 | Gemini Vision for screen understanding + Gemini Live 2.5 for voice output |
| Best LiveKit | LiveKit is the real-time backbone for room presence and voice delivery — load-bearing, not decorative |
| Best DigitalOcean | Hermes deployed on DigitalOcean App Platform; MongoDB Atlas on DO-adjacent infrastructure |
| Prize | How PodMan earns it |
| --------------------- | ----------------------------------------------------------------------------------------------------- |
| Best Gemini 3.5 / 2.5 | Gemini Vision for screen understanding + Gemini Live 2.5 for voice output |
| Best LiveKit | LiveKit is the real-time backbone for room presence and voice delivery — load-bearing, not decorative |
| Best DigitalOcean | Hermes deployed on DigitalOcean App Platform; MongoDB Atlas on DO-adjacent infrastructure |
+7
View File
@@ -16,17 +16,20 @@ LiveKit is the real-time backbone for PodMan. It handles room presence and voice
## Engineer side (PWA)
**Joining:**
1. PWA calls `POST /pods/:podId/token` → receives `{ token, url }`
2. LiveKit client connects to the room with the token
3. PWA publishes screen track via `getDisplayMedia` (used client-side for frame capture — Hermes does NOT subscribe to this track)
4. PWA sets mic enabled for ambient presence
**Receiving:**
- LiveKit client automatically receives Hermes audio track
- No special subscription needed — LiveKit delivers audio to all participants
- PWA also listens for data channel messages from Hermes for UI card updates
**Data channel listener (PWA):**
```ts
room.on(RoomEvent.DataReceived, (payload, participant) => {
if (participant?.identity !== 'podman-hermes') return;
@@ -43,17 +46,20 @@ room.on(RoomEvent.DataReceived, (payload, participant) => {
**Framework:** LiveKit Agents (Node.js)
**Startup:**
1. Hermes mints its own token via the same `createPodToken` function with `identity: 'podman-hermes'`
2. Connects to the room on pod creation / first engineer joining
3. Registers as a LiveKit Agent with Gemini Live 2.5 as voice provider
**Voice delivery:**
1. Nudge message text is ready (from Gemini text generation)
2. Hermes passes text to Gemini Live 2.5 via LiveKit Agents voice pipeline
3. Audio streams into the room in real-time
4. All participants hear it
**Data channel message (sent alongside audio):**
```ts
const nudge = {
type: 'DEPENDENCY_READY' | 'BLOCKER_DETECTED' | 'DUPLICATE_WORK',
@@ -75,6 +81,7 @@ room.localParticipant.publishData(
Already implemented at `POST /pods/:podId/token`.
Hermes uses the same endpoint. Grants:
- `roomJoin: true`
- `canPublish: true` (for audio track)
- `canPublishData: true` (for data channel)
+1
View File
@@ -63,6 +63,7 @@ Tracks who works on which files. Built up over the session. **Persists across se
**Index:** `{ podId: 1, file: 1 }` (unique)
**Upsert logic:**
- On each context update where `currentFile` is non-null:
- Increment `observationCount`
- Update `primaryOwner` to the engineer with the most recent `lastSeenAt` on this file
@@ -46,6 +46,7 @@ PodMan is a real-time AI team coordination agent for software teams. Engineers j
## Components
### PWA (local agent)
- Joins LiveKit room via existing `joinPod` flow
- Captures frame every 30s via `getDisplayMedia`, compresses to JPEG (1280×720, quality 0.7)
- POSTs `{ engineerId, podId, screenshotBase64, capturedAt }` to `POST /ingest`
@@ -54,6 +55,7 @@ PodMan is a real-time AI team coordination agent for software teams. Engineers j
- Two screens: join screen (built), active session screen (to build)
### Hermes (orchestrator)
- Express server + LiveKit Agent on DigitalOcean
- `POST /ingest`: receives frame, queues for vision
- Vision pipeline: Gemini 2.0 Flash → `EngineerContext`
@@ -66,18 +68,21 @@ PodMan is a real-time AI team coordination agent for software teams. Engineers j
- Cooldown: 3 min between nudges per pod
### Gemini usage
- **Vision:** `gemini-2.0-flash` — screen → `{ currentFile, inferredTask, terminalVisible, recentTerminalOutput, confidence }`
- **Event detection:** `gemini-2.0-flash` — all engineer states → `{ event, involvedEngineers, file, reason }`
- **Nudge generation:** `gemini-2.0-flash` — event → spoken message text
- **Voice:** `gemini-live-2.5-flash` via LiveKit Agents — text → streaming audio
### MongoDB Atlas (4 collections)
- `engineer_states`: latest context per engineer, upserted each ingest
- `ownership_map`: file → primaryOwner + contributors, persists across sessions (continual learning)
- `events`: all detected coordination events
- `nudges`: all voice nudges sent + cooldown history
### LiveKit
- One room per pod
- Engineers publish screen track (used client-side for capture — Hermes does not subscribe)
- Hermes joins as `podman-hermes`, publishes audio + data channel messages
@@ -87,17 +92,18 @@ PodMan is a real-time AI team coordination agent for software teams. Engineers j
## Event types
| Event | Trigger | Example nudge |
|---|---|---|
| Event | Trigger | Example nudge |
| ------------------ | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `BLOCKER_DETECTED` | Engineer stuck (error in terminal, same file N frames) + teammate can help | "Carol, looks like you're waiting on auth. Alice is actively building it — hang tight." |
| `DEPENDENCY_READY` | Engineer A completes work that Engineer B was waiting on | "Carol, Bob — Alice just got the auth endpoint running. You're clear to integrate." |
| `DUPLICATE_WORK` | 2+ engineers on same file simultaneously | "Alice and Bob — you're both in login.tsx. Coordinate before pushing." |
| `DEPENDENCY_READY` | Engineer A completes work that Engineer B was waiting on | "Carol, Bob — Alice just got the auth endpoint running. You're clear to integrate." |
| `DUPLICATE_WORK` | 2+ engineers on same file simultaneously | "Alice and Bob — you're both in login.tsx. Coordinate before pushing." |
---
## Continual learning story
The `ownership_map` collection persists across sessions. On Hermes startup:
1. Load ownership map for this pod from Atlas
2. Build in-memory cache: `Map<file, { primaryOwner, contributors }>`
3. Event detection uses priors immediately — no ramp-up phase
@@ -120,10 +126,10 @@ The `ownership_map` collection persists across sessions. On Hermes startup:
## Key risks
| Risk | Mitigation |
|---|---|
| Gemini Vision accuracy | Large font, single editor window, confidence gate |
| Gemini Live 2.5 + LiveKit Agents integration | Build together hour 57, have TTS fallback |
| Frame POST latency | JPEG compression, target < 500ms |
| Event false positives | 3-min cooldown, pre-staged demo |
| DO deploy failure | Hermes runs local, PWA defaults to localhost:8787 |
| Risk | Mitigation |
| -------------------------------------------- | ------------------------------------------------- |
| Gemini Vision accuracy | Large font, single editor window, confidence gate |
| Gemini Live 2.5 + LiveKit Agents integration | Build together hour 57, have TTS fallback |
| Frame POST latency | JPEG compression, target < 500ms |
| Event false positives | 3-min cooldown, pre-staged demo |
| DO deploy failure | Hermes runs local, PWA defaults to localhost:8787 |
+11
View File
@@ -7,6 +7,17 @@ export default tseslint.config(
},
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['scripts/**/*.mjs'],
languageOptions: {
globals: {
console: 'readonly',
process: 'readonly',
setTimeout: 'readonly',
URL: 'readonly',
},
},
},
{
rules: {
'@typescript-eslint/no-unused-vars': [
+2 -11
View File
@@ -192,20 +192,11 @@ export default function App() {
</header>
{joinedPod ? (
<PodView
team={joinedPod}
me={member}
room={room}
devMode={devMode}
onLeave={handleLeave}
/>
<PodView team={joinedPod} me={member} room={room} devMode={devMode} onLeave={handleLeave} />
) : showReconnecting ? (
<div className="flex flex-col items-start gap-3">
<p className="text-sm text-slate-400">Reconnecting to your pod</p>
<button
className="text-xs text-slate-500 hover:text-slate-300"
onClick={handleLeave}
>
<button className="text-xs text-slate-500 hover:text-slate-300" onClick={handleLeave}>
Cancel
</button>
</div>
+4 -4
View File
@@ -170,9 +170,7 @@ export function PodView({
onClick={toggleBeat}
disabled={!room}
className={`rounded-md px-4 py-2 text-sm font-medium disabled:opacity-50 ${
playingBeat
? 'bg-red-600 hover:bg-red-500'
: 'bg-emerald-600 hover:bg-emerald-500'
playingBeat ? 'bg-red-600 hover:bg-red-500' : 'bg-emerald-600 hover:bg-emerald-500'
}`}
>
{playingBeat ? '⏹ Stop beat' : '▶ Play beat'}
@@ -185,7 +183,9 @@ export function PodView({
{sharing ? '🛑 Stop sharing' : '📺 Share my screen'}
</button>
<span className="ml-auto text-xs text-slate-400">
{playingBeat ? '🔊 broadcasting beat to the pod' : 'press “Play beat” — everyone should hear it'}
{playingBeat
? '🔊 broadcasting beat to the pod'
: 'press “Play beat” — everyone should hear it'}
</span>
</section>
{note && <p className="text-sm text-amber-400">{note}</p>}
+3 -1
View File
@@ -15,7 +15,9 @@ export function useInterventions(room: Room | null) {
if (msg.type === 'COLLISION') setActive(msg.intervention);
};
room.on(RoomEvent.DataReceived, onData);
return () => { room.off(RoomEvent.DataReceived, onData); };
return () => {
room.off(RoomEvent.DataReceived, onData);
};
}, [room]);
const respond = useCallback(
+11 -8
View File
@@ -7,14 +7,17 @@ export function useScreenPublish() {
const [connected, setConnected] = useState(false);
const [sharing, setSharing] = useState(false);
const join = useCallback(async (pod: string, identity: string, name: string, githubLogin?: string) => {
const { token, url } = await fetchToken({ room: pod, identity, name, githubLogin });
const room = new Room({ adaptiveStream: true, dynacast: true });
await room.connect(url, token);
roomRef.current = room;
setConnected(true);
return room;
}, []);
const join = useCallback(
async (pod: string, identity: string, name: string, githubLogin?: string) => {
const { token, url } = await fetchToken({ room: pod, identity, name, githubLogin });
const room = new Room({ adaptiveStream: true, dynacast: true });
await room.connect(url, token);
roomRef.current = room;
setConnected(true);
return room;
},
[],
);
const startSharing = useCallback(async () => {
const room = roomRef.current;