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:
150
test/fixture/.claude/CODEBASE_CONTEXT.md
Normal file
150
test/fixture/.claude/CODEBASE_CONTEXT.md
Normal 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
|
||||
Reference in New Issue
Block a user