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
2.5 KiB
2.5 KiB
Task ID: 45
Title: Implement tag-matching algorithm for prompt selection
Status: done
Dependencies: 44 ✓
Priority: high
Description: Create a deterministic keyword-based tag matching algorithm as the no-LLM fallback for prompt selection, with byte-budget allocation and priority weighting.
Details:
-
Create
/src/mcplocal/src/services/tag-matcher.service.ts:interface MatchedPrompt { prompt: PromptIndex; score: number; matchedTags: string[]; } export class TagMatcherService { constructor(private byteBudget: number = 8192) {} matchPrompts(tags: string[], promptIndex: PromptIndex[]): { fullContent: PromptIndex[]; // Prompts to include in full indexOnly: PromptIndex[]; // Prompts to include as index entries remaining: PromptIndex[]; // Non-matched prompts (names only) } { // 1. Priority 10 prompts: always included (score = Infinity) // 2. For each prompt, compute score: // - Check tags against summary + chapters (case-insensitive substring) // - score = matching_tags_count * priority // 3. Sort by score descending // 4. Fill byte budget from top: // - Include full content until budget exhausted // - Remaining matched: include as index entries // - Non-matched: names only } private computeScore(tags: string[], prompt: PromptIndex): number { if (prompt.priority === 10) return Infinity; const matchingTags = tags.filter(tag => this.matchesPrompt(tag.toLowerCase(), prompt) ); return matchingTags.length * prompt.priority; } private matchesPrompt(tag: string, prompt: PromptIndex): boolean { const searchText = [ prompt.summary || '', ...(prompt.chapters || []) ].join(' ').toLowerCase(); return searchText.includes(tag); } } -
Handle edge cases: empty tags, no prompts, all priority 10, etc.
Test Strategy:
- Unit test: Priority 10 prompts always included regardless of tags
- Unit test: Score calculation: matching_tags * priority
- Unit test: Case-insensitive matching
- Unit test: Substring matching in summary and chapters
- Unit test: Byte budget exhaustion stops full content inclusion
- Unit test: Matched prompts beyond budget become index entries
- Unit test: Non-matched prompts listed as names only
- Unit test: Sorting by score descending
- Unit test: Empty tags returns priority 10 only
- Unit test: No prompts returns empty result