tribunal-kit 5.7.0 → 5.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.agent/ARCHITECTURE.md +6 -7
  2. package/.agent/agents/frontend-reviewer.md +13 -0
  3. package/.agent/agents/frontend-specialist.md +14 -0
  4. package/.agent/agents/logic-reviewer.md +11 -0
  5. package/.agent/agents/orchestrator.md +15 -0
  6. package/.agent/agents/security-auditor.md +13 -0
  7. package/.agent/agents/ui-ux-auditor.md +7 -31
  8. package/.agent/history/memory/.memory.idx +766 -0
  9. package/.agent/history/memory/MEMORY.md +62 -0
  10. package/.agent/routing_index.json +694 -714
  11. package/.agent/rules/GEMINI.md +58 -8
  12. package/.agent/scripts/_colors.js +131 -89
  13. package/.agent/scripts/_utils.js +163 -128
  14. package/.agent/scripts/auto_preview.js +207 -197
  15. package/.agent/scripts/bundle_analyzer.js +227 -192
  16. package/.agent/scripts/case_law_manager.js +991 -689
  17. package/.agent/scripts/checklist.js +233 -190
  18. package/.agent/scripts/context_broker.js +930 -605
  19. package/.agent/scripts/dependency_analyzer.js +275 -184
  20. package/.agent/scripts/graph_builder.js +412 -341
  21. package/.agent/scripts/graph_visualizer.js +392 -390
  22. package/.agent/scripts/graph_zoom.js +198 -156
  23. package/.agent/scripts/inner_loop_validator.js +523 -445
  24. package/.agent/scripts/lint_runner.js +199 -157
  25. package/.agent/scripts/marathon_harness.js +819 -661
  26. package/.agent/scripts/minify_context.js +115 -100
  27. package/.agent/scripts/mutation_runner.js +321 -280
  28. package/.agent/scripts/prompt_compiler.js +62 -42
  29. package/.agent/scripts/schema_validator.js +373 -280
  30. package/.agent/scripts/security_scan.js +333 -190
  31. package/.agent/scripts/session_manager.js +306 -270
  32. package/.agent/scripts/skill_evolution.js +810 -637
  33. package/.agent/scripts/skill_integrator.js +327 -307
  34. package/.agent/scripts/strengthen_skills.js +203 -193
  35. package/.agent/scripts/swarm_dispatcher.js +558 -457
  36. package/.agent/scripts/test_runner.js +178 -152
  37. package/.agent/scripts/verify_all.js +200 -168
  38. package/.agent/skills/fabel-protocol/SKILL.md +235 -0
  39. package/.agent/skills/thinking-protocol/SKILL.md +27 -0
  40. package/.agent/workflows/generate.md +1 -1
  41. package/.agent/workflows/tribunal-speed.md +1 -1
  42. package/README.md +53 -53
  43. package/bin/mcp-server.js +460 -175
  44. package/bin/tribunal-kit.js +1245 -987
  45. package/bin/wrapper.js +104 -74
  46. package/dist/cli.js +31 -0
  47. package/dist/commands/case.js +23 -0
  48. package/dist/commands/compile.js +84 -0
  49. package/dist/commands/init.js +42 -0
  50. package/dist/commands/learn.js +57 -0
  51. package/dist/commands/memory.js +456 -0
  52. package/package.json +2 -2
  53. package/scripts/benchmark.js +162 -125
  54. package/scripts/changelog.js +196 -168
  55. package/scripts/sync-version.js +94 -81
  56. package/scripts/validate-payload.js +85 -78
