Files
diligence/test/fixture/.claude/CODEBASE_CONTEXT.md
Marc J. Schmidt bd178fcaf0 Initial release: MCP server enforcing Worker-Reviewer loop
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.
2026-01-22 06:22:59 +01:00

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 33
  • startDmCall() - 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:

  1. Does this change affect other services?
  2. Is there a broker event for this? If not, should there be?
  3. Are all relevant services subscribed to the event?
  1. Is blocking checked on all relevant ACTIONS (not just permissions)?
  2. What happens if block is created DURING an action (e.g., mid-call)?
  3. Are broker events emitted for blocking changes?
  4. Do voice services subscribe to BusUserBlockChange?

For permission/cache changes:

  1. What events should invalidate this cache?
  2. Is the cache subscribed to all relevant broker events?
  3. 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

  1. Fixing in ONE place - If blocking is checked in startDmCall(), it should also be in answerDmCall()
  2. Changing permissions - Don't change voiceListen: true to voiceListen: !isBlocked. Use action checks instead.
  3. Forgetting broker events - Every CRUD operation on roles/permissions should emit an event
  4. Assuming cache is fresh - If an operation can change state, subscribe to its event