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.
This commit is contained in:
2026-01-22 06:22:59 +01:00
commit bd178fcaf0
23 changed files with 4001 additions and 0 deletions

View File

@@ -0,0 +1,150 @@
# 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
```typescript
// 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.
```typescript
// 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**:
```typescript
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
```typescript
// 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?
### For blocking-related changes:
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