tribunal-kit 4.4.0 → 4.4.2

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 (90) hide show
  1. package/.agent/agents/api-architect.md +66 -66
  2. package/.agent/agents/db-latency-auditor.md +216 -216
  3. package/.agent/agents/precedence-reviewer.md +250 -250
  4. package/.agent/agents/resilience-reviewer.md +88 -88
  5. package/.agent/agents/schema-reviewer.md +67 -67
  6. package/.agent/agents/throughput-optimizer.md +299 -299
  7. package/.agent/agents/ui-ux-auditor.md +292 -292
  8. package/.agent/agents/vitals-reviewer.md +223 -223
  9. package/.agent/history/architecture-graph.yaml +32 -1
  10. package/.agent/history/graph-cache.json +66 -19
  11. package/.agent/history/snapshots/bin__tribunal-kit.js.json +19 -0
  12. package/.agent/history/snapshots/eslint.config.js.json +9 -0
  13. package/.agent/history/snapshots/migrate_refs.js.json +3 -3
  14. package/.agent/history/snapshots/scripts__changelog.js.json +2 -1
  15. package/.agent/history/snapshots/scripts__sync-version.js.json +2 -1
  16. package/.agent/history/snapshots/scripts__validate-payload.js.json +1 -0
  17. package/.agent/history/snapshots/test__integration__bridges.test.js.json +2 -1
  18. package/.agent/history/snapshots/test__integration__init.test.js.json +1 -0
  19. package/.agent/history/snapshots/test__integration__routing.test.js.json +1 -0
  20. package/.agent/history/snapshots/test__integration__swarm_dispatcher.test.js.json +2 -1
  21. package/.agent/history/snapshots/test__integration__wave2.test.js.json +2 -1
  22. package/.agent/history/snapshots/test__unit__args.test.js.json +11 -1
  23. package/.agent/history/snapshots/test__unit__case_law_manager.test.js.json +1 -0
  24. package/.agent/history/snapshots/test__unit__context_broker.test.js.json +11 -0
  25. package/.agent/history/snapshots/test__unit__copyDir.test.js.json +11 -1
  26. package/.agent/history/snapshots/test__unit__graph_tools.test.js.json +1 -0
  27. package/.agent/history/snapshots/test__unit__inner_loop_validator.test.js.json +11 -0
  28. package/.agent/history/snapshots/test__unit__selfInstall.test.js.json +11 -1
  29. package/.agent/history/snapshots/test__unit__semver.test.js.json +11 -1
  30. package/.agent/history/snapshots/test__unit__swarm_dispatcher.test.js.json +1 -0
  31. package/.agent/scripts/_colors.js +154 -2
  32. package/.agent/scripts/_utils.js +205 -3
  33. package/.agent/scripts/append_flow.js +72 -72
  34. package/.agent/scripts/auto_preview.js +197 -197
  35. package/.agent/scripts/bundle_analyzer.js +90 -119
  36. package/.agent/scripts/case_law_manager.js +18 -13
  37. package/.agent/scripts/checklist.js +100 -88
  38. package/.agent/scripts/colors.js +7 -13
  39. package/.agent/scripts/compress_skills.js +141 -141
  40. package/.agent/scripts/consolidate_skills.js +149 -149
  41. package/.agent/scripts/context_broker.js +605 -609
  42. package/.agent/scripts/deep_compress.js +150 -150
  43. package/.agent/scripts/dependency_analyzer.js +68 -106
  44. package/.agent/scripts/graph_builder.js +341 -311
  45. package/.agent/scripts/graph_visualizer.js +390 -384
  46. package/.agent/scripts/graph_zoom.js +6 -4
  47. package/.agent/scripts/inner_loop_validator.js +445 -465
  48. package/.agent/scripts/lint_runner.js +27 -28
  49. package/.agent/scripts/minify_context.js +100 -100
  50. package/.agent/scripts/mutation_runner.js +280 -280
  51. package/.agent/scripts/patch_skills_meta.js +156 -156
  52. package/.agent/scripts/patch_skills_output.js +244 -244
  53. package/.agent/scripts/schema_validator.js +280 -297
  54. package/.agent/scripts/security_scan.js +37 -64
  55. package/.agent/scripts/session_manager.js +270 -276
  56. package/.agent/scripts/skill_evolution.js +637 -644
  57. package/.agent/scripts/skill_integrator.js +307 -313
  58. package/.agent/scripts/strengthen_skills.js +193 -193
  59. package/.agent/scripts/strip_tribunal.js +47 -47
  60. package/.agent/scripts/swarm_dispatcher.js +360 -360
  61. package/.agent/scripts/test_runner.js +32 -39
  62. package/.agent/scripts/utils.js +10 -25
  63. package/.agent/scripts/verify_all.js +84 -92
  64. package/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +1 -1
  65. package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +1 -1
  66. package/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +1 -1
  67. package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +1 -1
  68. package/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +1 -1
  69. package/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +1 -1
  70. package/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +1 -1
  71. package/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +1 -1
  72. package/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +1 -1
  73. package/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +1 -1
  74. package/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +1 -1
  75. package/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +1 -1
  76. package/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +1 -1
  77. package/.agent/skills/doc.md +1 -1
  78. package/.agent/skills/knowledge-graph/SKILL.md +52 -52
  79. package/.agent/skills/ui-ux-pro-max/SKILL.md +562 -562
  80. package/.agent/workflows/generate.md +183 -183
  81. package/.agent/workflows/tribunal-speed.md +183 -183
  82. package/README.md +1 -1
  83. package/bin/tribunal-kit.js +76 -87
  84. package/package.json +6 -3
  85. package/scripts/changelog.js +167 -167
  86. package/scripts/sync-version.js +81 -81
  87. package/.agent/history/architecture-explorer.html +0 -352
  88. package/.agent/scripts/__pycache__/_colors.cpython-311.pyc +0 -0
  89. package/.agent/scripts/__pycache__/_utils.cpython-311.pyc +0 -0
  90. package/.agent/scripts/__pycache__/case_law_manager.cpython-311.pyc +0 -0
