merge: AgentProject membership — the junction is now load-bearing

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
This commit is contained in:
Michal
2026-08-09 23:34:18 +01:00
169 changed files with 8677 additions and 87 deletions

View File

@@ -0,0 +1,84 @@
# Task ID: 76
**Title:** Implement summarize-tree Stage
**Status:** done
**Dependencies:** 71 ✓, 72 ✓, 73 ✓, 74 ✓
**Priority:** high
**Description:** Create the built-in summarize-tree stage that recursively summarizes sections, using structural summaries for programmatic content and LLM summaries for prose.
**Details:**
Create `src/mcplocal/src/proxymodel/stages/summarize-tree.ts`:
```typescript
import type { StageHandler, Section, StageContext } from '../types';
import { detectContentType } from '../content-detection';
const handler: StageHandler = async (content, ctx) => {
const maxTokens = (ctx.config.maxSummaryTokens as number) ?? 200;
const maxGroup = (ctx.config.maxGroupSize as number) ?? 5;
const maxDepth = (ctx.config.maxDepth as number) ?? 3;
// Parse sections from previous stage or create single section
const inputSections = parseSectionsFromContent(content);
const tree = await buildTree(inputSections, ctx, { maxTokens, maxGroup, maxDepth, depth: 0 });
const toc = tree.map(s =>
`[${s.id}] ${s.title} — ${s.metadata?.summary ?? ''}` +
(s.sections?.length ? `\n → ${s.sections.length} sub-sections` : '')
).join('\n');
return {
content: `${tree.length} sections:\n${toc}\n\nUse section parameter to read details.`,
sections: tree,
};
};
async function buildTree(
sections: Section[],
ctx: StageContext,
opts: { maxTokens: number; maxGroup: number; maxDepth: number; depth: number }
): Promise<Section[]> {
for (const section of sections) {
const contentType = detectContentType(section.content);
// Structural summary for programmatic content (no LLM needed)
if (contentType !== 'prose') {
section.metadata = { summary: generateStructuralSummary(section.content, contentType) };
} else {
// LLM summary for prose (cached)
const cacheKey = `summary:${ctx.cache.hash(section.content)}:${opts.maxTokens}`;
const summary = await ctx.cache.getOrCompute(cacheKey, () =>
ctx.llm.complete(
`Summarize in ${opts.maxTokens} tokens, preserve MUST/REQUIRED items:\n\n${section.content}`
)
);
section.metadata = { summary };
}
// Recurse if large and not at max depth
if (section.content.length > 5000 && opts.depth < opts.maxDepth) {
section.sections = await buildTree(
splitContent(section.content),
ctx,
{ ...opts, depth: opts.depth + 1 }
);
}
}
return sections;
}
function generateStructuralSummary(content: string, type: string): string {
// Generate summary from structure: key names, array lengths, types
// No LLM needed for JSON/YAML/XML/code
}
```
**Test Strategy:**
Unit tests: prose content gets LLM summary (mock LLM), JSON content gets structural summary without LLM call, recursive splitting triggers at 5000 chars, maxDepth is respected, cache is used for repeated content. Integration test with real LLM provider.