Diligence prevents AI agents from shipping quick fixes that break things by enforcing a research-propose-verify loop before any code changes. Key features: - Worker sub-agent researches and proposes with file:line citations - Reviewer sub-agent independently verifies claims by searching codebase - Iterates until approved (max 5 rounds) - Loads project-specific context from .claude/CODEBASE_CONTEXT.md - State persisted across sessions Validated on production codebase: caught architectural mistake (broker subscriptions on client-side code) that naive agent would have shipped.
4.8 KiB
4.8 KiB
Codebase Context: Test Fixture
This is a simplified test codebase that mirrors real-world patterns. Use this context to understand the architecture before making changes.
Architecture Overview
src/
├── broker/
│ └── events.ts # Broker event bus (Subject-based pub/sub)
├── services/
│ ├── user-block.service.ts # Blocking logic
│ ├── voice-channel.service.ts # Voice channels and DM calls
│ ├── chat.service.ts # Chat channels and messages
│ └── team.service.ts # Team state and permission caching
└── controllers/
└── roles.controller.ts # REST API for roles
Critical Pattern: Broker Events
All state changes that affect multiple services MUST emit broker events.
Available Events
| Event | Emitted When | Expected Subscribers |
|---|---|---|
BusUserBlockChange |
User blocks/unblocks another | Voice services, DM services |
BusTeamRoleChange |
Role created/updated/deleted | Permission caches |
BusTeamMemberRoleChange |
User role assigned/removed | Permission caches |
BusVoiceParticipant |
User joins/leaves voice | Voice UI components |
BusDmCall |
DM call state changes | Call observers |
Pattern Example
// CORRECT: Emit event after state change
async updateRole(teamId, roleId, updates) {
const role = await db.update(roleId, updates);
BusTeamRoleChange.next({ // ← MUST emit event
teamId,
roleId,
action: 'updated',
timestamp: new Date(),
});
return role;
}
Critical Pattern: Permission vs Action Checks
Permission = Visibility. Action = Separate Check.
Why This Matters
For DM channels, blocking creates a 'read' permission, NOT 'denied'. The user can still SEE the DM channel, but cannot SEND messages.
// Permission check (for visibility)
if (isBlocked) {
return { permission: 'read', reason: 'blocked' }; // ← 'read', not 'denied'
}
// Action check (separate from permission)
async sendMessage(userId, channel, content) {
if (await isBlockingEitherWay(userA, userB)) {
throw new Error('Cannot send messages'); // ← Separate check
}
}
Voice Permission Pattern
For DM channels, voice permissions are always true:
return {
permission: 'read',
voiceListen: true, // Always true for DM
voiceTalk: true, // Blocking checked on JOIN, not here
voiceWebcam: true,
voiceScreenshare: true,
};
Blocking is enforced by action checks in:
joinVoiceChannel()- line 33startDmCall()- line 56
Critical Pattern: Cache Invalidation
Caches MUST subscribe to relevant broker events.
Current Bug Pattern
// BAD: Only clears on team switch
constructor() {
teamChange$.subscribe(() => {
this.memoizedPermissions.clear();
});
}
// GOOD: Also clear on role changes
constructor() {
teamChange$.subscribe(() => this.clearCache());
BusTeamRoleChange.subscribe(() => this.clearCache()); // ← ADD THIS
BusTeamMemberRoleChange.subscribe(() => this.clearCache()); // ← AND THIS
}
Checklist: Before Making Changes
For ANY state change:
- Does this change affect other services?
- Is there a broker event for this? If not, should there be?
- Are all relevant services subscribed to the event?
For blocking-related changes:
- Is blocking checked on all relevant ACTIONS (not just permissions)?
- What happens if block is created DURING an action (e.g., mid-call)?
- Are broker events emitted for blocking changes?
- Do voice services subscribe to
BusUserBlockChange?
For permission/cache changes:
- What events should invalidate this cache?
- Is the cache subscribed to all relevant broker events?
- What's the TTL? Is stale data acceptable?
Files Quick Reference
| File | Key Functions | Known Issues |
|---|---|---|
user-block.service.ts |
blockUser(), unblockUser() |
Missing voice cleanup on block |
voice-channel.service.ts |
answerDmCall(), startDmCall() |
Missing blocking check on answer |
team.service.ts |
getPermission(), clearCache() |
Cache doesn't subscribe to role events |
roles.controller.ts |
createRole(), deleteRole() |
Missing broker events |
chat.service.ts |
getChannelPermission() |
Reference implementation (correct) |
Anti-Patterns to Avoid
- Fixing in ONE place - If blocking is checked in
startDmCall(), it should also be inanswerDmCall() - Changing permissions - Don't change
voiceListen: truetovoiceListen: !isBlocked. Use action checks instead. - Forgetting broker events - Every CRUD operation on roles/permissions should emit an event
- Assuming cache is fresh - If an operation can change state, subscribe to its event