@@ -1,605 +1,930 @@
1
- #!/usr/bin/env node
2
- /**
3
- * context_broker.js — Tribunal Kit Context Density Broker
4
- * =========================================================
5
- * "Focus without Compromise" — Intelligent skill selection for all model sizes.
6
- *
7
- * Philosophy:
8
- * This is NOT a filter that removes context. It is a PRIORITIZER that
9
- * ensures the most relevant rules occupy the highest-attention positions
10
- * in the AI's context window. Supplementary context is condensed, not cut.
11
- *
12
- * For LARGER models (Claude Opus, Gemini 2.5 Pro, GPT-4o):
13
- * → Level 0 (Essential) skills are injected with full fidelity at the top.
14
- * → Level 1 (Supplementary) skills are condensed to their key rules only.
15
- * → Nothing is removed — the model gets everything, ordered optimally.
16
- *
17
- * For SMALLER/FASTER models (Gemini Flash, GPT-4o-mini):
18
- * → Only Level 0 (Essential) skills are included.
19
- * → This prevents context overflow and attention dilution.
20
- * → Quality gates remain uncompromised — just fewer rules to track.
21
- *
22
- * Tiered Context Priority:
23
- * Level 0 — Essential: Top matches, full SKILL.md text, injected first
24
- * Level 1 — Supplementary: Medium matches, condensed to "key rules" section
25
- * Level 2 — Available: Low matches, listed by name only (for reference)
26
- *
27
- * Scoring Algorithm:
28
- * - Task keyword TF-IDF match against skill frontmatter + description
29
- * - File type affinity (e.g., .tsx → react-specialist gets +2 boost)
30
- * - Domain tag match (e.g., "sql" in task → sql-pro gets +3 boost)
31
- * - Recency boost: skills referenced in the last 3 sessions rank higher
32
- * - Tribunal alignment: skills matching active reviewers rank higher
33
- *
34
- * Usage:
35
- * node .agent/scripts/context_broker.js --task "Build a login API with JWT"
36
- * node .agent/scripts/context_broker.js --task "Design a premium landing page" --file Login.tsx
37
- * node .agent/scripts/context_broker.js --task "..." --model large --output json
38
- * node .agent/scripts/context_broker.js --task "..." --model small --output names
39
- * node .agent/scripts/context_broker.js demo
40
- *
41
- * Output modes:
42
- * report (default) — human-readable tiered selection report
43
- * json — JSON with tiered skill lists
44
- * names — newline-separated skill names only (for piping)
45
- * prompt — full injected prompt text ready for an LLM
46
- */
47
-
48
- 'use strict';
49
-
50
- const fs = require('fs');
51
- const path = require('path');
52
-
53
- // ── Colours ───────────────────────────────────────────────────────────────────
54
- const { GREEN, YELLOW, CYAN, RED, BOLD, DIM, RESET } = require('./_colors');
55
-
56
- // ── Domain → Skill affinity map ───────────────────────────────────────────────
57
- // Keywords in the user's task that strongly indicate specific skills.
58
- // Higher weight = stronger signal.
59
- const DOMAIN_AFFINITIES = [
60
- // Backend / API
61
- { keywords: ['api', 'rest', 'route', 'endpoint', 'handler', 'express', 'fastapi', 'hono'],
62
- skills: ['api-patterns', 'nodejs-best-practices', 'backend-specialist', 'error-resilience'], weight: 3 },
63
- // Database
64
- { keywords: ['sql', 'query', 'database', 'prisma', 'drizzle', 'orm', 'postgres', 'mysql', 'schema'],
65
- skills: ['database-design', 'sql-pro', 'supabase-postgres-best-practices'], weight: 3 },
66
- // Authentication
67
- { keywords: ['auth', 'jwt', 'login', 'oauth', 'session', 'password', 'token', 'rbac'],
68
- skills: ['authentication-best-practices', 'vulnerability-scanner', 'api-security-auditor'], weight: 3 },
69
- // React / Next.js
70
- { keywords: ['react', 'next', 'nextjs', 'component', 'hook', 'jsx', 'tsx', 'server component', 'server action'],
71
- skills: ['react-specialist', 'nextjs-react-expert', 'frontend-design'], weight: 3 },
72
- // Frontend / UI
73
- { keywords: ['ui', 'design', 'landing', 'page', 'layout', 'responsive', 'tailwind', 'css', 'style'],
74
- skills: ['frontend-design', 'ui-ux-pro-max', 'tailwind-patterns', 'web-design-guidelines'], weight: 2 },
75
- // Animation / Motion
76
- { keywords: ['animation', 'gsap', 'framer', 'motion', 'scroll', 'transition', 'parallax'],
77
- skills: ['motion-engineering', 'framer-motion-expert', 'gsap-core', 'gsap-scrolltrigger'], weight: 3 },
78
- // AI / LLM
79
- { keywords: ['llm', 'openai', 'anthropic', 'gemini', 'embedding', 'ai', 'rag', 'vector', 'chat', 'prompt'],
80
- skills: ['llm-engineering', 'ai-prompt-injection-defense', 'agentic-patterns'], weight: 3 },
81
- // Security
82
- { keywords: ['security', 'xss', 'injection', 'owasp', 'vulnerability', 'csrf', 'sanitize', 'audit'],
83
- skills: ['vulnerability-scanner', 'api-security-auditor', 'authentication-best-practices'], weight: 3 },
84
- // Testing
85
- { keywords: ['test', 'spec', 'jest', 'vitest', 'playwright', 'unit test', 'e2e', 'mock'],
86
- skills: ['testing-patterns', 'playwright-best-practices', 'tdd-workflow'], weight: 3 },
87
- // Performance
88
- { keywords: ['performance', 'optimize', 'bundle', 'cache', 'speed', 'slow', 'lighthouse', 'cwv'],
89
- skills: ['performance-profiling', 'motion-engineering', 'edge-computing'], weight: 2 },
90
- // Mobile
91
- { keywords: ['mobile', 'react native', 'expo', 'ios', 'android', 'gesture', 'haptic'],
92
- skills: ['building-native-ui', 'mobile-design', 'agentic-patterns'], weight: 3 },
93
- // DevOps / CI
94
- { keywords: ['docker', 'ci', 'cd', 'deploy', 'pipeline', 'k8s', 'kubernetes', 'github actions'],
95
- skills: ['devops-engineer', 'deployment-procedures', 'observability'], weight: 2 },
96
- // Real-time
97
- { keywords: ['realtime', 'websocket', 'sse', 'socket', 'live', 'multiplayer', 'collaborative'],
98
- skills: ['realtime-patterns', 'error-resilience'], weight: 3 },
99
- // TypeScript
100
- { keywords: ['typescript', 'type', 'generic', 'interface', 'satisfies', 'zod', 'pydantic'],
101
- skills: ['typescript-advanced', 'data-validation-schemas'], weight: 2 },
102
- // Python
103
- { keywords: ['python', 'fastapi', 'django', 'flask', 'pydantic', 'asyncio'],
104
- skills: ['python-pro', 'python-patterns'], weight: 3 },
105
- // Architecture
106
- { keywords: ['architecture', 'refactor', 'clean', 'solid', 'design pattern', 'monorepo', 'microservice'],
107
- skills: ['architecture', 'clean-code', 'monorepo-management'], weight: 2 },
108
- // C# / .NET
109
- { keywords: ['csharp', 'c#', 'dotnet', '.net', 'blazor', 'aspnet', 'entity framework'],
110
- skills: ['csharp-developer'], weight: 3 },
111
- ];
112
-
113
- // ── File extension → skill boost map ─────────────────────────────────────────
114
- const EXT_AFFINITIES = {
115
- '.tsx': ['react-specialist', 'nextjs-react-expert', 'typescript-advanced'],
116
- '.jsx': ['react-specialist', 'frontend-design'],
117
- '.ts': ['typescript-advanced', 'nodejs-best-practices'],
118
- '.vue': ['vue-expert'],
119
- '.py': ['python-pro', 'python-patterns'],
120
- '.cs': ['csharp-developer'],
121
- '.sql': ['sql-pro', 'database-design'],
122
- '.css': ['tailwind-patterns', 'frontend-design'],
123
- };
124
-
125
- // ── Core baseline skills — always available to all model sizes ────────────────
126
- // These are injected for every request at a condensed level.
127
- const BASELINE_SKILLS = [
128
- 'clean-code',
129
- 'systematic-debugging',
130
- 'error-resilience',
131
- ];
132
-
133
- // ── Skill catalogue (loaded from disk) ───────────────────────────────────────
134
-
135
- /**
136
- * Find the .agent directory by walking up from cwd.
137
- * @returns {string} path to .agent/
138
- */
139
- function findAgentDir() {
140
- let current = path.resolve(process.cwd());
141
- const root = path.parse(current).root;
142
- while (current !== root) {
143
- const candidate = path.join(current, '.agent');
144
- if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) return candidate;
145
- current = path.dirname(current);
146
- }
147
- console.error(`${RED}✖ .agent/ not found. Run: npx tribunal-kit init${RESET}`);
148
- process.exit(1);
149
- }
150
-
151
- /**
152
- * Parse the YAML frontmatter from a SKILL.md file.
153
- * Returns { name, description, ... } or null on parse failure.
154
- * @param {string} content - Full SKILL.md file text
155
- */
156
- function parseFrontmatter(content) {
157
- const match = content.match(/^---\n([\s\S]*?)\n---/);
158
- if (!match) return null;
159
- const yaml = match[1];
160
- const obj = {};
161
- for (const line of yaml.split('\n')) {
162
- const sep = line.indexOf(':');
163
- if (sep === -1) continue;
164
- const key = line.slice(0, sep).trim();
165
- const val = line.slice(sep + 1).trim().replace(/^["']|["']$/g, '');
166
- obj[key] = val;
167
- }
168
- return obj;
169
- }
170
-
171
- /**
172
- * Extract the "key rules" section from a SKILL.md for condensed Level-1 context.
173
- * Falls back to first 800 chars of content if no section found.
174
- * @param {string} content
175
- */
176
- function extractKeyRules(content) {
177
- // Try to find sections named: Key Rules, Rules, Core Rules, Critical Rules, Guardrails
178
- const sectionMatch = content.match(
179
- /##\s+(?:Key Rules?|Core Rules?|Critical Rules?|Guardrails?|Rules?)\n([\s\S]*?)(?=\n##\s|$)/i
180
- );
181
- if (sectionMatch) return sectionMatch[1].trim().slice(0, 1200);
182
- // Fallback: strip frontmatter and take first 800 chars
183
- const bodyStart = content.indexOf('---', 3);
184
- const body = bodyStart !== -1 ? content.slice(bodyStart + 3).trim() : content;
185
- return body.slice(0, 800).trim();
186
- }
187
-
188
- /**
189
- * Load all skills from .agent/skills/ directory.
190
- * Returns an array of { name, file, frontmatter, content, keyRules } objects.
191
- * @param {string} agentDir
192
- */
193
- function loadSkills(agentDir) {
194
- const skillsDir = path.join(agentDir, 'skills');
195
- if (!fs.existsSync(skillsDir)) return [];
196
-
197
- const skills = [];
198
- const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
199
-
200
- for (const entry of entries) {
201
- if (!entry.isDirectory()) continue;
202
- const skillFile = path.join(skillsDir, entry.name, 'SKILL.md');
203
- if (!fs.existsSync(skillFile)) continue;
204
-
205
- try {
206
- const content = fs.readFileSync(skillFile, 'utf8');
207
- const frontmatter = parseFrontmatter(content) || {};
208
- const keyRules = extractKeyRules(content);
209
- skills.push({
210
- name: entry.name,
211
- file: skillFile,
212
- frontmatter,
213
- content,
214
- keyRules,
215
- description: frontmatter.description || '',
216
- });
217
- } catch {
218
- // Skip unreadable skills silently
219
- }
220
- }
221
- return skills;
222
- }
223
-
224
- // ── Scoring engine ────────────────────────────────────────────────────────────
225
-
226
- /**
227
- * Tokenize text into lowercase words (3+ chars).
228
- * @param {string} text
229
- * @returns {string[]}
230
- */
231
- function tokenize(text) {
232
- return (text.match(/\b[a-zA-Z_][a-zA-Z0-9_]{2,}\b/g) || []).map(t => t.toLowerCase());
233
- }
234
-
235
- /**
236
- * Compute a relevance score for a skill against the user's task.
237
- *
238
- * Scoring breakdown:
239
- * - Text overlap between task tokens and skill description/name: up to +5
240
- * - Domain affinity keyword match: +weight (2 or 3) per match
241
- * - File extension affinity: +2 per match
242
- * - Baseline skill bonus: +1 (always present)
243
- *
244
- * @param {{ name, description, content }} skill
245
- * @param {string} task - Raw task text from the user
246
- * @param {string[]} fileExts - File extensions being touched
247
- * @param {string[]} taskTokens - Pre-tokenized task
248
- * @returns {number} score
249
- */
250
- function scoreSkill(skill, task, fileExts, taskTokens) {
251
- let score = 0;
252
- const taskLower = task.toLowerCase();
253
- const skillText = (skill.name + ' ' + skill.description).toLowerCase();
254
- const skillTokens = tokenize(skillText);
255
-
256
- // 1. Token overlap (lightweight TF match — no IDF needed at this scale)
257
- const skillSet = new Set(skillTokens);
258
- for (const token of taskTokens) {
259
- if (skillSet.has(token)) score += 1;
260
- }
261
-
262
- // 2. Domain affinity boost
263
- for (const affinity of DOMAIN_AFFINITIES) {
264
- const keywordMatch = affinity.keywords.some(k => taskLower.includes(k));
265
- if (!keywordMatch) continue;
266
- if (affinity.skills.includes(skill.name)) {
267
- score += affinity.weight;
268
- }
269
- }
270
-
271
- // 3. File extension boost
272
- for (const ext of fileExts) {
273
- const extSkills = EXT_AFFINITIES[ext] || [];
274
- if (extSkills.includes(skill.name)) score += 2;
275
- }
276
-
277
- // 4. Baseline skill safety net
278
- if (BASELINE_SKILLS.includes(skill.name)) score += 1;
279
-
280
- return score;
281
- }
282
-
283
- /**
284
- * Run the tiered selection algorithm.
285
- *
286
- * @param {string} task - Raw user task description
287
- * @param {string[]} files - Affected filenames (for ext detection)
288
- * @param {string} model - 'large' | 'small' | 'auto'
289
- * @param {object[]} skills - Loaded skills array from loadSkills()
290
- * @returns {{ essential: object[], supplementary: object[], available: object[], scores: Map }}
291
- */
292
- function selectSkills(task, files, model, skills) {
293
- const taskTokens = tokenize(task);
294
- const fileExts = files.map(f => path.extname(f).toLowerCase()).filter(Boolean);
295
-
296
- // Score every available skill
297
- const scored = skills.map(skill => ({
298
- ...skill,
299
- score: scoreSkill(skill, task, fileExts, taskTokens),
300
- }));
301
- scored.sort((a, b) => b.score - a.score);
302
-
303
- // Determine tier thresholds
304
- const maxScore = scored[0]?.score || 1;
305
- const tier0Cut = Math.max(maxScore * 0.65, 2); // Essential: top 65%+ of max score
306
- const tier1Cut = Math.max(maxScore * 0.3, 1); // Supplementary: 30–65%
307
-
308
- const essential = scored.filter(s => s.score >= tier0Cut).slice(0, 10);
309
- const supplementary = scored.filter(s => s.score < tier0Cut && s.score >= tier1Cut).slice(0, 8);
310
- const available = scored.filter(s => s.score < tier1Cut && s.score > 0).slice(0, 10);
311
-
312
- // Ensure baseline skills always appear at minimum in supplementary
313
- for (const base of BASELINE_SKILLS) {
314
- const inEssential = essential.find(s => s.name === base);
315
- const inSupplementary = supplementary.find(s => s.name === base);
316
- if (!inEssential && !inSupplementary) {
317
- const baseSkill = skills.find(s => s.name === base);
318
- if (baseSkill) supplementary.push({ ...baseSkill, score: 0.5 });
319
- }
320
- }
321
-
322
- // For small models: collapse supplementary into available
323
- if (model === 'small') {
324
- return {
325
- essential: essential.slice(0, 6),
326
- supplementary: [],
327
- available: [...supplementary, ...available].slice(0, 8),
328
- scores: buildScoreMap(scored),
329
- };
330
- }
331
-
332
- return { essential, supplementary, available, scores: buildScoreMap(scored) };
333
- }
334
-
335
- function buildScoreMap(scored) {
336
- const m = new Map();
337
- for (const s of scored) m.set(s.name, s.score);
338
- return m;
339
- }
340
-
341
- // ── Output formatters ─────────────────────────────────────────────────────────
342
-
343
- function formatReport(task, model, selection, elapsed) {
344
- const { essential, supplementary, available } = selection;
345
- const total = essential.length + supplementary.length + available.length;
346
-
347
- console.log(`\n${BOLD}${CYAN}━━━ Context Broker Skill Selection ━━━━━━━━━━━━━━━${RESET}`);
348
- console.log(` Task : ${BOLD}${task.slice(0, 80)}${task.length > 80 ? '...' : ''}${RESET}`);
349
- console.log(` Model : ${model === 'large' ? GREEN : YELLOW}${model}${RESET} ${DIM}(Focus without Compromise)${RESET}`);
350
- console.log(` Skills : ${GREEN}${essential.length} essential${RESET} + ${YELLOW}${supplementary.length} supplementary${RESET} + ${DIM}${available.length} available${RESET} of ${total} matched`);
351
- console.log(` Time : ${elapsed}ms\n`);
352
-
353
- if (essential.length) {
354
- console.log(` ${GREEN}${BOLD}▶ Level 0 Essential (Full Context, Top Priority):${RESET}`);
355
- for (const s of essential) {
356
- const score = selection.scores.get(s.name) || 0;
357
- console.log(` ${GREEN}✦${RESET} ${BOLD}${s.name}${RESET} ${DIM}score=${score.toFixed(1)}${RESET}`);
358
- if (s.description) console.log(` ${DIM}${s.description.slice(0, 90)}${RESET}`);
359
- }
360
- }
361
-
362
- if (supplementary.length) {
363
- console.log(`\n ${YELLOW}${BOLD}▶ Level 1 — Supplementary (Key Rules Only):${RESET}`);
364
- for (const s of supplementary) {
365
- const score = selection.scores.get(s.name) || 0;
366
- console.log(` ${YELLOW}◆${RESET} ${s.name} ${DIM}score=${score.toFixed(1)}${RESET}`);
367
- }
368
- }
369
-
370
- if (available.length) {
371
- console.log(`\n ${DIM}▶ Level 2 — Available (Name Reference Only):${RESET}`);
372
- console.log(` ${DIM}${available.map(s => s.name).join(', ')}${RESET}`);
373
- }
374
-
375
- if (model === 'small') {
376
- console.log(`\n ${YELLOW}⚡ Small model mode: supplementary collapsed. Essential only injected.${RESET}`);
377
- }
378
-
379
- console.log(`\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n`);
380
- }
381
-
382
- function formatJson(task, model, selection) {
383
- return JSON.stringify({
384
- task,
385
- model,
386
- timestamp: new Date().toISOString(),
387
- essential: selection.essential.map(s => ({ name: s.name, score: selection.scores.get(s.name), description: s.description })),
388
- supplementary: selection.supplementary.map(s => ({ name: s.name, score: selection.scores.get(s.name) })),
389
- available: selection.available.map(s => s.name),
390
- }, null, 2);
391
- }
392
-
393
- function formatNames(selection, model) {
394
- const all = model === 'large'
395
- ? [...selection.essential, ...selection.supplementary]
396
- : selection.essential;
397
- return all.map(s => s.name).join('\n');
398
- }
399
-
400
- /**
401
- * Build a full LLM-ready context prompt string.
402
- * This is the full output to be injected into an AI system prompt.
403
- *
404
- * For large models: Essential = full SKILL.md, Supplementary = key rules section.
405
- * For small models: Essential = key rules section only.
406
- *
407
- * @param {string} task
408
- * @param {string} model
409
- * @param {object} selection
410
- * @returns {string}
411
- */
412
- function formatPrompt(task, model, selection) {
413
- const lines = [
414
- `# Tribunal Context Broker — Injected Skills`,
415
- `# Task: ${task}`,
416
- `# Model tier: ${model}`,
417
- `# Generated: ${new Date().toISOString()}`,
418
- '',
419
- '## Instructions for the AI',
420
- 'The following skills are ordered by relevance to the current task.',
421
- 'Level 0 skills contain full rule sets. Level 1 skills contain key rules only.',
422
- 'Treat ALL injected rules as mandatory constraints, not suggestions.',
423
- '',
424
- '---',
425
- '',
426
- ];
427
-
428
- if (selection.essential.length) {
429
- lines.push('## Level 0 Essential Skills (Full Context)');
430
- lines.push('');
431
- for (const s of selection.essential) {
432
- lines.push(`### Skill: ${s.name}`);
433
- lines.push('');
434
- if (model === 'large') {
435
- lines.push(s.content || s.keyRules);
436
- } else {
437
- lines.push(s.keyRules);
438
- }
439
- lines.push('');
440
- lines.push('---');
441
- lines.push('');
442
- }
443
- }
444
-
445
- if (model === 'large' && selection.supplementary.length) {
446
- lines.push('## Level 1 — Supplementary Skills (Key Rules)');
447
- lines.push('');
448
- for (const s of selection.supplementary) {
449
- lines.push(`### Skill: ${s.name} (condensed)`);
450
- lines.push('');
451
- lines.push(s.keyRules);
452
- lines.push('');
453
- lines.push('---');
454
- lines.push('');
455
- }
456
- }
457
-
458
- if (selection.available.length) {
459
- lines.push('## Level 2 Available Skills (Reference Names Only)');
460
- lines.push('');
461
- lines.push('The following skills are relevant but not injected to maintain context density.');
462
- lines.push('Request their full content if needed: ' + selection.available.map(s => s.name).join(', '));
463
- lines.push('');
464
- }
465
-
466
- return lines.join('\n');
467
- }
468
-
469
- // ── Built-in demo ─────────────────────────────────────────────────────────────
470
-
471
- function runDemo(agentDir) {
472
- const skills = loadSkills(agentDir);
473
-
474
- const scenarios = [
475
- { task: 'Build a JWT authentication API with Express.js and Zod validation', file: 'auth.ts', model: 'large' },
476
- { task: 'Design a premium landing page with GSAP scroll animations', file: 'Hero.tsx', model: 'large' },
477
- { task: 'Write a Prisma query for paginated user orders', file: 'orders.ts', model: 'small' },
478
- { task: 'Add RAG pipeline to an OpenAI-powered chat interface', file: 'chat.ts', model: 'large' },
479
- ];
480
-
481
- console.log(`\n${BOLD}${CYAN}━━━ Context Broker Demo Mode ━━━━━━━━━━━━━━━━━━━━━${RESET}`);
482
- console.log(` Loaded ${skills.length} skills from .agent/skills/\n`);
483
-
484
- for (const scenario of scenarios) {
485
- const t0 = Date.now();
486
- const model = scenario.model;
487
- const selection = selectSkills(scenario.task, [scenario.file], model, skills);
488
- const elapsed = Date.now() - t0;
489
-
490
- console.log(`\n ${BOLD}Task: "${scenario.task.slice(0, 70)}"${RESET} ${DIM}[${model} model]${RESET}`);
491
- console.log(` Essential : ${GREEN}${selection.essential.map(s => s.name).join(', ')}${RESET}`);
492
- console.log(` Supplementary : ${YELLOW}${selection.supplementary.map(s => s.name).join(', ') || '(small mode)'}${RESET}`);
493
- console.log(` Time : ${DIM}${elapsed}ms${RESET}`);
494
- }
495
-
496
- console.log(`\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n`);
497
- }
498
-
499
- // ── Public API ────────────────────────────────────────────────────────────────
500
-
501
- /**
502
- * Programmatic API for use by other Tribunal scripts.
503
- *
504
- * @param {string} task - User task description
505
- * @param {string[]} files - Files being touched
506
- * @param {string} model - 'large' | 'small' | 'auto'
507
- * @param {string} agentDir - Path to .agent/ directory
508
- * @returns {{ essential, supplementary, available, promptText }}
509
- */
510
- function broker(task, files = [], model = 'large', agentDir = null) {
511
- const resolvedAgentDir = agentDir || findAgentDir();
512
- const skills = loadSkills(resolvedAgentDir);
513
- const selection = selectSkills(task, files, model, skills);
514
- const promptText = formatPrompt(task, model, selection);
515
- return { ...selection, promptText };
516
- }
517
-
518
- module.exports = { broker, selectSkills, loadSkills, scoreSkill, tokenize, findAgentDir };
519
-
520
- // ── CLI Entry ─────────────────────────────────────────────────────────────────
521
-
522
- if (require.main === module) {
523
- const argv = process.argv.slice(2);
524
-
525
- if (!argv.length || argv.includes('--help') || argv.includes('-h')) {
526
- console.log(`
527
- ${BOLD}context_broker.js${RESET} Tribunal Focus-without-Compromise Context Engine
528
-
529
- ${BOLD}Usage:${RESET}
530
- node .agent/scripts/context_broker.js --task "<description>" [options]
531
- node .agent/scripts/context_broker.js demo
532
-
533
- ${BOLD}Options:${RESET}
534
- --task <text> Task description to match against skill catalogue
535
- --file <path> File being touched (repeat for multiple files)
536
- --model <size> large (default) | small — model tier
537
- --output <format> report (default) | json | names | prompt
538
-
539
- ${BOLD}Model tiers:${RESET}
540
- large Full Essential + condensed Supplementary (Claude Opus, Gemini 2.5 Pro, GPT-4o)
541
- small Essential only (Gemini Flash, GPT-4o-mini)
542
-
543
- ${BOLD}Examples:${RESET}
544
- node .agent/scripts/context_broker.js --task "JWT auth API" --model large
545
- node .agent/scripts/context_broker.js --task "landing page" --file Hero.tsx --output names
546
- node .agent/scripts/context_broker.js --task "RAG pipeline" --output prompt > context.md
547
- node .agent/scripts/context_broker.js demo
548
- `);
549
- process.exit(0);
550
- }
551
-
552
- const agentDir = findAgentDir();
553
-
554
- if (argv[0] === 'demo') {
555
- runDemo(agentDir);
556
- process.exit(0);
557
- }
558
-
559
- // Parse args
560
- const taskIdx = argv.indexOf('--task');
561
- const modelIdx = argv.indexOf('--model');
562
- const outputIdx = argv.indexOf('--output');
563
-
564
- const task = taskIdx !== -1 && argv[taskIdx + 1] ? argv[taskIdx + 1] : '';
565
- const model = modelIdx !== -1 && argv[modelIdx + 1] ? argv[modelIdx + 1] : 'large';
566
- const output = outputIdx !== -1 && argv[outputIdx + 1] ? argv[outputIdx + 1] : 'report';
567
-
568
- if (!task) {
569
- console.error(`${RED}✖ --task is required${RESET}`);
570
- process.exit(1);
571
- }
572
-
573
- // Collect --file arguments (may appear multiple times)
574
- const files = [];
575
- for (let i = 0; i < argv.length; i++) {
576
- if (argv[i] === '--file' && argv[i + 1]) files.push(argv[i + 1]);
577
- }
578
-
579
- const validModels = ['large', 'small'];
580
- let effectiveModel = model;
581
- if (!validModels.includes(model)) {
582
- console.error(`${YELLOW}⚠ Unknown model tier "${model}" — defaulting to "large"${RESET}`);
583
- effectiveModel = 'large';
584
- }
585
-
586
- const t0 = Date.now();
587
- const skills = loadSkills(agentDir);
588
- const selection = selectSkills(task, files, effectiveModel, skills);
589
- const elapsed = Date.now() - t0;
590
-
591
- switch (output) {
592
- case 'json':
593
- console.log(formatJson(task, effectiveModel, selection));
594
- break;
595
- case 'names':
596
- console.log(formatNames(selection, effectiveModel));
597
- break;
598
- case 'prompt':
599
- console.log(formatPrompt(task, effectiveModel, selection));
600
- break;
601
- default: // 'report'
602
- formatReport(task, effectiveModel, selection, elapsed);
603
- break;
604
- }
605
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * context_broker.js — Tribunal Kit Context Density Broker
4
+ * =========================================================
5
+ * "Focus without Compromise" — Intelligent skill selection for all model sizes.
6
+ *
7
+ * Philosophy:
8
+ * This is NOT a filter that removes context. It is a PRIORITIZER that
9
+ * ensures the most relevant rules occupy the highest-attention positions
10
+ * in the AI's context window. Supplementary context is condensed, not cut.
11
+ *
12
+ * For LARGER models (Claude Opus, Gemini 2.5 Pro, GPT-4o):
13
+ * → Level 0 (Essential) skills are injected with full fidelity at the top.
14
+ * → Level 1 (Supplementary) skills are condensed to their key rules only.
15
+ * → Nothing is removed — the model gets everything, ordered optimally.
16
+ *
17
+ * For SMALLER/FASTER models (Gemini Flash, GPT-4o-mini):
18
+ * → Only Level 0 (Essential) skills are included.
19
+ * → This prevents context overflow and attention dilution.
20
+ * → Quality gates remain uncompromised — just fewer rules to track.
21
+ *
22
+ * Tiered Context Priority:
23
+ * Level 0 — Essential: Top matches, full SKILL.md text, injected first
24
+ * Level 1 — Supplementary: Medium matches, condensed to "key rules" section
25
+ * Level 2 — Available: Low matches, listed by name only (for reference)
26
+ *
27
+ * Scoring Algorithm:
28
+ * - Task keyword TF-IDF match against skill frontmatter + description
29
+ * - File type affinity (e.g., .tsx → react-specialist gets +2 boost)
30
+ * - Domain tag match (e.g., "sql" in task → sql-pro gets +3 boost)
31
+ * - Recency boost: skills referenced in the last 3 sessions rank higher
32
+ * - Tribunal alignment: skills matching active reviewers rank higher
33
+ *
34
+ * Usage:
35
+ * node .agent/scripts/context_broker.js --task "Build a login API with JWT"
36
+ * node .agent/scripts/context_broker.js --task "Design a premium landing page" --file Login.tsx
37
+ * node .agent/scripts/context_broker.js --task "..." --model large --output json
38
+ * node .agent/scripts/context_broker.js --task "..." --model small --output names
39
+ * node .agent/scripts/context_broker.js demo
40
+ *
41
+ * Output modes:
42
+ * report (default) — human-readable tiered selection report
43
+ * json — JSON with tiered skill lists
44
+ * names — newline-separated skill names only (for piping)
45
+ * prompt — full injected prompt text ready for an LLM
46
+ */
47
+
48
+ "use strict";
49
+
50
+ const fs = require("fs");
51
+ const path = require("path");
52
+
53
+ // ── Colours ───────────────────────────────────────────────────────────────────
54
+ const { GREEN, YELLOW, CYAN, RED, BOLD, DIM, RESET } = require("./_colors");
55
+
56
+ // ── Domain → Skill affinity map ───────────────────────────────────────────────
57
+ // Keywords in the user's task that strongly indicate specific skills.
58
+ // Higher weight = stronger signal.
59
+ const DOMAIN_AFFINITIES = [
60
+ // Backend / API
61
+ {
62
+ keywords: [
63
+ "api",
64
+ "rest",
65
+ "route",
66
+ "endpoint",
67
+ "handler",
68
+ "express",
69
+ "fastapi",
70
+ "hono",
71
+ ],
72
+ skills: [
73
+ "api-patterns",
74
+ "nodejs-best-practices",
75
+ "backend-specialist",
76
+ "error-resilience",
77
+ ],
78
+ weight: 3,
79
+ },
80
+ // Database
81
+ {
82
+ keywords: [
83
+ "sql",
84
+ "query",
85
+ "database",
86
+ "prisma",
87
+ "drizzle",
88
+ "orm",
89
+ "postgres",
90
+ "mysql",
91
+ "schema",
92
+ ],
93
+ skills: ["database-design", "sql-pro", "supabase-postgres-best-practices"],
94
+ weight: 3,
95
+ },
96
+ // Authentication
97
+ {
98
+ keywords: [
99
+ "auth",
100
+ "jwt",
101
+ "login",
102
+ "oauth",
103
+ "session",
104
+ "password",
105
+ "token",
106
+ "rbac",
107
+ ],
108
+ skills: [
109
+ "authentication-best-practices",
110
+ "vulnerability-scanner",
111
+ "api-security-auditor",
112
+ ],
113
+ weight: 3,
114
+ },
115
+ // React / Next.js
116
+ {
117
+ keywords: [
118
+ "react",
119
+ "next",
120
+ "nextjs",
121
+ "component",
122
+ "hook",
123
+ "jsx",
124
+ "tsx",
125
+ "server component",
126
+ "server action",
127
+ ],
128
+ skills: ["react-specialist", "nextjs-react-expert", "frontend-design"],
129
+ weight: 3,
130
+ },
131
+ // Frontend / UI
132
+ {
133
+ keywords: [
134
+ "ui",
135
+ "design",
136
+ "landing",
137
+ "page",
138
+ "layout",
139
+ "responsive",
140
+ "tailwind",
141
+ "css",
142
+ "style",
143
+ ],
144
+ skills: [
145
+ "frontend-design",
146
+ "ui-ux-pro-max",
147
+ "tailwind-patterns",
148
+ "web-design-guidelines",
149
+ ],
150
+ weight: 2,
151
+ },
152
+ // Animation / Motion
153
+ {
154
+ keywords: [
155
+ "animation",
156
+ "gsap",
157
+ "framer",
158
+ "motion",
159
+ "scroll",
160
+ "transition",
161
+ "parallax",
162
+ ],
163
+ skills: [
164
+ "motion-engineering",
165
+ "framer-motion-expert",
166
+ "gsap-core",
167
+ "gsap-scrolltrigger",
168
+ ],
169
+ weight: 3,
170
+ },
171
+ // AI / LLM
172
+ {
173
+ keywords: [
174
+ "llm",
175
+ "openai",
176
+ "anthropic",
177
+ "gemini",
178
+ "embedding",
179
+ "ai",
180
+ "rag",
181
+ "vector",
182
+ "chat",
183
+ "prompt",
184
+ ],
185
+ skills: [
186
+ "llm-engineering",
187
+ "ai-prompt-injection-defense",
188
+ "agentic-patterns",
189
+ ],
190
+ weight: 3,
191
+ },
192
+ // Security
193
+ {
194
+ keywords: [
195
+ "security",
196
+ "xss",
197
+ "injection",
198
+ "owasp",
199
+ "vulnerability",
200
+ "csrf",
201
+ "sanitize",
202
+ "audit",
203
+ ],
204
+ skills: [
205
+ "vulnerability-scanner",
206
+ "api-security-auditor",
207
+ "authentication-best-practices",
208
+ ],
209
+ weight: 3,
210
+ },
211
+ // Testing
212
+ {
213
+ keywords: [
214
+ "test",
215
+ "spec",
216
+ "jest",
217
+ "vitest",
218
+ "playwright",
219
+ "unit test",
220
+ "e2e",
221
+ "mock",
222
+ ],
223
+ skills: ["testing-patterns", "playwright-best-practices", "tdd-workflow"],
224
+ weight: 3,
225
+ },
226
+ // Performance
227
+ {
228
+ keywords: [
229
+ "performance",
230
+ "optimize",
231
+ "bundle",
232
+ "cache",
233
+ "speed",
234
+ "slow",
235
+ "lighthouse",
236
+ "cwv",
237
+ ],
238
+ skills: ["performance-profiling", "motion-engineering", "edge-computing"],
239
+ weight: 2,
240
+ },
241
+ // Mobile
242
+ {
243
+ keywords: [
244
+ "mobile",
245
+ "react native",
246
+ "expo",
247
+ "ios",
248
+ "android",
249
+ "gesture",
250
+ "haptic",
251
+ ],
252
+ skills: ["building-native-ui", "mobile-design", "agentic-patterns"],
253
+ weight: 3,
254
+ },
255
+ // DevOps / CI
256
+ {
257
+ keywords: [
258
+ "docker",
259
+ "ci",
260
+ "cd",
261
+ "deploy",
262
+ "pipeline",
263
+ "k8s",
264
+ "kubernetes",
265
+ "github actions",
266
+ ],
267
+ skills: ["devops-engineer", "deployment-procedures", "observability"],
268
+ weight: 2,
269
+ },
270
+ // Real-time
271
+ {
272
+ keywords: [
273
+ "realtime",
274
+ "websocket",
275
+ "sse",
276
+ "socket",
277
+ "live",
278
+ "multiplayer",
279
+ "collaborative",
280
+ ],
281
+ skills: ["realtime-patterns", "error-resilience"],
282
+ weight: 3,
283
+ },
284
+ // TypeScript
285
+ {
286
+ keywords: [
287
+ "typescript",
288
+ "type",
289
+ "generic",
290
+ "interface",
291
+ "satisfies",
292
+ "zod",
293
+ "pydantic",
294
+ ],
295
+ skills: ["typescript-advanced", "data-validation-schemas"],
296
+ weight: 2,
297
+ },
298
+ // Python
299
+ {
300
+ keywords: ["python", "fastapi", "django", "flask", "pydantic", "asyncio"],
301
+ skills: ["python-pro", "python-patterns"],
302
+ weight: 3,
303
+ },
304
+ // Architecture
305
+ {
306
+ keywords: [
307
+ "architecture",
308
+ "refactor",
309
+ "clean",
310
+ "solid",
311
+ "design pattern",
312
+ "monorepo",
313
+ "microservice",
314
+ ],
315
+ skills: ["architecture", "clean-code", "monorepo-management"],
316
+ weight: 2,
317
+ },
318
+ // C# / .NET
319
+ {
320
+ keywords: [
321
+ "csharp",
322
+ "c#",
323
+ "dotnet",
324
+ ".net",
325
+ "blazor",
326
+ "aspnet",
327
+ "entity framework",
328
+ ],
329
+ skills: ["csharp-developer"],
330
+ weight: 3,
331
+ },
332
+ ];
333
+
334
+ // ── File extension → skill boost map ─────────────────────────────────────────
335
+ const EXT_AFFINITIES = {
336
+ ".tsx": ["react-specialist", "nextjs-react-expert", "typescript-advanced"],
337
+ ".jsx": ["react-specialist", "frontend-design"],
338
+ ".ts": ["typescript-advanced", "nodejs-best-practices"],
339
+ ".vue": ["vue-expert"],
340
+ ".py": ["python-pro", "python-patterns"],
341
+ ".cs": ["csharp-developer"],
342
+ ".sql": ["sql-pro", "database-design"],
343
+ ".css": ["tailwind-patterns", "frontend-design"],
344
+ };
345
+
346
+ // ── Core baseline skills — always available to all model sizes ────────────────
347
+ // These are injected for every request at a condensed level.
348
+ const BASELINE_SKILLS = [
349
+ "clean-code",
350
+ "systematic-debugging",
351
+ "error-resilience",
352
+ ];
353
+
354
+ // ── Skill catalogue (loaded from disk) ───────────────────────────────────────
355
+
356
+ /**
357
+ * Find the .agent directory by walking up from cwd.
358
+ * @returns {string} path to .agent/
359
+ */
360
+ function findAgentDir() {
361
+ let current = path.resolve(process.cwd());
362
+ const root = path.parse(current).root;
363
+ while (current !== root) {
364
+ const candidate = path.join(current, ".agent");
365
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory())
366
+ return candidate;
367
+ current = path.dirname(current);
368
+ }
369
+ console.error(
370
+ `${RED}✖ .agent/ not found. Run: npx tribunal-kit init${RESET}`,
371
+ );
372
+ process.exit(1);
373
+ }
374
+
375
+ /**
376
+ * Parse the YAML frontmatter from a SKILL.md file.
377
+ * Returns { name, description, ... } or null on parse failure.
378
+ * @param {string} content - Full SKILL.md file text
379
+ */
380
+ function parseFrontmatter(content) {
381
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
382
+ if (!match) return null;
383
+ const yaml = match[1];
384
+ const obj = {};
385
+ for (const line of yaml.split("\n")) {
386
+ const sep = line.indexOf(":");
387
+ if (sep === -1) continue;
388
+ const key = line.slice(0, sep).trim();
389
+ const val = line
390
+ .slice(sep + 1)
391
+ .trim()
392
+ .replace(/^["']|["']$/g, "");
393
+ obj[key] = val;
394
+ }
395
+ return obj;
396
+ }
397
+
398
+ /**
399
+ * Extract the "key rules" section from a SKILL.md for condensed Level-1 context.
400
+ * Falls back to first 800 chars of content if no section found.
401
+ * @param {string} content
402
+ */
403
+ function extractKeyRules(content) {
404
+ // Try to find sections named: Key Rules, Rules, Core Rules, Critical Rules, Guardrails
405
+ const sectionMatch = content.match(
406
+ /##\s+(?:Key Rules?|Core Rules?|Critical Rules?|Guardrails?|Rules?)\n([\s\S]*?)(?=\n##\s|$)/i,
407
+ );
408
+ if (sectionMatch) return sectionMatch[1].trim().slice(0, 1200);
409
+ // Fallback: strip frontmatter and take first 800 chars
410
+ const bodyStart = content.indexOf("---", 3);
411
+ const body = bodyStart !== -1 ? content.slice(bodyStart + 3).trim() : content;
412
+ return body.slice(0, 800).trim();
413
+ }
414
+
415
+ /**
416
+ * Load all skills from .agent/skills/ directory.
417
+ * Returns an array of { name, file, frontmatter, content, keyRules } objects.
418
+ * @param {string} agentDir
419
+ */
420
+ function loadSkills(agentDir) {
421
+ const skillsDir = path.join(agentDir, "skills");
422
+ if (!fs.existsSync(skillsDir)) return [];
423
+
424
+ const skills = [];
425
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
426
+
427
+ for (const entry of entries) {
428
+ if (!entry.isDirectory()) continue;
429
+ const skillFile = path.join(skillsDir, entry.name, "SKILL.md");
430
+ if (!fs.existsSync(skillFile)) continue;
431
+
432
+ try {
433
+ let content = fs.readFileSync(skillFile, "utf8");
434
+
435
+ // Strip duplicated global boilerplate sections
436
+ content = content.replace(/AI coding assistants often fall into specific bad habits[\s\S]*$/g, "");
437
+ content = content.replace(/## 🤖 LLM-Specific Traps[\s\S]*$/g, "");
438
+ content = content.replace(/## 🏛️ Tribunal Integration[\s\S]*$/g, "");
439
+ content = content.replace(/## Pre-Flight Checklist[\s\S]*$/g, "");
440
+ content = content.trim();
441
+
442
+ const frontmatter = parseFrontmatter(content) || {};
443
+ const keyRules = extractKeyRules(content);
444
+ skills.push({
445
+ name: entry.name,
446
+ file: skillFile,
447
+ frontmatter,
448
+ content,
449
+ keyRules,
450
+ description: frontmatter.description || "",
451
+ });
452
+ } catch {
453
+ // Skip unreadable skills silently
454
+ }
455
+ }
456
+ return skills;
457
+ }
458
+
459
+ // ── Scoring engine ────────────────────────────────────────────────────────────
460
+
461
+ /**
462
+ * Tokenize text into lowercase words (3+ chars).
463
+ * @param {string} text
464
+ * @returns {string[]}
465
+ */
466
+ function tokenize(text) {
467
+ return (text.match(/\b[a-zA-Z_][a-zA-Z0-9_]{2,}\b/g) || []).map((t) =>
468
+ t.toLowerCase(),
469
+ );
470
+ }
471
+
472
+ /**
473
+ * Compute a relevance score for a skill against the user's task.
474
+ *
475
+ * Scoring breakdown:
476
+ * - Text overlap between task tokens and skill description/name: up to +5
477
+ * - Domain affinity keyword match: +weight (2 or 3) per match
478
+ * - File extension affinity: +2 per match
479
+ * - Baseline skill bonus: +1 (always present)
480
+ *
481
+ * @param {{ name, description, content }} skill
482
+ * @param {string} task - Raw task text from the user
483
+ * @param {string[]} fileExts - File extensions being touched
484
+ * @param {string[]} taskTokens - Pre-tokenized task
485
+ * @returns {number} score
486
+ */
487
+ function scoreSkill(skill, task, fileExts, taskTokens) {
488
+ let score = 0;
489
+ const taskLower = task.toLowerCase();
490
+ const skillText = (skill.name + " " + skill.description).toLowerCase();
491
+ const skillTokens = tokenize(skillText);
492
+
493
+ // 1. Token overlap (lightweight TF match — no IDF needed at this scale)
494
+ const skillSet = new Set(skillTokens);
495
+ for (const token of taskTokens) {
496
+ if (skillSet.has(token)) score += 1;
497
+ }
498
+
499
+ // 2. Domain affinity boost
500
+ for (const affinity of DOMAIN_AFFINITIES) {
501
+ const keywordMatch = affinity.keywords.some((k) => taskLower.includes(k));
502
+ if (!keywordMatch) continue;
503
+ if (affinity.skills.includes(skill.name)) {
504
+ score += affinity.weight;
505
+ }
506
+ }
507
+
508
+ // 3. File extension boost
509
+ for (const ext of fileExts) {
510
+ const extSkills = EXT_AFFINITIES[ext] || [];
511
+ if (extSkills.includes(skill.name)) score += 2;
512
+ }
513
+
514
+ // 4. Baseline skill safety net
515
+ if (BASELINE_SKILLS.includes(skill.name)) score += 1;
516
+
517
+ return score;
518
+ }
519
+
520
+ /**
521
+ * Run the tiered selection algorithm.
522
+ *
523
+ * @param {string} task - Raw user task description
524
+ * @param {string[]} files - Affected filenames (for ext detection)
525
+ * @param {string} model - 'large' | 'small' | 'auto'
526
+ * @param {object[]} skills - Loaded skills array from loadSkills()
527
+ * @returns {{ essential: object[], supplementary: object[], available: object[], scores: Map }}
528
+ */
529
+ function selectSkills(task, files, model, skills) {
530
+ const taskTokens = tokenize(task);
531
+ const fileExts = files
532
+ .map((f) => path.extname(f).toLowerCase())
533
+ .filter(Boolean);
534
+
535
+ // Score every available skill
536
+ const scored = skills.map((skill) => ({
537
+ ...skill,
538
+ score: scoreSkill(skill, task, fileExts, taskTokens),
539
+ }));
540
+ scored.sort((a, b) => b.score - a.score);
541
+
542
+ // Determine tier thresholds
543
+ const maxScore = scored[0]?.score || 1;
544
+ const tier0Cut = Math.max(maxScore * 0.65, 2); // Essential: top 65%+ of max score
545
+ const tier1Cut = Math.max(maxScore * 0.3, 1); // Supplementary: 30–65%
546
+
547
+ const essential = scored.filter((s) => s.score >= tier0Cut).slice(0, 10);
548
+ const supplementary = scored
549
+ .filter((s) => s.score < tier0Cut && s.score >= tier1Cut)
550
+ .slice(0, 8);
551
+ const available = scored
552
+ .filter((s) => s.score < tier1Cut && s.score > 0)
553
+ .slice(0, 10);
554
+
555
+ // Ensure baseline skills always appear at minimum in supplementary
556
+ for (const base of BASELINE_SKILLS) {
557
+ const inEssential = essential.find((s) => s.name === base);
558
+ const inSupplementary = supplementary.find((s) => s.name === base);
559
+ if (!inEssential && !inSupplementary) {
560
+ const baseSkill = skills.find((s) => s.name === base);
561
+ if (baseSkill) supplementary.push({ ...baseSkill, score: 0.5 });
562
+ }
563
+ }
564
+
565
+ // For small models: collapse supplementary into available
566
+ if (model === "small") {
567
+ return {
568
+ essential: essential.slice(0, 6),
569
+ supplementary: [],
570
+ available: [...supplementary, ...available].slice(0, 8),
571
+ scores: buildScoreMap(scored),
572
+ };
573
+ }
574
+
575
+ return { essential, supplementary, available, scores: buildScoreMap(scored) };
576
+ }
577
+
578
+ function buildScoreMap(scored) {
579
+ const m = new Map();
580
+ for (const s of scored) m.set(s.name, s.score);
581
+ return m;
582
+ }
583
+
584
+ // ── Output formatters ─────────────────────────────────────────────────────────
585
+
586
+ function formatReport(task, model, selection, elapsed) {
587
+ const { essential, supplementary, available } = selection;
588
+ const total = essential.length + supplementary.length + available.length;
589
+
590
+ console.log(
591
+ `\n${BOLD}${CYAN}━━━ Context Broker — Skill Selection ━━━━━━━━━━━━━━━${RESET}`,
592
+ );
593
+ console.log(
594
+ ` Task : ${BOLD}${task.slice(0, 80)}${task.length > 80 ? "..." : ""}${RESET}`,
595
+ );
596
+ console.log(
597
+ ` Model : ${model === "large" ? GREEN : YELLOW}${model}${RESET} ${DIM}(Focus without Compromise)${RESET}`,
598
+ );
599
+ console.log(
600
+ ` Skills : ${GREEN}${essential.length} essential${RESET} + ${YELLOW}${supplementary.length} supplementary${RESET} + ${DIM}${available.length} available${RESET} of ${total} matched`,
601
+ );
602
+ console.log(` Time : ${elapsed}ms\n`);
603
+
604
+ if (essential.length) {
605
+ console.log(
606
+ ` ${GREEN}${BOLD}▶ Level 0 — Essential (Full Context, Top Priority):${RESET}`,
607
+ );
608
+ for (const s of essential) {
609
+ const score = selection.scores.get(s.name) || 0;
610
+ console.log(
611
+ ` ${GREEN}✦${RESET} ${BOLD}${s.name}${RESET} ${DIM}score=${score.toFixed(1)}${RESET}`,
612
+ );
613
+ if (s.description)
614
+ console.log(` ${DIM}${s.description.slice(0, 90)}${RESET}`);
615
+ }
616
+ }
617
+
618
+ if (supplementary.length) {
619
+ console.log(
620
+ `\n ${YELLOW}${BOLD}▶ Level 1 — Supplementary (Key Rules Only):${RESET}`,
621
+ );
622
+ for (const s of supplementary) {
623
+ const score = selection.scores.get(s.name) || 0;
624
+ console.log(
625
+ ` ${YELLOW}◆${RESET} ${s.name} ${DIM}score=${score.toFixed(1)}${RESET}`,
626
+ );
627
+ }
628
+ }
629
+
630
+ if (available.length) {
631
+ console.log(
632
+ `\n ${DIM}▶ Level 2 — Available (Name Reference Only):${RESET}`,
633
+ );
634
+ console.log(` ${DIM}${available.map((s) => s.name).join(", ")}${RESET}`);
635
+ }
636
+
637
+ if (model === "small") {
638
+ console.log(
639
+ `\n ${YELLOW}⚡ Small model mode: supplementary collapsed. Essential only injected.${RESET}`,
640
+ );
641
+ }
642
+
643
+ console.log(
644
+ `\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n`,
645
+ );
646
+ }
647
+
648
+ function formatJson(task, model, selection) {
649
+ return JSON.stringify(
650
+ {
651
+ task,
652
+ model,
653
+ timestamp: new Date().toISOString(),
654
+ essential: selection.essential.map((s) => ({
655
+ name: s.name,
656
+ score: selection.scores.get(s.name),
657
+ description: s.description,
658
+ })),
659
+ supplementary: selection.supplementary.map((s) => ({
660
+ name: s.name,
661
+ score: selection.scores.get(s.name),
662
+ })),
663
+ available: selection.available.map((s) => s.name),
664
+ },
665
+ null,
666
+ 2,
667
+ );
668
+ }
669
+
670
+ function formatNames(selection, model) {
671
+ const all =
672
+ model === "large"
673
+ ? [...selection.essential, ...selection.supplementary]
674
+ : selection.essential;
675
+ return all.map((s) => s.name).join("\n");
676
+ }
677
+
678
+ /**
679
+ * Build a full LLM-ready context prompt string.
680
+ * This is the full output to be injected into an AI system prompt.
681
+ *
682
+ * For large models: Essential = full SKILL.md, Supplementary = key rules section.
683
+ * For small models: Essential = key rules section only.
684
+ *
685
+ * @param {string} task
686
+ * @param {string} model
687
+ * @param {object} selection
688
+ * @returns {string}
689
+ */
690
+ function formatPrompt(task, model, selection) {
691
+ const lines = [
692
+ `# Tribunal Context Broker — Injected Skills`,
693
+ `# Task: ${task}`,
694
+ `# Model tier: ${model}`,
695
+ `# Generated: ${new Date().toISOString()}`,
696
+ "",
697
+ "## Instructions for the AI",
698
+ "The following skills are ordered by relevance to the current task.",
699
+ "Level 0 skills contain full rule sets. Level 1 skills contain key rules only.",
700
+ "Treat ALL injected rules as mandatory constraints, not suggestions.",
701
+ "",
702
+ "---",
703
+ "",
704
+ ];
705
+
706
+ if (selection.essential.length) {
707
+ lines.push("## Level 0 — Essential Skills (Full Context)");
708
+ lines.push("");
709
+ for (const s of selection.essential) {
710
+ lines.push(`### Skill: ${s.name}`);
711
+ lines.push("");
712
+ if (model === "large") {
713
+ lines.push(s.content || s.keyRules);
714
+ } else {
715
+ lines.push(s.keyRules);
716
+ }
717
+ lines.push("");
718
+ lines.push("---");
719
+ lines.push("");
720
+ }
721
+ }
722
+
723
+ if (model === "large" && selection.supplementary.length) {
724
+ lines.push("## Level 1 — Supplementary Skills (Key Rules)");
725
+ lines.push("");
726
+ for (const s of selection.supplementary) {
727
+ lines.push(`### Skill: ${s.name} (condensed)`);
728
+ lines.push("");
729
+ lines.push(s.keyRules);
730
+ lines.push("");
731
+ lines.push("---");
732
+ lines.push("");
733
+ }
734
+ }
735
+
736
+ if (selection.available.length) {
737
+ lines.push("## Level 2 — Available Skills (Reference Names Only)");
738
+ lines.push("");
739
+ lines.push(
740
+ "The following skills are relevant but not injected to maintain context density.",
741
+ );
742
+ lines.push(
743
+ "Request their full content if needed: " +
744
+ selection.available.map((s) => s.name).join(", "),
745
+ );
746
+ lines.push("");
747
+ }
748
+
749
+ return lines.join("\n");
750
+ }
751
+
752
+ // ── Built-in demo ─────────────────────────────────────────────────────────────
753
+
754
+ function runDemo(agentDir) {
755
+ const skills = loadSkills(agentDir);
756
+
757
+ const scenarios = [
758
+ {
759
+ task: "Build a JWT authentication API with Express.js and Zod validation",
760
+ file: "auth.ts",
761
+ model: "large",
762
+ },
763
+ {
764
+ task: "Design a premium landing page with GSAP scroll animations",
765
+ file: "Hero.tsx",
766
+ model: "large",
767
+ },
768
+ {
769
+ task: "Write a Prisma query for paginated user orders",
770
+ file: "orders.ts",
771
+ model: "small",
772
+ },
773
+ {
774
+ task: "Add RAG pipeline to an OpenAI-powered chat interface",
775
+ file: "chat.ts",
776
+ model: "large",
777
+ },
778
+ ];
779
+
780
+ console.log(
781
+ `\n${BOLD}${CYAN}━━━ Context Broker — Demo Mode ━━━━━━━━━━━━━━━━━━━━━${RESET}`,
782
+ );
783
+ console.log(` Loaded ${skills.length} skills from .agent/skills/\n`);
784
+
785
+ for (const scenario of scenarios) {
786
+ const t0 = Date.now();
787
+ const model = scenario.model;
788
+ const selection = selectSkills(
789
+ scenario.task,
790
+ [scenario.file],
791
+ model,
792
+ skills,
793
+ );
794
+ const elapsed = Date.now() - t0;
795
+
796
+ console.log(
797
+ `\n ${BOLD}Task: "${scenario.task.slice(0, 70)}"${RESET} ${DIM}[${model} model]${RESET}`,
798
+ );
799
+ console.log(
800
+ ` Essential : ${GREEN}${selection.essential.map((s) => s.name).join(", ")}${RESET}`,
801
+ );
802
+ console.log(
803
+ ` Supplementary : ${YELLOW}${selection.supplementary.map((s) => s.name).join(", ") || "(small mode)"}${RESET}`,
804
+ );
805
+ console.log(` Time : ${DIM}${elapsed}ms${RESET}`);
806
+ }
807
+
808
+ console.log(
809
+ `\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n`,
810
+ );
811
+ }
812
+
813
+ // ── Public API ────────────────────────────────────────────────────────────────
814
+
815
+ /**
816
+ * Programmatic API for use by other Tribunal scripts.
817
+ *
818
+ * @param {string} task - User task description
819
+ * @param {string[]} files - Files being touched
820
+ * @param {string} model - 'large' | 'small' | 'auto'
821
+ * @param {string} agentDir - Path to .agent/ directory
822
+ * @returns {{ essential, supplementary, available, promptText }}
823
+ */
824
+ function broker(task, files = [], model = "large", agentDir = null) {
825
+ const resolvedAgentDir = agentDir || findAgentDir();
826
+ const skills = loadSkills(resolvedAgentDir);
827
+ const selection = selectSkills(task, files, model, skills);
828
+ const promptText = formatPrompt(task, model, selection);
829
+ return { ...selection, promptText };
830
+ }
831
+
832
+ module.exports = {
833
+ broker,
834
+ selectSkills,
835
+ loadSkills,
836
+ scoreSkill,
837
+ tokenize,
838
+ findAgentDir,
839
+ };
840
+
841
+ // ── CLI Entry ─────────────────────────────────────────────────────────────────
842
+
843
+ if (require.main === module) {
844
+ const argv = process.argv.slice(2);
845
+
846
+ if (!argv.length || argv.includes("--help") || argv.includes("-h")) {
847
+ console.log(`
848
+ ${BOLD}context_broker.js${RESET} — Tribunal Focus-without-Compromise Context Engine
849
+
850
+ ${BOLD}Usage:${RESET}
851
+ node .agent/scripts/context_broker.js --task "<description>" [options]
852
+ node .agent/scripts/context_broker.js demo
853
+
854
+ ${BOLD}Options:${RESET}
855
+ --task <text> Task description to match against skill catalogue
856
+ --file <path> File being touched (repeat for multiple files)
857
+ --model <size> large (default) | small — model tier
858
+ --output <format> report (default) | json | names | prompt
859
+
860
+ ${BOLD}Model tiers:${RESET}
861
+ large Full Essential + condensed Supplementary (Claude Opus, Gemini 2.5 Pro, GPT-4o)
862
+ small Essential only (Gemini Flash, GPT-4o-mini)
863
+
864
+ ${BOLD}Examples:${RESET}
865
+ node .agent/scripts/context_broker.js --task "JWT auth API" --model large
866
+ node .agent/scripts/context_broker.js --task "landing page" --file Hero.tsx --output names
867
+ node .agent/scripts/context_broker.js --task "RAG pipeline" --output prompt > context.md
868
+ node .agent/scripts/context_broker.js demo
869
+ `);
870
+ process.exit(0);
871
+ }
872
+
873
+ const agentDir = findAgentDir();
874
+
875
+ if (argv[0] === "demo") {
876
+ runDemo(agentDir);
877
+ process.exit(0);
878
+ }
879
+
880
+ // Parse args
881
+ const taskIdx = argv.indexOf("--task");
882
+ const modelIdx = argv.indexOf("--model");
883
+ const outputIdx = argv.indexOf("--output");
884
+
885
+ const task = taskIdx !== -1 && argv[taskIdx + 1] ? argv[taskIdx + 1] : "";
886
+ const model =
887
+ modelIdx !== -1 && argv[modelIdx + 1] ? argv[modelIdx + 1] : "large";
888
+ const output =
889
+ outputIdx !== -1 && argv[outputIdx + 1] ? argv[outputIdx + 1] : "report";
890
+
891
+ if (!task) {
892
+ console.error(`${RED}✖ --task is required${RESET}`);
893
+ process.exit(1);
894
+ }
895
+
896
+ // Collect --file arguments (may appear multiple times)
897
+ const files = [];
898
+ for (let i = 0; i < argv.length; i++) {
899
+ if (argv[i] === "--file" && argv[i + 1]) files.push(argv[i + 1]);
900
+ }
901
+
902
+ const validModels = ["large", "small"];
903
+ let effectiveModel = model;
904
+ if (!validModels.includes(model)) {
905
+ console.error(
906
+ `${YELLOW}⚠ Unknown model tier "${model}" — defaulting to "large"${RESET}`,
907
+ );
908
+ effectiveModel = "large";
909
+ }
910
+
911
+ const t0 = Date.now();
912
+ const skills = loadSkills(agentDir);
913
+ const selection = selectSkills(task, files, effectiveModel, skills);
914
+ const elapsed = Date.now() - t0;
915
+
916
+ switch (output) {
917
+ case "json":
918
+ console.log(formatJson(task, effectiveModel, selection));
919
+ break;
920
+ case "names":
921
+ console.log(formatNames(selection, effectiveModel));
922
+ break;
923
+ case "prompt":
924
+ console.log(formatPrompt(task, effectiveModel, selection));
925
+ break;
926
+ default: // 'report'
927
+ formatReport(task, effectiveModel, selection, elapsed);
928
+ break;
929
+ }
930
+ }