Merges feat/agent-project-membership (task 166/152). An agent can finally join many projects: membership routes, per-membership role and optional brain override, chat resolving context from the membership, and CLI attach-agent / detach-agent / get memberships. The legacy Agent.projectId fallback is kept for this release as the schema comment promises. Conflict resolved in main.ts: that branch added its RBAC case to mapUrlToPermission inline, while this branch had extracted the function to middleware/url-permission.ts so it could be tested. Kept the extraction and ported the case into it — joining a team is `edit:agents:<name>`, not `create:agents`, which is the authority to mint a whole agent. Also fixes something the extraction makes possible. security.test.ts contained a hand-copied reproduction of mapUrlToPermission, because main.ts self-invokes main() and nothing in it was importable. A security test asserting against a copy stops testing the system the moment the real one changes, and this copy had already drifted. It now imports the real function; all 18 assertions still pass, against the code that actually runs. Local: build clean, workspace 2550 passed, lint unchanged at 794 errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
69 lines
2.3 KiB
Markdown
69 lines
2.3 KiB
Markdown
# Task ID: 79
|
|
|
|
**Title:** Implement Stage Registry and Dynamic Loader
|
|
|
|
**Status:** done
|
|
|
|
**Dependencies:** 71 ✓, 75 ✓, 76 ✓, 77 ✓
|
|
|
|
**Priority:** high
|
|
|
|
**Description:** Create the stage registry that resolves stage names to handlers, loading from ~/.mcpctl/stages/ for custom stages and falling back to built-ins.
|
|
|
|
**Details:**
|
|
|
|
Create `src/mcplocal/src/proxymodel/stage-registry.ts`:
|
|
|
|
```typescript
|
|
import { readdir, stat } from 'fs/promises';
|
|
import { join } from 'path';
|
|
import type { StageHandler } from './types';
|
|
|
|
const STAGES_DIR = join(process.env.HOME ?? '', '.mcpctl', 'stages');
|
|
|
|
const builtInStages: Map<string, StageHandler> = new Map();
|
|
const customStages: Map<string, StageHandler> = new Map();
|
|
|
|
// Register built-ins at module load
|
|
import passthrough from './stages/passthrough';
|
|
import paginate from './stages/paginate';
|
|
import sectionSplit from './stages/section-split';
|
|
import summarizeTree from './stages/summarize-tree';
|
|
|
|
builtInStages.set('passthrough', passthrough);
|
|
builtInStages.set('paginate', paginate);
|
|
builtInStages.set('section-split', sectionSplit);
|
|
builtInStages.set('summarize-tree', summarizeTree);
|
|
|
|
export async function loadCustomStages(): Promise<void> {
|
|
customStages.clear();
|
|
try {
|
|
const files = await readdir(STAGES_DIR);
|
|
for (const file of files.filter(f => f.endsWith('.ts') || f.endsWith('.js'))) {
|
|
const name = file.replace(/\.(ts|js)$/, '');
|
|
const module = await import(join(STAGES_DIR, file));
|
|
customStages.set(name, module.default);
|
|
}
|
|
} catch { /* directory doesn't exist */ }
|
|
}
|
|
|
|
export function getStage(name: string): StageHandler | null {
|
|
return customStages.get(name) ?? builtInStages.get(name) ?? null;
|
|
}
|
|
|
|
export function listStages(): { name: string; source: 'built-in' | 'local' }[] {
|
|
const result: { name: string; source: 'built-in' | 'local' }[] = [];
|
|
for (const name of builtInStages.keys()) {
|
|
result.push({ name, source: customStages.has(name) ? 'local' : 'built-in' });
|
|
}
|
|
for (const name of customStages.keys()) {
|
|
if (!builtInStages.has(name)) result.push({ name, source: 'local' });
|
|
}
|
|
return result;
|
|
}
|
|
```
|
|
|
|
**Test Strategy:**
|
|
|
|
Unit tests: built-in stages are registered, getStage() returns correct handler, custom stages override built-ins, listStages() shows correct sources, missing stages return null. Integration test with actual stage files in temp directory.
|