analyzthis_design 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/HOW-TO-USE.md +2 -1
  2. package/README.md +39 -7
  3. package/dist/HOW-TO-USE.md +2 -1
  4. package/dist/README.md +39 -7
  5. package/dist/bin/cli.js +22 -4
  6. package/dist/lib/chunk-run.js +2 -0
  7. package/dist/lib/install.js +53 -0
  8. package/dist/lib/knowledge.js +49 -18
  9. package/dist/lib/mcp-server.js +206 -75
  10. package/dist/lib/orchestrator/run.js +5 -0
  11. package/dist/lib/session.js +33 -2
  12. package/dist/skills/anuj/SKILL.md +7 -0
  13. package/dist/skills/arjun/SKILL.md +5 -263
  14. package/dist/skills/arjun/references/lens.md +265 -0
  15. package/dist/skills/kavi/SKILL.md +7 -0
  16. package/dist/skills/knowledge-bank/SKILL.md +14 -116770
  17. package/dist/skills/meera/SKILL.md +7 -0
  18. package/dist/skills/noor/SKILL.md +7 -0
  19. package/dist/skills/persona-orchestrator/SKILL.md +5 -195
  20. package/dist/skills/persona-orchestrator/references/lens.md +197 -0
  21. package/dist/skills/priya/SKILL.md +7 -0
  22. package/dist/skills/raj/SKILL.md +7 -0
  23. package/dist/skills/receipt/SKILL.md +17 -0
  24. package/dist/skills/ux-story-gate/SKILL.md +5 -338
  25. package/dist/skills/ux-story-gate/references/lens.md +340 -0
  26. package/dist/skills/zara/SKILL.md +7 -0
  27. package/package.json +1 -1
  28. package/skills/anuj/SKILL.md +7 -0
  29. package/skills/arjun/SKILL.md +5 -263
  30. package/skills/arjun/references/lens.md +265 -0
  31. package/skills/kavi/SKILL.md +7 -0
  32. package/skills/knowledge-bank/SKILL.md +14 -116770
  33. package/skills/meera/SKILL.md +7 -0
  34. package/skills/noor/SKILL.md +7 -0
  35. package/skills/persona-orchestrator/SKILL.md +5 -195
  36. package/skills/persona-orchestrator/references/lens.md +197 -0
  37. package/skills/priya/SKILL.md +7 -0
  38. package/skills/raj/SKILL.md +7 -0
  39. package/skills/receipt/SKILL.md +17 -0
  40. package/skills/ux-story-gate/SKILL.md +5 -338
  41. package/skills/ux-story-gate/references/lens.md +340 -0
  42. package/skills/zara/SKILL.md +7 -0
@@ -248,16 +248,18 @@ var TOOLS = [
248
248
  },
249
249
  {
250
250
  name: 'analyzthis_retrieve',
251
- description: 'Retrieve CSV reference data. Query the 28-file reference library (colors, styles, typography, UX guidelines, charts, products, landing patterns, 16 stack guides). Returns filtered, citation-ready rows.',
251
+ description: 'Retrieve on demand: CSV rows, a skill reference file, or one knowledge-bank persona slice. Do not load the full bank.',
252
252
  inputSchema: {
253
253
  type: 'object',
254
254
  properties: {
255
- file: { type: 'string', description: 'CSV filename (e.g., "styles.csv", "colors.csv", "stacks/nextjs.csv")' },
256
- column: { type: 'string', description: 'Column to filter on (e.g., "Product Type", "Category", "Best For Tags")' },
257
- keywords: { type: 'string', description: 'Comma-separated keywords to match (e.g., "saas,dashboard")' },
258
- limit: { type: 'number', description: 'Max rows to return (default: 3)' },
255
+ kind: { type: 'string', enum: ['csv', 'skill', 'knowledge'], description: 'csv (default), skill (references/*.md), or knowledge (one persona slice)' },
256
+ file: { type: 'string', description: 'CSV filename, or skill id (e.g. arjun), or knowledge/<persona>' },
257
+ column: { type: 'string', description: 'CSV column to filter on' },
258
+ keywords: { type: 'string', description: 'Comma-separated CSV keywords' },
259
+ limit: { type: 'number', description: 'Max CSV rows (default: 3)' },
260
+ persona: { type: 'string', description: 'Persona id for knowledge slice (arjun, zara, …)' },
261
+ ref: { type: 'string', description: 'Skill reference filename without path (e.g. lens.md)' },
259
262
  },
260
- required: ['file'],
261
263
  },
262
264
  },
