chore: cut agent token burn with scoped bootstrap and handoffs

Slim always-on agent docs, default to toju-app plus targeted electron, and add HANDOFF.md plus a Cursor rule so long chats reset cleanly without agents opening new chats.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-22 21:49:06 +02:00
co-authored by Cursor
parent cdc32db30f
commit cfe10907be
8 changed files with 293 additions and 156 deletions
+36 -72
View File
@@ -1,110 +1,74 @@
# Agent Workflow & Operating Instructions
These rules apply to **all AI agents** working on this project, regardless of platform or model.
These rules apply to **all AI agents** working on this project.
Read this file at the start of every session.
**Token budget (mandatory):**
- Default scope: **`toju-app/` + targeted `electron/` + `.gitea/workflows/`** — see `/AGENTS.md`. No `server/`, `e2e/`, `website/`, `docs-site/` unless the user expands scope.
- Electron: follow the renderer → preload → one handler path; never dump the whole `electron/` tree.
- Prefer path-scoped search; one focused agent; no Bugbot / security / best-of-N unless asked.
- **Handoff > fat chats:** agents cannot open new chats. Write `agents-docs/HANDOFF.md` and ask the user to start a new chat with that file attached.
- **Models:** keep the users latest problem-solving model. Cut cost with scope + handoffs, not weaker models.
Do **not** re-read this whole file every turn after the first skim.
---
## Workflow Orchestration
### 1. Plan Mode Default
### 1. Plan only when ambiguity is real
- Enter plan mode for ANY non-trivial task (3+ steps or architectural decisions)
- If something goes sideways, STOP and re-plan immediately — don't keep pushing
- Use plan mode for verification steps, not just building
- Write detailed specs upfront to reduce ambiguity
- Plan for unclear architecture or when the user asks.
- Skip plan mode for focused, in-scope edits with clear acceptance criteria.
### 2. Subagent Strategy
### 2. Subagents sparingly
- Use subagents liberally to keep the main context window clean
- Offload research, exploration, and parallel analysis to subagents
- For complex problems, throw more compute at it via subagents
- One task per subagent for focused execution
- Default: one agent.
- Subagents only for true parallel search inside allowed paths.
- Never spawn extra review agents unless the user asks.
### 3. Self-Improvement Loop
### 3. Handoff / short sessions
The goal is a small, sharp file of project-specific rules in `agents-docs/LESSONS.md` that future sessions read and apply. The format of a lesson is defined at the top of `agents-docs/LESSONS.md` — read it before writing one.
Triggers: user says handoff / new chat; thread is long with more major work left; switching objectives.
**Read at session start.** Open `agents-docs/LESSONS.md` and apply any rules that match the work you're about to do. This is non-optional; the file exists so the same mistake isn't made twice.
Action: overwrite `agents-docs/HANDOFF.md` with `Status: active` (Goal, Completed, Changed files, Decisions, Failed approaches, Current issue, Next steps, Commands). Then stop major work and ask the user to open a new chat.
**Triggers — record a lesson when any of these happen.** Don't wait for a formal request; these are the signals:
New chat: if handoff is active, read it first; continue Next steps; do not redo Completed work.
- User says "no", "actually", "don't", "stop", "that's wrong", or "instead do X"
- User reverts, rewrites, or asks you to redo your edit
- User re-prompts you with the same or similar instruction (signal that the first attempt missed something)
- User points out a hidden constraint, past incident, or convention you didn't know
- Code review (human or `/review`) surfaces an issue caused by your approach
- You catch yourself about to do the same thing the project has been corrected on before
### 4. Self-Improvement Loop
If unsure whether it's worth recording: write it. Sharper is better than missing, and grooming the file is cheap.
**At session start:** `LESSONS-INDEX.md` only; open matching lesson bodies by tag.
**Write before reporting done.** A session that produced a correction must produce a lesson — record it in the same turn the work is completed, not "later". The `AGENTS.md` completion checklist has a line for this; don't tick the box without it.
Record a lesson + index line when corrected. Prefer fewer sharp rules (~20).
**Groom periodically.** When `agents-docs/LESSONS.md` passes ~20 entries, propose consolidations to the user — merge duplicates, delete rules that no longer apply, shorten anything vague.
### 5. CONTEXT.md upkeep
### 4. CONTEXT.md upkeep
Default: `toju-app/CONTEXT.md`. Read `electron/CONTEXT.md` only when touching Electron. Other packages only when in scope.
Read `CONTEXT.md` (or `agents-docs/CONTEXT-MAP.md` → per-subdomain `CONTEXT.md`) when working in a subdomain. Use its vocabulary verbatim **where defined** in code, tests, issues, and commits. If a needed term isn't in the glossary, treat it as a trigger (see below) rather than silently inventing a synonym; the full contract lives in `agents-docs/AGENTS_CONTEXT.md`.
### 6. ADR upkeep
**Triggers — capture vocabulary in the moment:**
Only when hard-to-reverse + surprising + real trade-offs. Contract: `agents-docs/AGENTS_ADRS.md`.
- A previously-ambiguous domain term gets a clear resolution → add it (one-sentence definition, aliases to avoid).
- User corrects your terminology → record the correct term; mark the wrong one as an alias to avoid.
- A new feature introduces a concept absent from the glossary → add it before claiming the feature done.
- You catch yourself inventing a synonym because the right term isn't there → flag the gap; don't silently coin a new term.
### 7. Verification Before Done (behavior first)
**Write before reporting done.** Update the relevant `CONTEXT.md` in the same turn the trigger fires. Append-only — add new entries, don't reshuffle existing ones. The format is documented at the top of each `CONTEXT.md`. See `agents-docs/AGENTS_CONTEXT.md` for the full contract.
Done = asked functionality works. Unit green ≠ done for product asks. Prefer in-scope proof; dont pull e2e/server “just to be sure.”
### 5. ADR upkeep
### 8. Demand Elegance (Balanced)
Read `agents-docs/adr/` when about to change anything that crosses an existing decision boundary. If your work would contradict an ADR, surface it explicitly — never silently override.
One pause for non-trivial design; skip for obvious fixes.
**Triggers — write an ADR only when all three apply:**
### 9. Autonomous Bug Fixing
- **Hard to reverse** (schema migration, framework swap, integration redesign).
- **Surprising without context** (future engineers will question the approach).
- **Result of genuine trade-offs** (real alternatives existed and you chose deliberately).
If all three apply: write the ADR in the same turn as the decision. Next number (4-digit zero-padded), kebab-case slug, Nygard short form — see `agents-docs/adr/0001-record-architectural-decisions.md` for the canonical example and `agents-docs/AGENTS_ADRS.md` for the contract. If any of the three is missing: don't write one.
**Supersede, don't delete.** Overturned decisions get a new ADR; the old one stays with a `Superseded by ADR-NNNN` note.
### 6. Verification Before Done
- Never mark a task complete without proving it works
- Diff behavior between main and your changes when relevant
- Ask yourself: "Would a staff engineer approve this?"
- Run tests, check logs, demonstrate correctness
### 7. Demand Elegance (Balanced)
- For non-trivial changes: pause and ask "is there a more elegant way?"
- If a fix feels hacky: "Knowing everything I know now, implement the elegant solution"
- Skip this for simple, obvious fixes — don't over-engineer
- Challenge your own work before presenting it
### 8. Autonomous Bug Fixing
- When given a bug report: just fix it. Don't ask for hand-holding
- Point at logs, errors, failing tests — then resolve them
- Zero context switching required from the user
Fix with evidence in default scope. If root cause is clearly `server/` (or similar), say so and ask to expand scope.
---
## Pull Requests
This project hosts at Gitea (`git.azaaxin.com/myxelium/Toju`). Gitea PRs and issues use GitHub-style syntax.
- Create a feature branch for every change: `<type>/<short-description>` (e.g. `feat/add-retry-logic`, `fix/null-pointer-webhook`) — `<type>` should match the Conventional Commits prefix (`feat`, `fix`, `chore`, `docs`, `perf`, `refactor`, `test`)
- Open the PR via the Gitea web UI (or `tea pulls create` if `tea` CLI is installed) — include a summary and a test plan
- Link issues in the PR body with `Fixes #<number>` for auto-close or `Relates to #<number>` for reference (Gitea honors the same keywords as GitHub)
- After merge, delete the feature branch
Gitea: `git.azaaxin.com/myxelium/Toju`. Branch `<type>/<short-description>`; PR with summary + test plan; `Fixes #<n>` / `Relates to #<n>`.
---
## Core Principles
- **Simplicity First:** Make every change as simple as possible. Impact minimal code.
- **No Laziness:** Find root causes. No temporary fixes. Senior developer standards.
- **Minimal Impact:** Changes should only touch what's necessary. Avoid introducing bugs.
- **Simplicity First** · **No Laziness** · **Minimal Impact** · **Cheap Context** · **Default Scope Fence** · **Handoff Beats Fat Context**
+7 -2
View File
@@ -45,9 +45,14 @@ The server package does not currently have a test runner script — there is one
E2E tests exercise the real Electron app against the real signaling server. The `.agents/skills/playwright-e2e/SKILL.md` describes the convention this repo uses for E2E test design — read it before adding new tests.
### TDD discipline
### Test-backed development (balanced)
Write the failing test first. Run it, watch it fail, then write the smallest code that makes it pass. This rule is non-negotiable (see `/AGENTS.md` § CRITICAL).
See `/AGENTS.md` § CRITICAL for the full policy. Summary:
- **Done** = the asked behavior works, proven at the right level. Green unit tests alone are not done when the ask was product behavior.
- Prefer red-green for domain/logic and bug invariants; encode the **user-facing or cross-boundary** outcome, not only implementation details.
- Skip full red-green for docs/copy/agent-instruction text, pure formatting, and trivial wiring already covered by a higher-level test you will run.
- Run **targeted** specs while iterating (`cd toju-app && npx vitest run <path>`); broaden to full suite / e2e when risk is cross-cutting or before merge.
Integration / cross-package work that needs a real database can rely on Electron's TypeORM + sql.js setup (in-memory by default) — no Testcontainers required.
+29
View File
@@ -0,0 +1,29 @@
# Session Handoff
> **New chat:** attach `@agents-docs/HANDOFF.md` (and only the files listed under Changed files). Say: continue from this handoff; do not redo completed work. Stay in default scope unless Next steps expand it.
>
> **Agents cannot open a new chat** — write/update this file, then ask the user to start one.
**Status:** none
<!-- When Status is `active`, fill every section below. When done with a handoff cycle, set Status back to `none` or overwrite on the next handoff. -->
## Goal
## Completed
## Changed files
## Decisions (this session only)
## Failed approaches
## Current issue / blockers
## Next steps (exact)
## Commands to continue
## Scope note
Default: `toju-app/` + targeted `electron/` + `.gitea/workflows/`. Out of scope unless listed here: `server/`, `e2e/`, `website/`, `docs-site/`.
+58
View File
@@ -0,0 +1,58 @@
# Agent Lessons — Index
**Session start:** read this file only. Match tags to the task. Open the matching lesson body in `agents-docs/LESSONS.md` — do **not** load every lesson.
When adding a lesson: append the full entry near the top of `LESSONS.md` (under `## Lessons`) **and** add one bullet here.
Tags help grepping: `rg '\\[attachments\\]' agents-docs/LESSONS-INDEX.md`
## Index
- Keep `NgOptimizedImage` off runtime blob and data URLs — `[angular] [images]`
- Read the exact Obsidian bug note before diagnosing a named ticket — `[workflow] [bugs]`
- Run `npm run i18n:sync` after editing any `public/i18n/catalog/*.json` file — `[i18n] [testing]`
- Match direct-call recipients against every local identity alias, exactly like DMs already do — `[direct-call] [identity]`
- Resolve outbound direct-call recipient ids to the peer's connected signal identity — `[direct-call] [identity] [signaling]`
- Canonicalize direct conversation ids across cross-signal actor aliases — `[direct-message] [identity]`
- Decide attachment receive admission once at request time; never re-gate size in the chunk handler — `[attachments]`
- Re-queue attachment auto-downloads on every message/room binding event; never trust one transport's ordering — `[attachments] [realtime]`
- Scope per-user UI state by user id, not by the client database — `[persistence] [multi-user] [custom-emoji]`
- Don't strand signed-out mobile users on a logged-out dashboard — `[auth] [mobile] [routing]`
- "Shared from your device" must gate on local bytes, not uploader user id — `[attachments] [multi-device]`
- Generate Android brand icons from the source mark; guard against stock Capacitor placeholders — `[mobile] [android] [assets]`
- Bind chat attachments to a pre-allocated message id, never by matching content — `[attachments] [chat] [mobile]`
- Attachment file persistence must be platform-agnostic, not Electron-only — `[attachments] [persistence] [mobile]`
- Never count duplicate chunks toward transfer progress, and never finalize on byte counters — `[attachments] [webrtc]`
- Don't bump E2E timeouts for sync flakes - gate on presence and read server logs — `[testing] [realtime]`
- When renaming an Angular route, sweep every navigate/url-match/doc reference — `[routing]`
- Server discovery must fan out across all endpoints and self-heal on 404 — never hardcode a host capability blocklist — `[server-directory]`
- Server registration needs `ownerPublicKey: oderId || id`, and must not be fire-and-forget — `[server-directory] [rooms]`
- Identify must fall back to the legacy session token, not only the new credential store — `[realtime] [authentication]`
- Keep the per-signal-URL identify credential resolvable from the store — `[realtime] [authentication]`
- Store clientInstanceId in sessionStorage not localStorage — `[realtime] [multi-device]`
- Revalidate IndexedDB scope without reinitializing on every read — `[persistence] [performance]`
- Restore local user scope before protected writes — `[authentication] [persistence]`
- Persisted local user state still requires a session token — `[authentication] [signaling]`
- Declare MODIFY_AUDIO_SETTINGS for Android WebRTC mic capture — `[mobile] [android]`
- Do not override Tailwind with box-sizing inherit — `[mobile] [css]`
- Use the app-shell servers rail for mobile discovery pages — `[mobile] [layout]`
- Defer attachment blob hydration on Electron startup — `[attachments] [electron]`
- Lazy-load Capacitor modules on Electron/desktop — `[mobile] [electron]`
- Use the upgrade transaction during IndexedDB schema migrations — `[persistence] [browser]`
- Wait for authenticateUser storage prep before post-login navigation — `[authentication] [browser]`
- Use dense arrays for chunked transfer buffers — `[custom-emoji] [webrtc]`
- Route custom emoji right-click through the native context menu — `[custom-emoji] [ux]`
- Separate known emoji assets from saved library — `[custom-emoji] [ux]`
- Chunk custom emoji assets over data channels — `[custom-emoji] [webrtc]`
- Re-clear visible notification channels after recompute — `[notifications] [startup]`
- Disambiguate nested chat cards — `[chat] [ui]`
- Use terminal Vitest when the test tool hangs — `[testing]`
- Do not add fake chrome around screenshots — `[website] [design]`
- Verify lint exits 0 before claiming done — `[verification]`
- Use blob URLs for inline attachment previews — `[attachments] [electron]`
- Resolve Electron drag-and-drop file paths with webUtils — `[attachments] [electron]`
- Preserve uploader local attachment paths across sync — `[attachments] [persistence]`
- Prove the asked behavior; unit-green is not done — `[verification] [testing] [workflow]`
- Default to `toju-app/` + targeted `electron/` + CI; do not crawl the monorepo — `[workflow] [tokens] [scope]`
- Write HANDOFF.md and ask the user for a new chat — agents cannot open chats — `[workflow] [tokens] [handoff]`
+38 -3
View File
@@ -1,12 +1,12 @@
# Agent Lessons
Durable rules for AI agents working on this project. Read this file at session start. Append to it when this session produces a correction worth remembering.
Durable rules for AI agents working on this project.
## How to use this file
**At session start:** scan the rules below. If any match the work you're about to do, apply them.
**At session start:** read `agents-docs/LESSONS-INDEX.md` only. Open lesson bodies here **only** for tags that match the task. Do not load this entire file into context by default.
**During the session:** if the user corrects you, reverts your edit, or re-prompts with the same instruction — that is a signal to record a lesson before closing the task. See the trigger list in `agents-docs/AGENT_WORKFLOW.md`.
**During the session:** if the user corrects you, reverts your edit, or re-prompts with the same instruction — record a lesson here **and** add a one-line entry to `LESSONS-INDEX.md` before closing the task. See triggers in `agents-docs/AGENT_WORKFLOW.md`.
**Format of a lesson:** every entry uses the four-slot template below. Brevity matters — if you can't state the rule in one sentence, the lesson isn't sharp enough yet.
@@ -25,6 +25,27 @@ Durable rules for AI agents working on this project. Read this file at session s
## Lessons
### Default to `toju-app/` + targeted `electron/` + CI; do not crawl the monorepo [workflow] [tokens] [scope]
- **Trigger:** about to browse all of `electron/`, or to `grep`/`Read` under `server/`, `e2e/`, `website/`, or `docs-site/` on a normal product bug without the user naming those packages.
- **Rule:** stay in `toju-app/`, `.gitea/workflows/`, and **only the Electron files on the renderer→preload→handler path**; if the fix looks like `server/`/e2e, ask once instead of exploring those trees.
- **Why:** monorepo-wide (and whole-`electron/`) exploration multiplies context on expensive problem-solving models without fixing the asked client bug.
- **Example:** attachment disk restore → `toju-app` persistence service + `electron/preload.ts` + the one IPC/file helper involved — not every file under `electron/migrations/` or `electron/api/`.
### Write HANDOFF.md and ask the user for a new chat — agents cannot open chats [workflow] [tokens] [handoff]
- **Trigger:** the thread is long, the user says "handoff"/"new chat", or a new major objective starts while more work remains.
- **Rule:** overwrite `agents-docs/HANDOFF.md` with `Status: active` and the eight sections, then ask the user to start a new chat attaching that file; never pretend you opened a fresh chat.
- **Why:** fat chat history dominates token burn on problem-solving models; a 510k handoff replaces 100k+ of mixed failure/tool output.
- **Example:** user: "handoff" → write HANDOFF → reply: "Start a new chat and attach `@agents-docs/HANDOFF.md`; say continue from handoff."
### Prove the asked behavior; unit-green is not done [verification] [testing] [workflow]
- **Trigger:** about to report a task finished because colocated Vitest specs (or a narrow mocked unit) are green, while the users ask was a product behavior, UI flow, or bug they can still reproduce.
- **Rule:** treat acceptance as “the asked functionality works” — prove it with a user-visible path, focused e2e, or an explicit manual check; keep unit tests as support, never as the sole done signal.
- **Why:** agents optimized for TDD often stop at implementation-shaped tests that pass while the real feature/bug remains broken, which wastes follow-up turns and burns tokens on false completion.
- **Example:** for “DM reply doesnt show for the caller,” a passing `DirectMessageService` mock test is insufficient until the cross-signal conversation identity path is exercised (e2e or a behavior-level regression that fails on the old fork-thread bug).
### Keep `NgOptimizedImage` off runtime blob and data URLs [angular] [images]
- **Trigger:** Angular template lint suggests replacing `[src]` with `ngSrc` for a user-uploaded image rendered from `blob:` or `data:`.
@@ -53,6 +74,20 @@ Durable rules for AI agents working on this project. Read this file at session s
- **Why:** the failure only reproduces when caller and callee have different home signal servers, which no same-server e2e covers; and when one identity-alias bug is fixed in a domain, grep for the same `=== currentUserId` pattern in sibling domains that share the transport — the direct-call domain reused `PeerDeliveryService` but kept the naive check for another month.
- **Example:** `direct-call-participant-identity.rules.ts#directCallPayloadIncludesAnyId` / `normalizeDirectCallPayloadSelfAliases`; regression e2e `e2e/tests/voice/dm-header-call-ring.spec.ts` registers Bob on a secondary signal server, meets in a primary-signal room, and asserts the DM-header call rings Bob's incoming-call modal (fails on old code, passes after).
### Resolve outbound direct-call recipient ids to the peer's connected signal identity [direct-call] [identity] [signaling]
- **Trigger:** cross-signal direct calls still failed after the inbound alias fix — the caller joined voice and showed "In voice" while the callee never rang. `PeerDeliveryService.resolveSignalingPeerId` returned null when the stored peer id was a home id but presence/route was registered under the provisioned actor id, so `sendRawMessage` was never called; even when attempted, the server relays only when `targetUserId` exactly matches the callee's connected `oderId`.
- **Rule:** outbound DM/call delivery must collect every recipient alias (`peer-delivery-identity.rules.ts#collectRecipientDeliveryCandidateIds`), pick the routable id with `pickRoutableRecipientId`, always attempt signaling send (broadcast fallback when no single route works), and surface `call.errors.recipientUnreachable` to the caller when delivery cannot succeed — never leave the caller in a silent "In voice" state.
- **Why:** inbound and outbound identity bugs are independent; fixing admission on the callee does not help if the ring never leaves the caller or hits the wrong `targetUserId` on the wire.
- **Example:** `PeerDeliveryService.sendViaSignaling` + `DirectCallService.resolveRoutableRecipientId`; e2e `e2e/tests/voice/dm-header-call-ring.spec.ts` (callee-home room, people-search call).
### Canonicalize direct conversation ids across cross-signal actor aliases [direct-message] [identity]
- **Trigger:** cross-signal DMs delivered to the callee but the caller never saw replies — User1 had two DM rows for User2, replies landed in the actor-id thread, and DM routes could throw "Cannot use direct messages without a current user" when `openConversation` ran before hydration.
- **Rule:** incoming DM traffic must resolve to the existing home-id conversation for the same peer (`direct-message-conversation-identity.rules.ts`), merge duplicate alias threads on load/receive, queue inbound events until `currentUser` hydrates, and guard route-driven `openConversation` calls when no owner id is available yet.
- **Why:** recipient-alias admission alone is insufficient — if `message.conversationId` still carries a foreign actor id, the client forks a second local thread and the original chat stays empty.
- **Example:** `resolveDirectConversationId` + `mergeAliasDirectConversations` in `DirectMessageService.handleIncomingMessage`; `appendCrossSignalActorAliases` + `pickDeliveryTargetIds` when a foreign-home recipient is addressed by home id but only their shared-room actor id is routed (debug pattern: `c84813bb…` home id vs `e79a95e5…` actor on `signal.toju.app`).
### Decide attachment receive admission once at request time; never re-gate size in the chunk handler [attachments]
- **Trigger:** "Sending files between users doesn't really work" — a browser user clicked Request on a 1050 MB generic file, the request gate (`canReceiveAttachment`) admitted it for in-memory receive, the sender streamed chunks, but `handleFileChunk` still had a leftover hard `size > MAX_AUTO_SAVE_SIZE_BYTES` rejection on the in-memory path, so every chunk was dropped, no ack was ever sent, the sender's `waitForAck` timed out, and the GUI never changed.