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