263
265
  {
@@ -285,6 +287,43 @@ var TOOLS = [
285
287
  },
286
288
  ];
287
289
 
290
+ var ROUTER_TOOL = {
291
+ name: 'analyzthis_design',
292
+ description: 'Design team router. Set who to zara, arjun, noor, anuj, meera, priya, raj, kavi, orchestrator, ux-ideator, design-critic, ux-story-gate, design-director, or mood-board. Returns a short skill front — call analyzthis_retrieve for the rest.',
293
+ inputSchema: {
294
+ type: 'object',
295
+ properties: {
296
+ who: { type: 'string', description: 'Persona or pass name (e.g. zara, arjun, orchestrator)' },
297
+ task: { type: 'string', description: 'What to design or critique' },
298
+ context: { type: 'string', description: 'Optional extra context' },
299
+ },
300
+ required: ['who', 'task'],
301
+ },
302
+ };
303
+
304
+ var RECEIPT_TOOL = {
305
+ name: 'analyzthis_receipt',
306
+ description: 'Show inferred token receipt for this project. Not a bill. Do not invent a dollar figure.',
307
+ inputSchema: {
308
+ type: 'object',
309
+ properties: {
310
+ project: { type: 'string', description: 'Optional project id override' },
311
+ },
312
+ },
313
+ };
314
+
315
+ function resolveCatalog(explicit) {
316
+ var raw = explicit || process.env.ANALYZTHIS_MCP_CATALOG || 'lite';
317
+ return String(raw).toLowerCase() === 'full' ? 'full' : 'lite';
318
+ }
319
+
320
+ function listedTools(catalog) {
321
+ if (resolveCatalog(catalog) === 'full') {
322
+ return TOOLS.concat([ROUTER_TOOL, RECEIPT_TOOL]);
323
+ }
324
+ return [ROUTER_TOOL, TOOLS.find(function (t) { return t.name === 'analyzthis_retrieve'; }), TOOLS.find(function (t) { return t.name === 'analyzthis_session'; }), RECEIPT_TOOL].filter(Boolean);
325
+ }
326
+
288
327
  // ── Tool handlers ────────────────────────────────────────────────────────
289
328
 