@@ -1,609 +1,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 = '\x1b[92m';
55
- const YELLOW = '\x1b[93m';
56
- const CYAN = '\x1b[96m';
57
- const BLUE = '\x1b[94m';
58
- const RED = '\x1b[91m';
59
- const BOLD = '\x1b[1m';
60
- const DIM = '\x1b[2m';
61
- const RESET = '\x1b[0m';
62
-
63
- // ── Domain → Skill affinity map ───────────────────────────────────────────────
64
- // Keywords in the user's task that strongly indicate specific skills.
65
- // Higher weight = stronger signal.
66
- const DOMAIN_AFFINITIES = [
67
- // Backend / API
68
- { keywords: ['api', 'rest', 'route', 'endpoint', 'handler', 'express', 'fastapi', 'hono'],
69
- skills: ['api-patterns', 'nodejs-best-practices', 'backend-specialist', 'error-resilience'], weight: 3 },
70
- // Database
71
- { keywords: ['sql', 'query', 'database', 'prisma', 'drizzle', 'orm', 'postgres', 'mysql', 'schema'],
72
- skills: ['database-design', 'sql-pro', 'supabase-postgres-best-practices'], weight: 3 },
73
- // Authentication
74
- { keywords: ['auth', 'jwt', 'login', 'oauth', 'session', 'password', 'token', 'rbac'],
75
- skills: ['authentication-best-practices', 'vulnerability-scanner', 'api-security-auditor'], weight: 3 },
76
- // React / Next.js
77
- { keywords: ['react', 'next', 'nextjs', 'component', 'hook', 'jsx', 'tsx', 'server component', 'server action'],
78
- skills: ['react-specialist', 'nextjs-react-expert', 'frontend-design'], weight: 3 },
79
- // Frontend / UI
80
- { keywords: ['ui', 'design', 'landing', 'page', 'layout', 'responsive', 'tailwind', 'css', 'style'],
81
- skills: ['frontend-design', 'ui-ux-pro-max', 'tailwind-patterns', 'web-design-guidelines'], weight: 2 },
82
- // Animation / Motion
83
- { keywords: ['animation', 'gsap', 'framer', 'motion', 'scroll', 'transition', 'parallax'],
84
- skills: ['motion-engineering', 'framer-motion-expert', 'gsap-core', 'gsap-scrolltrigger'], weight: 3 },
85
- // AI / LLM
86
- { keywords: ['llm', 'openai', 'anthropic', 'gemini', 'embedding', 'ai', 'rag', 'vector', 'chat', 'prompt'],
87
- skills: ['llm-engineering', 'ai-prompt-injection-defense', 'agentic-patterns'], weight: 3 },
88
- // Security
89
- { keywords: ['security', 'xss', 'injection', 'owasp', 'vulnerability', 'csrf', 'sanitize', 'audit'],
90
- skills: ['vulnerability-scanner', 'api-security-auditor', 'authentication-best-practices'], weight: 3 },
91
- // Testing
92
- { keywords: ['test', 'spec', 'jest', 'vitest', 'playwright', 'unit test', 'e2e', 'mock'],
93
- skills: ['testing-patterns', 'playwright-best-practices', 'tdd-workflow'], weight: 3 },
94
- // Performance
95
- { keywords: ['performance', 'optimize', 'bundle', 'cache', 'speed', 'slow', 'lighthouse', 'cwv'],
96
- skills: ['performance-profiling', 'motion-engineering', 'edge-computing'], weight: 2 },
97
- // Mobile
98
- { keywords: ['mobile', 'react native', 'expo', 'ios', 'android', 'gesture', 'haptic'],
99
- skills: ['building-native-ui', 'mobile-design', 'agentic-patterns'], weight: 3 },
100
- // DevOps / CI
101
- { keywords: ['docker', 'ci', 'cd', 'deploy', 'pipeline', 'k8s', 'kubernetes', 'github actions'],
102
- skills: ['devops-engineer', 'deployment-procedures', 'observability'], weight: 2 },
103
- // Real-time
104
- { keywords: ['realtime', 'websocket', 'sse', 'socket', 'live', 'multiplayer', 'collaborative'],
105
- skills: ['realtime-patterns', 'error-resilience'], weight: 3 },
106
- // TypeScript
107
- { keywords: ['typescript', 'type', 'generic', 'interface', 'satisfies', 'zod', 'pydantic'],
108
- skills: ['typescript-advanced', 'data-validation-schemas'], weight: 2 },
109
- // Python
110
- { keywords: ['python', 'fastapi', 'django', 'flask', 'pydantic', 'asyncio'],
111
- skills: ['python-pro', 'python-patterns'], weight: 3 },
112
- // Architecture
113
- { keywords: ['architecture', 'refactor', 'clean', 'solid', 'design pattern', 'monorepo', 'microservice'],
114
- skills: ['architecture', 'clean-code', 'monorepo-management'], weight: 2 },
115
- // C# / .NET
116
- { keywords: ['csharp', 'c#', 'dotnet', '.net', 'blazor', 'aspnet', 'entity framework'],
117
- skills: ['csharp-developer'], weight: 3 },
118
- ];
119
-
120
- // ── File extension → skill boost map ─────────────────────────────────────────
121
- const EXT_AFFINITIES = {
122
- '.tsx': ['react-specialist', 'nextjs-react-expert', 'typescript-advanced'],
123
- '.jsx': ['react-specialist', 'frontend-design'],
124
- '.ts': ['typescript-advanced', 'nodejs-best-practices'],
125
- '.vue': ['vue-expert'],
126
- '.py': ['python-pro', 'python-patterns'],
127
- '.cs': ['csharp-developer'],
128
- '.sql': ['sql-pro', 'database-design'],
129
- '.css': ['tailwind-patterns', 'frontend-design'],
130
- };
131
-
132
- // ── Core baseline skills — always available to all model sizes ────────────────
133
- // These are injected for every request at a condensed level.
134
- const BASELINE_SKILLS = [
135
- 'clean-code',
136
- 'systematic-debugging',
137
- 'error-resilience',
138
- ];
139
-
140
- // ── Skill catalogue (loaded from disk) ───────────────────────────────────────
141
-
142
- /**
143
- * Find the .agent directory by walking up from cwd.
144
- * @returns {string} path to .agent/
145
- */
146
- function findAgentDir() {
147
- let current = path.resolve(process.cwd());
148
- const root = path.parse(current).root;
149
- while (current !== root) {
150
- const candidate = path.join(current, '.agent');
151
- if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) return candidate;
152
- current = path.dirname(current);
153
- }
154
- console.error(`${RED} .agent/ not found. Run: npx tribunal-kit init${RESET}`);
155
- process.exit(1);
156
- }
157
-
158
- /**
159
- * Parse the YAML frontmatter from a SKILL.md file.
160
- * Returns { name, description, ... } or null on parse failure.
161
- * @param {string} content - Full SKILL.md file text
162
- */
163
- function parseFrontmatter(content) {
164
- const match = content.match(/^---\n([\s\S]*?)\n---/);
165
- if (!match) return null;
166
- const yaml = match[1];
167
- const obj = {};
168
- for (const line of yaml.split('\n')) {
169
- const sep = line.indexOf(':');
170
- if (sep === -1) continue;
171
- const key = line.slice(0, sep).trim();
172
- const val = line.slice(sep + 1).trim().replace(/^["']|["']$/g, '');
173
- obj[key] = val;
174
- }
175
- return obj;
176
- }
177
-
178
- /**
179
- * Extract the "key rules" section from a SKILL.md for condensed Level-1 context.
180
- * Falls back to first 800 chars of content if no section found.
181
- * @param {string} content
182
- */
183
- function extractKeyRules(content) {
184
- // Try to find sections named: Key Rules, Rules, Core Rules, Critical Rules, Guardrails
185
- const sectionMatch = content.match(
186
- /##\s+(?:Key Rules?|Core Rules?|Critical Rules?|Guardrails?|Rules?)\n([\s\S]*?)(?=\n##\s|$)/i
187
- );
188
- if (sectionMatch) return sectionMatch[1].trim().slice(0, 1200);
189
- // Fallback: strip frontmatter and take first 800 chars
190
- const bodyStart = content.indexOf('---', 3);
191
- const body = bodyStart !== -1 ? content.slice(bodyStart + 3).trim() : content;
192
- return body.slice(0, 800).trim();
193
- }
194
-
195
- /**
196
- * Load all skills from .agent/skills/ directory.
197
- * Returns an array of { name, file, frontmatter, content, keyRules } objects.
198
- * @param {string} agentDir
199
- */
200
- function loadSkills(agentDir) {
201
- const skillsDir = path.join(agentDir, 'skills');
202
- if (!fs.existsSync(skillsDir)) return [];
203
-
204
- const skills = [];
205
- const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
206
-
207
- for (const entry of entries) {
208
- if (!entry.isDirectory()) continue;
209
- const skillFile = path.join(skillsDir, entry.name, 'SKILL.md');
210
- if (!fs.existsSync(skillFile)) continue;
211
-
212
- try {
213
- const content = fs.readFileSync(skillFile, 'utf8');
214
- const frontmatter = parseFrontmatter(content) || {};
215
- const keyRules = extractKeyRules(content);
216
- skills.push({
217
- name: entry.name,
218
- file: skillFile,
219
- frontmatter,
220
- content,
221
- keyRules,
222
- description: frontmatter.description || '',
223
- });
224
- } catch {
225
- // Skip unreadable skills silently
226
- }
227
- }
228
- return skills;
229
- }
230
-
231
- // ── Scoring engine ────────────────────────────────────────────────────────────
232
-
233
- /**
234
- * Tokenize text into lowercase words (3+ chars).
235
- * @param {string} text
236
- * @returns {string[]}
237
- */
238
- function tokenize(text) {
239
- return (text.match(/\b[a-zA-Z_][a-zA-Z0-9_]{2,}\b/g) || []).map(t => t.toLowerCase());
240
- }
241
-
242
- /**
243
- * Compute a relevance score for a skill against the user's task.
244
- *
245
- * Scoring breakdown:
246
- * - Text overlap between task tokens and skill description/name: up to +5
247
- * - Domain affinity keyword match: +weight (2 or 3) per match
248
- * - File extension affinity: +2 per match
249
- * - Baseline skill bonus: +1 (always present)
250
- *
251
- * @param {{ name, description, content }} skill
252
- * @param {string} task - Raw task text from the user
253
- * @param {string[]} fileExts - File extensions being touched
254
- * @param {string[]} taskTokens - Pre-tokenized task
255
- * @returns {number} score
256
- */
257
- function scoreSkill(skill, task, fileExts, taskTokens) {
258
- let score = 0;
259
- const taskLower = task.toLowerCase();
260
- const skillText = (skill.name + ' ' + skill.description).toLowerCase();
261
- const skillTokens = tokenize(skillText);
262
-
263
- // 1. Token overlap (lightweight TF match no IDF needed at this scale)
264
- const skillSet = new Set(skillTokens);
265
- for (const token of taskTokens) {
266
- if (skillSet.has(token)) score += 1;
267
- }
268
-
269
- // 2. Domain affinity boost
270
- for (const affinity of DOMAIN_AFFINITIES) {
271
- const keywordMatch = affinity.keywords.some(k => taskLower.includes(k));
272
- if (!keywordMatch) continue;
273
- if (affinity.skills.includes(skill.name)) {
274
- score += affinity.weight;
275
- }
276
- }
277
-
278
- // 3. File extension boost
279
- for (const ext of fileExts) {
280
- const extSkills = EXT_AFFINITIES[ext] || [];
281
- if (extSkills.includes(skill.name)) score += 2;
282
- }
283
-
284
- // 4. Baseline skill safety net
285
- if (BASELINE_SKILLS.includes(skill.name)) score += 1;
286
-
287
- return score;
288
- }
289
-
290
- /**
291
- * Run the tiered selection algorithm.
292
- *
293
- * @param {string} task - Raw user task description
294
- * @param {string[]} files - Affected filenames (for ext detection)
295
- * @param {string} model - 'large' | 'small' | 'auto'
296
- * @param {object[]} skills - Loaded skills array from loadSkills()
297
- * @returns {{ essential: object[], supplementary: object[], available: object[], scores: Map }}
298
- */
299
- function selectSkills(task, files, model, skills) {
300
- const taskTokens = tokenize(task);
301
- const fileExts = files.map(f => path.extname(f).toLowerCase()).filter(Boolean);
302
-
303
- // Score every available skill
304
- const scored = skills.map(skill => ({
305
- ...skill,
306
- score: scoreSkill(skill, task, fileExts, taskTokens),
307
- }));
308
- scored.sort((a, b) => b.score - a.score);
309
-
310
- // Determine tier thresholds
311
- const maxScore = scored[0]?.score || 1;
312
- const tier0Cut = Math.max(maxScore * 0.65, 2); // Essential: top 65%+ of max score
313
- const tier1Cut = Math.max(maxScore * 0.3, 1); // Supplementary: 30–65%
314
-
315
- const essential = scored.filter(s => s.score >= tier0Cut).slice(0, 10);
316
- const supplementary = scored.filter(s => s.score < tier0Cut && s.score >= tier1Cut).slice(0, 8);
317
- const available = scored.filter(s => s.score < tier1Cut && s.score > 0).slice(0, 10);
318
-
319
- // Ensure baseline skills always appear at minimum in supplementary
320
- for (const base of BASELINE_SKILLS) {
321
- const inEssential = essential.find(s => s.name === base);
322
- const inSupplementary = supplementary.find(s => s.name === base);
323
- if (!inEssential && !inSupplementary) {
324
- const baseSkill = skills.find(s => s.name === base);
325
- if (baseSkill) supplementary.push({ ...baseSkill, score: 0.5 });
326
- }
327
- }
328
-
329
- // For small models: collapse supplementary into available
330
- if (model === 'small') {
331
- return {
332
- essential: essential.slice(0, 6),
333
- supplementary: [],
334
- available: [...supplementary, ...available].slice(0, 8),
335
- scores: buildScoreMap(scored),
336
- };
337
- }
338
-
339
- return { essential, supplementary, available, scores: buildScoreMap(scored) };
340
- }
341
-
342
- function buildScoreMap(scored) {
343
- const m = new Map();
344
- for (const s of scored) m.set(s.name, s.score);
345
- return m;
346
- }
347
-
348
- // ── Output formatters ─────────────────────────────────────────────────────────
349
-
350
- function formatReport(task, model, selection, elapsed) {
351
- const { essential, supplementary, available } = selection;
352
- const total = essential.length + supplementary.length + available.length;
353
-
354
- console.log(`\n${BOLD}${CYAN}━━━ Context BrokerSkill Selection ━━━━━━━━━━━━━━━${RESET}`);
355
- console.log(` Task : ${BOLD}${task.slice(0, 80)}${task.length > 80 ? '...' : ''}${RESET}`);
356
- console.log(` Model : ${model === 'large' ? GREEN : YELLOW}${model}${RESET} ${DIM}(Focus without Compromise)${RESET}`);
357
- console.log(` Skills : ${GREEN}${essential.length} essential${RESET} + ${YELLOW}${supplementary.length} supplementary${RESET} + ${DIM}${available.length} available${RESET} of ${total} matched`);
358
- console.log(` Time : ${elapsed}ms\n`);
359
-
360
- if (essential.length) {
361
- console.log(` ${GREEN}${BOLD}▶ Level 0 — Essential (Full Context, Top Priority):${RESET}`);
362
- for (const s of essential) {
363
- const score = selection.scores.get(s.name) || 0;
364
- console.log(` ${GREEN}✦${RESET} ${BOLD}${s.name}${RESET} ${DIM}score=${score.toFixed(1)}${RESET}`);
365
- if (s.description) console.log(` ${DIM}${s.description.slice(0, 90)}${RESET}`);
366
- }
367
- }
368
-
369
- if (supplementary.length) {
370
- console.log(`\n ${YELLOW}${BOLD}▶ Level 1 — Supplementary (Key Rules Only):${RESET}`);
371
- for (const s of supplementary) {
372
- const score = selection.scores.get(s.name) || 0;
373
- console.log(` ${YELLOW}◆${RESET} ${s.name} ${DIM}score=${score.toFixed(1)}${RESET}`);
374
- }
375
- }
376
-
377
- if (available.length) {
378
- console.log(`\n ${DIM}▶ Level 2 — Available (Name Reference Only):${RESET}`);
379
- console.log(` ${DIM}${available.map(s => s.name).join(', ')}${RESET}`);
380
- }
381
-
382
- if (model === 'small') {
383
- console.log(`\n ${YELLOW}⚡ Small model mode: supplementary collapsed. Essential only injected.${RESET}`);
384
- }
385
-
386
- console.log(`\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n`);
387
- }
388
-
389
- function formatJson(task, model, selection) {
390
- return JSON.stringify({
391
- task,
392
- model,
393
- timestamp: new Date().toISOString(),
394
- essential: selection.essential.map(s => ({ name: s.name, score: selection.scores.get(s.name), description: s.description })),
395
- supplementary: selection.supplementary.map(s => ({ name: s.name, score: selection.scores.get(s.name) })),
396
- available: selection.available.map(s => s.name),
397
- }, null, 2);
398
- }
399
-
400
- function formatNames(selection, model) {
401
- const all = model === 'large'
402
- ? [...selection.essential, ...selection.supplementary]
403
- : selection.essential;
404
- return all.map(s => s.name).join('\n');
405
- }
406
-
407
- /**
408
- * Build a full LLM-ready context prompt string.
409
- * This is the full output to be injected into an AI system prompt.
410
- *
411
- * For large models: Essential = full SKILL.md, Supplementary = key rules section.
412
- * For small models: Essential = key rules section only.
413
- *
414
- * @param {string} task
415
- * @param {string} model
416
- * @param {object} selection
417
- * @returns {string}
418
- */
419
- function formatPrompt(task, model, selection) {
420
- const lines = [
421
- `# Tribunal Context Broker Injected Skills`,
422
- `# Task: ${task}`,
423
- `# Model tier: ${model}`,
424
- `# Generated: ${new Date().toISOString()}`,
425
- '',
426
- '## Instructions for the AI',
427
- 'The following skills are ordered by relevance to the current task.',
428
- 'Level 0 skills contain full rule sets. Level 1 skills contain key rules only.',
429
- 'Treat ALL injected rules as mandatory constraints, not suggestions.',
430
- '',
431
- '---',
432
- '',
433
- ];
434
-
435
- if (selection.essential.length) {
436
- lines.push('## Level 0 — Essential Skills (Full Context)');
437
- lines.push('');
438
- for (const s of selection.essential) {
439
- lines.push(`### Skill: ${s.name}`);
440
- lines.push('');
441
- if (model === 'large') {
442
- lines.push(s.content || s.keyRules);
443
- } else {
444
- lines.push(s.keyRules);
445
- }
446
- lines.push('');
447
- lines.push('---');
448
- lines.push('');
449
- }
450
- }
451
-
452
- if (model === 'large' && selection.supplementary.length) {
453
- lines.push('## Level 1 — Supplementary Skills (Key Rules)');
454
- lines.push('');
455
- for (const s of selection.supplementary) {
456
- lines.push(`### Skill: ${s.name} (condensed)`);
457
- lines.push('');
458
- lines.push(s.keyRules);
459
- lines.push('');
460
- lines.push('---');
461
- lines.push('');
462
- }
463
- }
464
-
465
- if (selection.available.length) {
466
- lines.push('## Level 2 — Available Skills (Reference Names Only)');
467
- lines.push('');
468
- lines.push('The following skills are relevant but not injected to maintain context density.');
469
- lines.push('Request their full content if needed: ' + selection.available.map(s => s.name).join(', '));
470
- lines.push('');
471
- }
472
-
473
- return lines.join('\n');
474
- }
475
-
476
- // ── Built-in demo ─────────────────────────────────────────────────────────────
477
-
478
- function runDemo(agentDir) {
479
- const skills = loadSkills(agentDir);
480
-
481
- const scenarios = [
482
- { task: 'Build a JWT authentication API with Express.js and Zod validation', file: 'auth.ts', model: 'large' },
483
- { task: 'Design a premium landing page with GSAP scroll animations', file: 'Hero.tsx', model: 'large' },
484
- { task: 'Write a Prisma query for paginated user orders', file: 'orders.ts', model: 'small' },
485
- { task: 'Add RAG pipeline to an OpenAI-powered chat interface', file: 'chat.ts', model: 'large' },
486
- ];
487
-
488
- console.log(`\n${BOLD}${CYAN}━━━ Context Broker — Demo Mode ━━━━━━━━━━━━━━━━━━━━━${RESET}`);
489
- console.log(` Loaded ${skills.length} skills from .agent/skills/\n`);
490
-
491
- for (const scenario of scenarios) {
492
- const t0 = Date.now();
493
- const model = scenario.model;
494
- const selection = selectSkills(scenario.task, [scenario.file], model, skills);
495
- const elapsed = Date.now() - t0;
496
-
497
- console.log(`\n ${BOLD}Task: "${scenario.task.slice(0, 70)}"${RESET} ${DIM}[${model} model]${RESET}`);
498
- console.log(` Essential : ${GREEN}${selection.essential.map(s => s.name).join(', ')}${RESET}`);
499
- console.log(` Supplementary : ${YELLOW}${selection.supplementary.map(s => s.name).join(', ') || '(small mode)'}${RESET}`);
500
- console.log(` Time : ${DIM}${elapsed}ms${RESET}`);
501
- }
502
-
503
- console.log(`\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n`);
504
- }
505
-
506
- // ── Public API ────────────────────────────────────────────────────────────────
507
-
508
- /**
509
- * Programmatic API for use by other Tribunal scripts.
510
- *
511
- * @param {string} task - User task description
512
- * @param {string[]} files - Files being touched
513
- * @param {string} model - 'large' | 'small' | 'auto'
514
- * @param {string} agentDir - Path to .agent/ directory
515
- * @returns {{ essential, supplementary, available, promptText }}
516
- */
517
- function broker(task, files = [], model = 'large', agentDir = null) {
518
- const resolvedAgentDir = agentDir || findAgentDir();
519
- const skills = loadSkills(resolvedAgentDir);
520
- const selection = selectSkills(task, files, model, skills);
521
- const promptText = formatPrompt(task, model, selection);
522
- return { ...selection, promptText };
523
- }
524
-
525
- module.exports = { broker, selectSkills, loadSkills, scoreSkill, findAgentDir };
526
-
527
- // ── CLI Entry ─────────────────────────────────────────────────────────────────
528
-
529
- if (require.main === module) {
530
- const argv = process.argv.slice(2);
531
-
532
- if (!argv.length || argv.includes('--help') || argv.includes('-h')) {
533
- console.log(`
534
- ${BOLD}context_broker.js${RESET} Tribunal Focus-without-Compromise Context Engine
535
-
536
- ${BOLD}Usage:${RESET}
537
- node .agent/scripts/context_broker.js --task "<description>" [options]
538
- node .agent/scripts/context_broker.js demo
539
-
540
- ${BOLD}Options:${RESET}
541
- --task <text> Task description to match against skill catalogue
542
- --file <path> File being touched (repeat for multiple files)
543
- --model <size> large (default) | small — model tier
544
- --output <format> report (default) | json | names | prompt
545
-
546
- ${BOLD}Model tiers:${RESET}
547
- large Full Essential + condensed Supplementary (Claude Opus, Gemini 2.5 Pro, GPT-4o)
548
- small Essential only (Gemini Flash, GPT-4o-mini)
549
-
550
- ${BOLD}Examples:${RESET}
551
- node .agent/scripts/context_broker.js --task "JWT auth API" --model large
552
- node .agent/scripts/context_broker.js --task "landing page" --file Hero.tsx --output names
553
- node .agent/scripts/context_broker.js --task "RAG pipeline" --output prompt > context.md
554
- node .agent/scripts/context_broker.js demo
555
- `);
556
- process.exit(0);
557
- }
558
-
559
- const agentDir = findAgentDir();
560
-
561
- if (argv[0] === 'demo') {
562
- runDemo(agentDir);
563
- process.exit(0);
564
- }
565
-
566
- // Parse args
567
- const taskIdx = argv.indexOf('--task');
568
- const modelIdx = argv.indexOf('--model');
569
- const outputIdx = argv.indexOf('--output');
570
-
571
- const task = taskIdx !== -1 && argv[taskIdx + 1] ? argv[taskIdx + 1] : '';
572
- const model = modelIdx !== -1 && argv[modelIdx + 1] ? argv[modelIdx + 1] : 'large';
573
- const output = outputIdx !== -1 && argv[outputIdx + 1] ? argv[outputIdx + 1] : 'report';
574
-
575
- if (!task) {
576
- console.error(`${RED}✖ --task is required${RESET}`);
577
- process.exit(1);
578
- }
579
-
580
- // Collect --file arguments (may appear multiple times)
581
- const files = [];
582
- for (let i = 0; i < argv.length; i++) {
583
- if (argv[i] === '--file' && argv[i + 1]) files.push(argv[i + 1]);
584
- }
585
-
586
- const t0 = Date.now();
587
- const skills = loadSkills(agentDir);
588
- const selection = selectSkills(task, files, model, skills);
589
- const elapsed = Date.now() - t0;
590
-
591
- if (!['large', 'small'].includes(model)) {
592
- console.error(`${YELLOW}⚠ Unknown model tier "${model}" — defaulting to "large"${RESET}`);
593
- }
594
-
595
- switch (output) {
596
- case 'json':
597
- console.log(formatJson(task, model, selection));
598
- break;
599
- case 'names':
600
- console.log(formatNames(selection, model));
601
- break;
602
- case 'prompt':
603
- console.log(formatPrompt(task, model, selection));
604
- break;
605
- default: // 'report'
606
- formatReport(task, model, selection, elapsed);
607
- break;
608
- }
609
- }
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 0Essential (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
+ }