tribunal-kit 4.4.0 → 4.4.1

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