290
329
  function handleToolCall(name, args) {
@@ -326,6 +365,10 @@ function handleToolCall(name, args) {
326
365
  return handleSession(args);
327
366
  case 'analyzthis_system_prompt':
328
367
  return handleSystemPrompt(args);
368
+ case 'analyzthis_design':
369
+ return handleRouter(args);
370
+ case 'analyzthis_receipt':
371
+ return handleReceipt(args);
329
372
 
330
373
  default:
331
374
  return { error: 'Unknown tool: ' + name };
@@ -333,7 +376,7 @@ function handleToolCall(name, args) {
333
376
  }
334
377
 
335
378
  function handleSinglePersona(personaId, args) {
336
- var card = readCard(personaId);
379
+ var card = readSkillFront(personaId);
337
380
  if (!card) return { error: 'Persona not found: ' + personaId };
338
381
 
339
382
  var task = args.task || '';
@@ -351,11 +394,10 @@ function handleSinglePersona(personaId, args) {
351
394
  '## Task\n' + task + '\n' +
352
395
  (context ? '\n## Context\n' + context : '') +
353
396
  extra + '\n\n' +
354
- 'Produce your output following the schema in your card above. ' +
355
- 'Be specific — cite components, zones, and fixes. ' +
356
- 'Do not give generic advice.';
397
+ 'Use the lite output contract. Call analyzthis_retrieve (kind=skill, file=' + personaId + ') only if you need the full lens. ' +
398
+ 'Be specific — cite components, zones, and fixes. Do not give generic advice.';
357
399
 
358
- return { prompt: prompt, persona: personaId, task: task };
400
+ return { prompt: prompt, persona: personaId, task: task, verdicts: 1 };
359
401
  }
360
402
 
361
403
  function handleCombinationPass(skillId, args) {
@@ -381,11 +423,11 @@ function handleCombinationPass(skillId, args) {
381
423
  skill + '\n\n' +
382
424
  '## Task\n' + task + '\n' +
383
425
  (context ? '\n## Context\n' + context : '') + '\n\n' +
384
- 'Run all phases sequentially. Challenge the premise first. ' +
385
- 'Resolve any persona disagreements explicitly pick a winner with a forward path. ' +
386
- 'Produce the final output in the format specified by the skill.';
426
+ 'Run the smallest useful team. Challenge the premise first. ' +
427
+ 'Call analyzthis_retrieve (kind=skill, file=' + skillId + ') if the front is not enough. ' +
428
+ 'Lite default: verdict + top 3 + one evidence line. Expand only if the user says expand.';
387
429
 
388
- return { prompt: prompt, skill: skillId, task: task };
430
+ return { prompt: prompt, skill: skillId, task: task, persona: skillId, verdicts: 1 };
389
431
  }
390
432
 
391
433
  function handleMoodBoard(args) {
@@ -439,10 +481,13 @@ function handleCollect(args) {
439
481
  }
440
482
 
441
483
  function handleRetrieve(args) {
484
+ var kind = args.kind || 'csv';
485
+ if (kind === 'skill') return handleRetrieveSkill(args);
486
+ if (kind === 'knowledge') return handleRetrieveKnowledge(args);
442
487
  try {
443
488
  var retrieve = require('./retrieve');
444
489
  var file = args.file;
445
- if (!file) return { error: 'file is required' };
490
+ if (!file) return { error: 'file is required for CSV retrieve' };
446
491
  var filters = [];
447
492
  if (args.column && args.keywords) {
448
493
  filters.push({ column: args.column, anyOf: args.keywords.split(',').map(function(s) { return s.trim(); }) });
@@ -464,6 +509,70 @@ function handleRetrieve(args) {
464
509
  }
465
510
  }
466
511
 
512
+ function handleRetrieveSkill(args) {
513
+ var id = String(args.file || args.persona || '').replace(/^skill\//, '');
514
+ if (!id) return { error: 'file (skill id) is required' };
515
+ var refName = args.ref || 'lens.md';
516
+ var refPath = path.join(PACKAGE_ROOT, 'skills', id, 'references', refName);
517
+ if (!fs.existsSync(refPath)) {
518
+ var dir = path.join(PACKAGE_ROOT, 'skills', id, 'references');
519
+ if (fs.existsSync(dir)) {
520
+ var names = fs.readdirSync(dir).filter(function (n) { return n.endsWith('.md'); });
521
+ if (names.length) refPath = path.join(dir, names[0]);
522
+ }
523
+ }
524
+ if (!fs.existsSync(refPath)) {
525
+ var full = readSkill(id);
526
+ if (!full) return { error: 'No skill references for ' + id };
527
+ return { result: full, persona: id };
528
+ }
529
+ return { result: fs.readFileSync(refPath, 'utf8'), persona: id };
530
+ }
531
+
532
+ function handleRetrieveKnowledge(args) {
533
+ var persona = args.persona || String(args.file || '').replace(/^knowledge\//, '');
534
+ if (!persona) return { error: 'persona is required for knowledge retrieve' };
535
+ try {
536
+ var knowledge = require('./knowledge');
537
+ var sessionMod = require('./session');
538
+ var projectId = sessionMod.getProjectId();
539
+ var slicePath = path.join(knowledge.KNOWLEDGE_SLICE_ROOT, projectId, persona + '.md');
540
+ if (!fs.existsSync(slicePath)) {
541
+ return { result: 'No knowledge slice for ' + persona + '. Run collect/sync first. Do not load a full bank.' };
542
+ }
543
+ var text = fs.readFileSync(slicePath, 'utf8');
544
+ var cap = 6000;
545
+ if (text.length > cap) text = text.slice(0, cap) + '\n…[truncated — this is one slice, not the full bank]';
546
+ return { result: text, persona: persona };
547
+ } catch (e) {
548
+ return { error: e.message };
549
+ }
550
+ }
551
+
552
+ var ROUTER_WHO = {
553
+ zara: 'persona', arjun: 'persona', meera: 'persona', priya: 'persona',
554
+ noor: 'persona', anuj: 'persona', raj: 'persona', kavi: 'persona',
555
+ orchestrator: 'persona-orchestrator', 'persona-orchestrator': 'persona-orchestrator',
556
+ 'ux-ideator': 'ux-ideator', 'design-critic': 'design-critic',
557
+ 'ux-story-gate': 'ux-story-gate', 'design-director': 'design-director',
558
+ 'mood-board': 'mood-board',
559
+ };
560
+
561
+ function handleRouter(args) {
562
+ var who = String(args.who || '').toLowerCase().replace(/^analyzthis_/, '').replace(/_/g, '-');
563
+ if (!who) return { error: 'who is required (e.g. zara, arjun, orchestrator)' };
564
+ var mapped = ROUTER_WHO[who];
565
+ if (!mapped) return { error: 'Unknown who: ' + who + '. Use a persona or orchestrator / ux-ideator / design-critic / ux-story-gate / design-director / mood-board.' };
566
+ if (mapped === 'persona') return handleSinglePersona(who, args);
567
+ if (mapped === 'mood-board') return handleMoodBoard(args);
568
+ return handleCombinationPass(mapped, args);
569
+ }
570
+
571
+ function handleReceipt(args) {
572
+ var receipt = require('./receipt');
573
+ return { result: receipt.report({ project: args.project }) };
574
+ }
575
+
467
576
  function handleSession(args) {
468
577
  try {
469
578
  var session = require('./session');
@@ -506,84 +615,86 @@ function handleSystemPrompt(args) {
506
615
 
507
616
  // ── Helpers ──
508
617
 
509
- function readCard(id) {
510
- var p = path.join(PACKAGE_ROOT, 'agents', 'cards', id + '.md');
511
- if (!fs.existsSync(p)) return null;
512
- return fs.readFileSync(p, 'utf8');
513
- }
514
-
515
618
  function readSkill(id) {
516
619
  var p = path.join(PACKAGE_ROOT, 'skills', id, 'SKILL.md');
517
620
  if (!fs.existsSync(p)) return null;
518
621
  return fs.readFileSync(p, 'utf8');
519
622
  }
520
623
 
624
+ function readSkillFront(id) {
625
+ return readSkill(id);
626
+ }
627
+
521
628
  // ── MCP config writer ──
522
629
 
523
- function writeMcpConfig(target) {
524
- var serverCmd = 'npx';
525
- var serverArgs = ['analyzthis_design', 'mcp'];
630
+ function mergeMcpServer(configPath) {
631
+ var dir = path.dirname(configPath);
632
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
633
+ var config = {};
634
+ if (fs.existsSync(configPath)) {
635
+ try { config = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch (e) {}
636
+ }
637
+ if (typeof config !== 'object' || config === null || Array.isArray(config)) config = {};
638
+ config.mcpServers = config.mcpServers || {};
639
+ config.mcpServers['analyzthis_design'] = {
640
+ command: 'npx',
641
+ args: ['analyzthis_design', 'mcp'],
642
+ env: { ANALYZTHIS_MCP_CATALOG: process.env.ANALYZTHIS_MCP_CATALOG || 'lite' },
643
+ };
644
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
645
+ return configPath;
646
+ }
647
+
648
+ function looksLikeProjectCwd() {
649
+ var cwd = process.cwd();
650
+ return fs.existsSync(path.join(cwd, 'package.json')) || fs.existsSync(path.join(cwd, '.git'));
651
+ }
652
+
653
+ function writeMcpConfig(target, opts) {
654
+ opts = opts || {};
655
+ if (opts.catalog) process.env.ANALYZTHIS_MCP_CATALOG = opts.catalog;
526
656
  var home = os.homedir();
527
657
 
528
658
  if (target === 'cursor') {
529
659
  var cursorConfigPath = path.join(home, '.cursor', 'mcp.json');
530
- var cursorDir = path.dirname(cursorConfigPath);
531
- if (!fs.existsSync(cursorDir)) fs.mkdirSync(cursorDir, { recursive: true });
532
- var cursorConfig = {};
533
- if (fs.existsSync(cursorConfigPath)) {
534
- try { cursorConfig = JSON.parse(fs.readFileSync(cursorConfigPath, 'utf8')); } catch (e) {}
535
- }
536
- cursorConfig.mcpServers = cursorConfig.mcpServers || {};
537
- cursorConfig.mcpServers['analyzthis_design'] = {
538
- command: serverCmd,
539
- args: serverArgs,
540
- };
541
- fs.writeFileSync(cursorConfigPath, JSON.stringify(cursorConfig, null, 2));
660
+ mergeMcpServer(cursorConfigPath);
542
661
  console.log('Cursor MCP config written to ' + cursorConfigPath);
662
+ console.log('Default MCP catalog is lite (router + retrieve + session + receipt).');
663
+ console.log('Full 19-tool list: ANALYZTHIS_MCP_CATALOG=full or mcp --catalog full');
543
664
  console.log('Restart Cursor to activate.\n');
544
665
  return true;
545
666
  }
546
667
 
547
668
  if (target === 'claude') {
548
- var claudeConfigPath;
669
+ var claudeDesktopPath;
549
670
  if (process.platform === 'darwin') {
550
- claudeConfigPath = path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
671
+ claudeDesktopPath = path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
551
672
  } else if (process.platform === 'win32') {
552
- claudeConfigPath = path.join(home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json');
673
+ claudeDesktopPath = path.join(home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json');
553
674
  } else {
554
- claudeConfigPath = path.join(home, '.config', 'Claude', 'claude_desktop_config.json');
675
+ claudeDesktopPath = path.join(home, '.config', 'Claude', 'claude_desktop_config.json');
555
676
  }
556
- var claudeDir = path.dirname(claudeConfigPath);
557
- if (!fs.existsSync(claudeDir)) fs.mkdirSync(claudeDir, { recursive: true });
558
- var claudeConfig = {};
559
- if (fs.existsSync(claudeConfigPath)) {
560
- try { claudeConfig = JSON.parse(fs.readFileSync(claudeConfigPath, 'utf8')); } catch (e) {}
677
+ mergeMcpServer(claudeDesktopPath);
678
+ console.log('Claude Desktop MCP config written to ' + claudeDesktopPath);
679
+
680
+ var claudeCodePath = path.join(home, '.claude.json');
681
+ mergeMcpServer(claudeCodePath);
682
+ console.log('Claude Code MCP config written to ' + claudeCodePath);
683
+
684
+ if (looksLikeProjectCwd()) {
685
+ var projectMcp = path.join(process.cwd(), '.mcp.json');
686
+ mergeMcpServer(projectMcp);
687
+ console.log('Project MCP config written to ' + projectMcp);
561
688
  }
562
- claudeConfig.mcpServers = claudeConfig.mcpServers || {};
563
- claudeConfig.mcpServers['analyzthis_design'] = {
564
- command: serverCmd,
565
- args: serverArgs,
566
- };
567
- fs.writeFileSync(claudeConfigPath, JSON.stringify(claudeConfig, null, 2));
568
- console.log('Claude Desktop MCP config written to ' + claudeConfigPath);
569
- console.log('Restart Claude Desktop to activate.\n');
689
+
690
+ console.log('Default MCP catalog is lite. Full list: ANALYZTHIS_MCP_CATALOG=full');
691
+ console.log('Restart Claude Code and Claude Desktop to activate.\n');
570
692
  return true;
571
693
  }
572
694
 
573
695
  if (target === 'windsurf') {
574
696
  var windsurfConfigPath = path.join(home, '.codeium', 'windsurf', 'mcp_config.json');
575
- var windsurfDir = path.dirname(windsurfConfigPath);
576
- if (!fs.existsSync(windsurfDir)) fs.mkdirSync(windsurfDir, { recursive: true });
577
- var windsurfConfig = {};
578
- if (fs.existsSync(windsurfConfigPath)) {
579
- try { windsurfConfig = JSON.parse(fs.readFileSync(windsurfConfigPath, 'utf8')); } catch (e) {}
580
- }
581
- windsurfConfig.mcpServers = windsurfConfig.mcpServers || {};
582
- windsurfConfig.mcpServers['analyzthis_design'] = {
583
- command: serverCmd,
584
- args: serverArgs,
585
- };
586
- fs.writeFileSync(windsurfConfigPath, JSON.stringify(windsurfConfig, null, 2));
697
+ mergeMcpServer(windsurfConfigPath);
587
698
  console.log('Windsurf MCP config written to ' + windsurfConfigPath);
588
699
  console.log('Restart Windsurf to activate.\n');
589
700
  return true;
@@ -593,8 +704,8 @@ function writeMcpConfig(target) {
593
704
  console.log(JSON.stringify({
594
705
  mcpServers: {
595
706
  'analyzthis_design': {
596
- command: serverCmd,
597
- args: serverArgs,
707
+ command: 'npx',
708
+ args: ['analyzthis_design', 'mcp'],
598
709
  }
599
710
  }
600
711
  }, null, 2));
@@ -604,7 +715,9 @@ function writeMcpConfig(target) {
604
715
 
605
716
  // ── MCP protocol (JSON-RPC 2.0 over stdio) ────────────────────────────────
606
717
 
607
- function startStdioServer() {
718
+ function startStdioServer(catalog) {
719
+ var catalogMode = resolveCatalog(catalog);
720
+ var activeTools = listedTools(catalogMode);
608
721
  var rl = readline.createInterface({ input: process.stdin, terminal: false });
609
722
  var initialized = false;
610
723
 
@@ -649,7 +762,7 @@ function startStdioServer() {
649
762
  jsonrpc: '2.0',
650
763
  id: id,
651
764
  result: {
652
- tools: TOOLS.map(function(t) {
765
+ tools: activeTools.map(function(t) {
653
766
  return {
654
767
  name: t.name,
655
768
  description: t.description,
@@ -675,11 +788,17 @@ function startStdioServer() {
675
788
  });
676
789
  } else {
677
790
  var text = result.prompt || result.result || result.command || JSON.stringify(result, null, 2);
791
+ var receipt = require('./receipt');
792
+ var wrapped = receipt.wrapPayload(text, {
793
+ path: 'mcp',
794
+ persona: result.persona || result.skill || toolName,
795
+ verdicts: result.verdicts || 0,
796
+ });
678
797
  send({
679
798
  jsonrpc: '2.0',
680
799
  id: id,
681
800
  result: {
682
- content: [{ type: 'text', text: String(text) }],
801
+ content: [{ type: 'text', text: wrapped }],
683
802
  },
684
803
  });
685
804
  }
@@ -713,24 +832,32 @@ function startStdioServer() {
713
832
 
714
833
  if (process.env.MCP_DEBUG) {
715
834
  process.stderr.write('analyzthis_design MCP server v' + VERSION + ' running on stdio\n');
716
- process.stderr.write('Tools: ' + TOOLS.length + ' available\n');
835
+ process.stderr.write('Tools: ' + activeTools.length + ' (' + catalogMode + ' catalog)\n');
717
836
  }
718
837
  }
719
838
 
720
839
  // ── Entry point ──
721
840
 
841
+ function parseCatalogFlag(args) {
842
+ var eq = args.filter(function (a) { return a.indexOf('--catalog=') === 0; })[0];
843
+ if (eq) return eq.split('=').slice(1).join('=');
844
+ var idx = args.indexOf('--catalog');
845
+ if (idx !== -1 && args[idx + 1] && args[idx + 1].indexOf('--') !== 0) return args[idx + 1];
846
+ return null;
847
+ }
848
+
722
849
  function main() {
723
850
  var args = process.argv.slice(2);
851
+ var catalog = parseCatalogFlag(args);
724
852
 
725
853
  // --configure <target>
726
854
  var configureIdx = args.indexOf('--configure');
727
855
  if (configureIdx !== -1 && args[configureIdx + 1]) {
728
- writeMcpConfig(args[configureIdx + 1]);
856
+ writeMcpConfig(args[configureIdx + 1], { catalog: catalog });
729
857
  return;
730
858
  }
731
859
 
732
- // Default: start stdio server
733
- startStdioServer();
860
+ startStdioServer(catalog);
734
861
  }
735
862
 
736
863
  if (require.main === module) {
@@ -739,6 +866,10 @@ if (require.main === module) {
739
866
 
740
867
  module.exports = {
741
868
  TOOLS: TOOLS,
869
+ ROUTER_TOOL: ROUTER_TOOL,
870
+ RECEIPT_TOOL: RECEIPT_TOOL,
871
+ listedTools: listedTools,
872
+ resolveCatalog: resolveCatalog,
742
873
  handleToolCall: handleToolCall,
743
874
  startStdioServer: startStdioServer,
744
875
  writeMcpConfig: writeMcpConfig,
@@ -819,6 +819,8 @@ async function runUnchunked(opts = {}) {
819
819
  objections_raised: delibResult.metrics.objections_raised,
820
820
  objections_resolved: delibResult.metrics.objections_resolved,
821
821
  raj_escalations: delibResult.metrics.raj_escalations,
822
+ verdicts_this_run: 1,
823
+ tokens_per_verdict: (delibResult.metrics.input_tokens_est || 0) || null,
822
824
  },
823
825
  },
824
826
  });
@@ -834,6 +836,8 @@ async function runUnchunked(opts = {}) {
834
836
  if (delibResult.deliberation.raj_escalated) console.log(' Raj escalated: yes');
835
837
  console.log(` Verdict: ${syn.composite.verdict} (${syn.composite.total}/${syn.composite.max_total})`);
836
838
  console.log(` Est. tokens: ${delibResult.metrics.input_tokens_est} in / ${delibResult.metrics.output_tokens_est} out`);
839
+ const tpvUn = delibResult.metrics.input_tokens_est || 0;
840
+ if (tpvUn) console.log(` Tokens per verdict: ${tpvUn} (CLI est. input / 1)`);
837
841
  if (costUsd) console.log(` Est. cost: $${costUsd.toFixed(4)}`);
838
842
  console.log(` Session: ${session.sessionPath(projectId)}`);
839
843
  console.log('\n── Synthesis ──────────────────────────────────────');
@@ -878,6 +882,7 @@ async function run(opts = {}) {
878
882
  console.log(` Cost: $${(chunkRunState.total_cost || 0).toFixed(4)}`);
879
883
  const tokens = chunkRunState.total_tokens || { input: 0, output: 0 };
880
884
  console.log(` Tokens: ${tokens.input} in / ${tokens.output} out`);
885
+ if (tokens.input) console.log(` Tokens per verdict: ${tokens.input} (CLI est. input / 1)`);
881
886
  if (syn) {
882
887
  console.log(` Verdict: ${syn.verdict || 'n/a'} (${syn.total || 0}/${syn.max_total || 0})`);
883
888
  if (syn.premise_challenge) {
@@ -95,6 +95,9 @@ function defaultState(projectId) {
95
95
  objections_raised: 0,
96
96
  objections_resolved: 0,
97
97
  raj_escalations: 0,
98
+ host_turns: [],
99
+ verdicts_this_run: 0,
100
+ tokens_per_verdict: null,
98
101
  },
99
102
  };
100
103
  }
@@ -147,7 +150,11 @@ function reset({ project, all = false } = {}) {
147
150
  function update({ project, patch = {} } = {}) {
148
151
  const projectId = project || getProjectId();
149
152
  const existing = show({ project: projectId }) || defaultState(projectId);
150
- const merged = { ...existing, ...patch, updated_at: new Date().toISOString() };
153
+ const patchCopy = { ...patch };
154
+ if (patchCopy.metrics && existing.metrics) {
155
+ patchCopy.metrics = { ...existing.metrics, ...patchCopy.metrics };
156
+ }
157
+ const merged = { ...existing, ...patchCopy, updated_at: new Date().toISOString() };
151
158
  const dir = sessionDir(projectId);
152
159
  fs.mkdirSync(dir, { recursive: true });
153
160
  fs.writeFileSync(sessionPath(projectId), JSON.stringify(merged, null, 2));
@@ -181,6 +188,30 @@ function markAccepted({ project, persona, accepted = true } = {}) {
181
188
  return { updated: true, state: merged };
182
189
  }
183
190
 
191
+ /**
192
+ * Append one inferred host (slash/MCP) turn. Caps the log. Recomputes tokens_per_verdict.
193
+ */
194
+ function logHostTurn({ project, path: turnPath, persona, inferred_in, verdicts = 0 } = {}) {
195
+ const projectId = project || getProjectId();
196
+ const existing = show({ project: projectId }) || defaultState(projectId);
197
+ const metrics = { ...(existing.metrics || {}) };
198
+ const turns = Array.isArray(metrics.host_turns) ? metrics.host_turns.slice() : [];
199
+ turns.push({
200
+ at: new Date().toISOString(),
201
+ path: turnPath || 'mcp',
202
+ persona: persona || '',
203
+ inferred_in: Number(inferred_in) || 0,
204
+ });
205
+ const cap = 40;
206
+ metrics.host_turns = turns.length > cap ? turns.slice(turns.length - cap) : turns;
207
+ if (verdicts) metrics.verdicts_this_run = (metrics.verdicts_this_run || 0) + verdicts;
208
+ const host = metrics.host_turns.reduce((sum, t) => sum + (t.inferred_in || 0), 0);
209
+ const cliIn = metrics.input_tokens_est || 0;
210
+ const v = metrics.verdicts_this_run || (metrics.experts_run && metrics.experts_run.length ? 1 : 0) || (metrics.host_turns.length ? 1 : 0);
211
+ metrics.tokens_per_verdict = v && (host + cliIn) ? Math.round((host + cliIn) / v) : null;
212
+ return update({ project: projectId, patch: { metrics } });
213
+ }
214
+
184
215
  module.exports = {
185
- getProjectId, sessionPath, sessionDir, init, show, reset, update, listProjects, markAccepted, SESSIONS_ROOT,
216
+ getProjectId, sessionPath, sessionDir, init, show, reset, update, listProjects, markAccepted, logHostTurn, SESSIONS_ROOT,
186
217
  };
@@ -21,6 +21,13 @@ You are Anuj (alias: Dev). 6 years as a domain analyst in high-volume operations
21
21
 
22
22
  **Assess-only:** if the user asked to assess/propose/critique rather than build/implement/ship, stop at the concept — do not edit code.
23
23
 
24
+
25
+ ## Lite output (default)
26
+
27
+ Verdict or one moment. Top three fixes (or one delight). One evidence line.
28
+ Use the full output schema below only if the user says expand.
29
+ You cannot know host tokens. Do not invent a dollar figure.
30
+
24
31
  ## Non-negotiables
25
32
 
26
33
  - **Density never flattens the information hierarchy.** Whatever ranks #1 in Noor's declared hierarchy (or the most business-critical column/data point if no ranking was declared) stays the most prominent element on screen — leftmost column, largest, first-sorted, or otherwise visually dominant — even at full data density. "Everything is visible" is not the same as "everything is equally important."