up-cc 0.14.0 → 0.16.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 (171) hide show
  1. package/package.json +9 -9
  2. package/{agents → up/agents}/up-architecture-supervisor.md +1 -1
  3. package/{agents → up/agents}/up-audit-supervisor.md +1 -1
  4. package/{agents → up/agents}/up-backend-specialist.md +13 -3
  5. package/{agents → up/agents}/up-chief-architect.md +1 -1
  6. package/{agents → up/agents}/up-chief-engineer.md +1 -1
  7. package/{agents → up/agents}/up-chief-operations.md +2 -2
  8. package/{agents → up/agents}/up-chief-product.md +1 -1
  9. package/{agents → up/agents}/up-chief-quality.md +1 -1
  10. package/{agents → up/agents}/up-code-reviewer.md +2 -2
  11. package/{agents → up/agents}/up-database-specialist.md +13 -3
  12. package/{agents → up/agents}/up-execution-supervisor.md +4 -4
  13. package/{agents → up/agents}/up-executor.md +107 -9
  14. package/{agents → up/agents}/up-frontend-specialist.md +13 -3
  15. package/{agents → up/agents}/up-operations-supervisor.md +1 -1
  16. package/{agents → up/agents}/up-planejador.md +17 -1
  17. package/{agents → up/agents}/up-planning-supervisor.md +3 -3
  18. package/{agents → up/agents}/up-product-supervisor.md +1 -1
  19. package/{agents → up/agents}/up-project-ceo.md +2 -2
  20. package/{agents → up/agents}/up-quality-supervisor.md +1 -1
  21. package/{agents → up/agents}/up-system-designer.md +2 -2
  22. package/{agents → up/agents}/up-verification-supervisor.md +1 -1
  23. package/{agents → up/agents}/up-visual-critic.md +1 -1
  24. package/{bin → up/bin}/lib/core.cjs +132 -0
  25. package/{bin → up/bin}/up-tools.cjs +1341 -3
  26. package/up/commands/adicionar-fase.md +47 -0
  27. package/up/commands/configurar.md +219 -0
  28. package/{commands → up/commands}/depurar.md +1 -1
  29. package/{commands → up/commands}/planejar-fase.md +4 -2
  30. package/up/templates/config.json +8 -0
  31. package/up/workflows/build.md +650 -0
  32. package/{workflows → up/workflows}/builder.md +766 -35
  33. package/{workflows → up/workflows}/executar-plano.md +69 -0
  34. package/{workflows → up/workflows}/governance.md +3 -3
  35. package/{workflows → up/workflows}/plan.md +214 -29
  36. package/{workflows → up/workflows}/planejar-fase.md +63 -5
  37. package/commands/adicionar-fase.md +0 -33
  38. package/commands/configurar.md +0 -106
  39. package/templates/config.json +0 -6
  40. package/workflows/build.md +0 -431
  41. /package/{agents → up/agents}/up-analista-codigo.md +0 -0
  42. /package/{agents → up/agents}/up-api-tester.md +0 -0
  43. /package/{agents → up/agents}/up-arquiteto.md +0 -0
  44. /package/{agents → up/agents}/up-auditor-modernidade.md +0 -0
  45. /package/{agents → up/agents}/up-auditor-performance.md +0 -0
  46. /package/{agents → up/agents}/up-auditor-ux.md +0 -0
  47. /package/{agents → up/agents}/up-blind-validator.md +0 -0
  48. /package/{agents → up/agents}/up-clone-crawler.md +0 -0
  49. /package/{agents → up/agents}/up-clone-design-extractor.md +0 -0
  50. /package/{agents → up/agents}/up-clone-feature-mapper.md +0 -0
  51. /package/{agents → up/agents}/up-clone-prd-writer.md +0 -0
  52. /package/{agents → up/agents}/up-clone-verifier.md +0 -0
  53. /package/{agents → up/agents}/up-consolidador-ideias.md +0 -0
  54. /package/{agents → up/agents}/up-delivery-auditor.md +0 -0
  55. /package/{agents → up/agents}/up-depurador.md +0 -0
  56. /package/{agents → up/agents}/up-devops-agent.md +0 -0
  57. /package/{agents → up/agents}/up-exhaustive-tester.md +0 -0
  58. /package/{agents → up/agents}/up-mapeador-codigo.md +0 -0
  59. /package/{agents → up/agents}/up-pesquisador-mercado.md +0 -0
  60. /package/{agents → up/agents}/up-pesquisador-projeto.md +0 -0
  61. /package/{agents → up/agents}/up-planning-auditor.md +0 -0
  62. /package/{agents → up/agents}/up-product-analyst.md +0 -0
  63. /package/{agents → up/agents}/up-qa-agent.md +0 -0
  64. /package/{agents → up/agents}/up-requirements-validator.md +0 -0
  65. /package/{agents → up/agents}/up-roteirista.md +0 -0
  66. /package/{agents → up/agents}/up-security-reviewer.md +0 -0
  67. /package/{agents → up/agents}/up-sintetizador-melhorias.md +0 -0
  68. /package/{agents → up/agents}/up-sintetizador.md +0 -0
  69. /package/{agents → up/agents}/up-technical-writer.md +0 -0
  70. /package/{agents → up/agents}/up-verificador.md +0 -0
  71. /package/{bin → up/bin}/install.js +0 -0
  72. /package/{bin → up/bin}/up-instrument.cjs +0 -0
  73. /package/{commands → up/commands}/adicionar-testes.md +0 -0
  74. /package/{commands → up/commands}/ajuda.md +0 -0
  75. /package/{commands → up/commands}/atualizar.md +0 -0
  76. /package/{commands → up/commands}/build.md +0 -0
  77. /package/{commands → up/commands}/clone-builder.md +0 -0
  78. /package/{commands → up/commands}/custos.md +0 -0
  79. /package/{commands → up/commands}/dashboard.md +0 -0
  80. /package/{commands → up/commands}/discutir-fase.md +0 -0
  81. /package/{commands → up/commands}/executar-fase.md +0 -0
  82. /package/{commands → up/commands}/ideias.md +0 -0
  83. /package/{commands → up/commands}/iniciar.md +0 -0
  84. /package/{commands → up/commands}/mapear-codigo.md +0 -0
  85. /package/{commands → up/commands}/melhorias.md +0 -0
  86. /package/{commands → up/commands}/mobile-first.md +0 -0
  87. /package/{commands → up/commands}/modo-builder.md +0 -0
  88. /package/{commands → up/commands}/novo-projeto.md +0 -0
  89. /package/{commands → up/commands}/onboard.md +0 -0
  90. /package/{commands → up/commands}/pausar.md +0 -0
  91. /package/{commands → up/commands}/plan.md +0 -0
  92. /package/{commands → up/commands}/progresso.md +0 -0
  93. /package/{commands → up/commands}/rapido.md +0 -0
  94. /package/{commands → up/commands}/remover-fase.md +0 -0
  95. /package/{commands → up/commands}/resetar.md +0 -0
  96. /package/{commands → up/commands}/retomar.md +0 -0
  97. /package/{commands → up/commands}/saude.md +0 -0
  98. /package/{commands → up/commands}/testar.md +0 -0
  99. /package/{commands → up/commands}/ux-tester.md +0 -0
  100. /package/{commands → up/commands}/verificar-trabalho.md +0 -0
  101. /package/{hooks → up/hooks}/up-context-monitor.js +0 -0
  102. /package/{hooks → up/hooks}/up-statusline.js +0 -0
  103. /package/{references → up/references}/audit-modernidade.md +0 -0
  104. /package/{references → up/references}/audit-performance.md +0 -0
  105. /package/{references → up/references}/audit-ux.md +0 -0
  106. /package/{references → up/references}/blueprints/audit.md +0 -0
  107. /package/{references → up/references}/blueprints/booking.md +0 -0
  108. /package/{references → up/references}/blueprints/community.md +0 -0
  109. /package/{references → up/references}/blueprints/crm.md +0 -0
  110. /package/{references → up/references}/blueprints/dashboard.md +0 -0
  111. /package/{references → up/references}/blueprints/data-management.md +0 -0
  112. /package/{references → up/references}/blueprints/ecommerce.md +0 -0
  113. /package/{references → up/references}/blueprints/marketplace.md +0 -0
  114. /package/{references → up/references}/blueprints/notifications.md +0 -0
  115. /package/{references → up/references}/blueprints/saas-users.md +0 -0
  116. /package/{references → up/references}/blueprints/settings.md +0 -0
  117. /package/{references → up/references}/checkpoints.md +0 -0
  118. /package/{references → up/references}/engineering-principles-compressed.md +0 -0
  119. /package/{references → up/references}/engineering-principles.md +0 -0
  120. /package/{references → up/references}/git-integration.md +0 -0
  121. /package/{references → up/references}/governance-rules-compressed.md +0 -0
  122. /package/{references → up/references}/governance-rules.md +0 -0
  123. /package/{references → up/references}/production-requirements-compressed.md +0 -0
  124. /package/{references → up/references}/production-requirements.md +0 -0
  125. /package/{references → up/references}/questioning.md +0 -0
  126. /package/{references → up/references}/rework-limits-compressed.md +0 -0
  127. /package/{references → up/references}/rework-limits.md +0 -0
  128. /package/{references → up/references}/severity-levels.md +0 -0
  129. /package/{references → up/references}/state-persistence.md +0 -0
  130. /package/{references → up/references}/ui-brand.md +0 -0
  131. /package/{templates → up/templates}/audit-plan.md +0 -0
  132. /package/{templates → up/templates}/audit-report.md +0 -0
  133. /package/{templates → up/templates}/builder-defaults.md +0 -0
  134. /package/{templates → up/templates}/checklist.md +0 -0
  135. /package/{templates → up/templates}/continue-here.md +0 -0
  136. /package/{templates → up/templates}/delivery.md +0 -0
  137. /package/{templates → up/templates}/design-tokens.md +0 -0
  138. /package/{templates → up/templates}/owner-profile.md +0 -0
  139. /package/{templates → up/templates}/owner.md +0 -0
  140. /package/{templates → up/templates}/pending.md +0 -0
  141. /package/{templates → up/templates}/plan-ready.md +0 -0
  142. /package/{templates → up/templates}/project.md +0 -0
  143. /package/{templates → up/templates}/report.md +0 -0
  144. /package/{templates → up/templates}/requirements.md +0 -0
  145. /package/{templates → up/templates}/roadmap.md +0 -0
  146. /package/{templates → up/templates}/state.md +0 -0
  147. /package/{templates → up/templates}/suggestion.md +0 -0
  148. /package/{templates → up/templates}/summary.md +0 -0
  149. /package/{workflows → up/workflows}/adicionar-fase.md +0 -0
  150. /package/{workflows → up/workflows}/builder-e2e.md +0 -0
  151. /package/{workflows → up/workflows}/ceo-intake.md +0 -0
  152. /package/{workflows → up/workflows}/ceo-updates.md +0 -0
  153. /package/{workflows → up/workflows}/clone-builder.md +0 -0
  154. /package/{workflows → up/workflows}/dcrv.md +0 -0
  155. /package/{workflows → up/workflows}/discutir-fase.md +0 -0
  156. /package/{workflows → up/workflows}/executar-fase.md +0 -0
  157. /package/{workflows → up/workflows}/ideias.md +0 -0
  158. /package/{workflows → up/workflows}/iniciar.md +0 -0
  159. /package/{workflows → up/workflows}/mapear-codigo.md +0 -0
  160. /package/{workflows → up/workflows}/melhorias.md +0 -0
  161. /package/{workflows → up/workflows}/mobile-first.md +0 -0
  162. /package/{workflows → up/workflows}/novo-projeto.md +0 -0
  163. /package/{workflows → up/workflows}/onboarding.md +0 -0
  164. /package/{workflows → up/workflows}/pausar.md +0 -0
  165. /package/{workflows → up/workflows}/progresso.md +0 -0
  166. /package/{workflows → up/workflows}/rapido.md +0 -0
  167. /package/{workflows → up/workflows}/remover-fase.md +0 -0
  168. /package/{workflows → up/workflows}/resetar.md +0 -0
  169. /package/{workflows → up/workflows}/retomar.md +0 -0
  170. /package/{workflows → up/workflows}/ux-tester.md +0 -0
  171. /package/{workflows → up/workflows}/verificar-trabalho.md +0 -0
@@ -12,7 +12,7 @@
12
12
  * state load|get|update|advance-plan|update-progress|add-decision|record-session|record-metric|snapshot|save-session
13
13
  * roadmap get-phase|analyze|update-plan-progress
14
14
  * phase add|remove|find|complete|generate-from-report
15
- * config get|set
15
+ * config get|set|resolve-model|list-presets
16
16
  * requirements mark-complete
17
17
  * commit <msg> --files
18
18
  * progress [json|table|bar]
@@ -26,7 +26,9 @@
26
26
  const fs = require('fs');
27
27
  const path = require('path');
28
28
  const {
29
- output, error, loadConfig, isGitIgnored, execGit,
29
+ output, error, loadConfig, resolveAgentModel,
30
+ MODEL_PRESETS, AGENT_ROLE_MAP,
31
+ isGitIgnored, execGit,
30
32
  escapeRegex, normalizePhaseName, comparePhaseNum,
31
33
  findPhaseInternal, getRoadmapPhaseInternal,
32
34
  pathExistsInternal, generateSlugInternal, toPosixPath,
@@ -314,8 +316,12 @@ function main() {
314
316
  cmdConfigGet(cwd, args[2], raw);
315
317
  } else if (sub === 'set') {
316
318
  cmdConfigSet(cwd, args[2], args[3], raw);
319
+ } else if (sub === 'resolve-model') {
320
+ cmdConfigResolveModel(cwd, args[2], raw);
321
+ } else if (sub === 'list-presets') {
322
+ cmdConfigListPresets(raw);
317
323
  } else {
318
- error('Unknown config subcommand. Available: get, set');
324
+ error('Unknown config subcommand. Available: get, set, resolve-model, list-presets');
319
325
  }
320
326
  break;
321
327
  }
@@ -348,6 +354,78 @@ function main() {
348
354
  break;
349
355
  }
350
356
 
357
+ // ==================== BUDGET ====================
358
+ case 'budget': {
359
+ cmdBudget(cwd, raw);
360
+ break;
361
+ }
362
+
363
+ // ==================== STATUS (headless aggregator) ====================
364
+ case 'status': {
365
+ cmdStatus(cwd, raw);
366
+ break;
367
+ }
368
+
369
+ // ==================== CONTEXT (pre-inline helper) ====================
370
+ case 'context': {
371
+ cmdContext(cwd, args.slice(1), raw);
372
+ break;
373
+ }
374
+
375
+ // ==================== TIMEOUT (Wave 3) ====================
376
+ case 'timeout': {
377
+ cmdTimeout(args.slice(1), raw);
378
+ break;
379
+ }
380
+
381
+ // ==================== STUCK CHECK (Wave 3) ====================
382
+ case 'stuck-check': {
383
+ cmdStuckCheck(cwd, args.slice(1), raw);
384
+ break;
385
+ }
386
+
387
+ // ==================== VERIFY-STATIC (Wave 4) ====================
388
+ case 'verify-static': {
389
+ cmdVerifyStatic(cwd, args.slice(1), raw);
390
+ break;
391
+ }
392
+
393
+ // ==================== CLASSIFY-TASK (Wave 5) ====================
394
+ case 'classify-task': {
395
+ cmdClassifyTask(cwd, args.slice(1), raw);
396
+ break;
397
+ }
398
+
399
+ // ==================== RESOLVE-MODEL-FOR-PLAN (Wave 5) ====================
400
+ case 'resolve-model-for-plan': {
401
+ cmdResolveModelForPlan(cwd, args.slice(1), raw);
402
+ break;
403
+ }
404
+
405
+ // ==================== ROUTING-LOG (Wave 5) ====================
406
+ case 'routing-log': {
407
+ cmdRoutingLog(cwd, args.slice(1), raw);
408
+ break;
409
+ }
410
+
411
+ // ==================== ANALYZE-ROUTING (Wave 5) ====================
412
+ case 'analyze-routing': {
413
+ cmdAnalyzeRouting(cwd, raw);
414
+ break;
415
+ }
416
+
417
+ // ==================== VALIDATE-PLAN (Wave 6) ====================
418
+ case 'validate-plan': {
419
+ cmdValidatePlan(cwd, args.slice(1), raw);
420
+ break;
421
+ }
422
+
423
+ // ==================== SKILL-MANIFEST (Wave 6) ====================
424
+ case 'skill-manifest': {
425
+ cmdSkillManifest(args.slice(1), raw);
426
+ break;
427
+ }
428
+
351
429
  // ==================== TIMESTAMP ====================
352
430
  case 'timestamp': {
353
431
  cmdTimestamp(args[1] || 'full', raw);
@@ -2303,6 +2381,7 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
2303
2381
  let parsedValue = value;
2304
2382
  if (value === 'true') parsedValue = true;
2305
2383
  else if (value === 'false') parsedValue = false;
2384
+ else if (value === 'null') parsedValue = null;
2306
2385
  else if (!isNaN(value) && value !== '') parsedValue = Number(value);
2307
2386
 
2308
2387
  let config = {};
@@ -2334,6 +2413,16 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
2334
2413
  }
2335
2414
  }
2336
2415
 
2416
+ function cmdConfigResolveModel(cwd, agentName, raw) {
2417
+ if (!agentName) error('Usage: config resolve-model <agent-name>');
2418
+ const model = resolveAgentModel(cwd, agentName);
2419
+ output({ agent: agentName, model: model, role: AGENT_ROLE_MAP[agentName] || 'unknown' }, raw, model || 'default');
2420
+ }
2421
+
2422
+ function cmdConfigListPresets(raw) {
2423
+ output(MODEL_PRESETS, raw, JSON.stringify(MODEL_PRESETS, null, 2));
2424
+ }
2425
+
2337
2426
  // =====================================================================
2338
2427
  // REQUIREMENTS COMMANDS
2339
2428
  // =====================================================================
@@ -2407,6 +2496,1255 @@ function cmdCommit(cwd, message, files, raw) {
2407
2496
  output({ committed: true, hash, reason: 'committed' }, raw, hash || 'committed');
2408
2497
  }
2409
2498
 
2499
+ // =====================================================================
2500
+ // BUDGET COMMAND
2501
+ // =====================================================================
2502
+
2503
+ function cmdBudget(cwd, raw) {
2504
+ const config = loadConfig(cwd);
2505
+ const ceiling = config.budget_ceiling;
2506
+
2507
+ // Spawn up-instrument.cjs report --json to get current spend
2508
+ const { execSync } = require('child_process');
2509
+ const instrumentPath = path.join(__dirname, 'up-instrument.cjs');
2510
+
2511
+ let spend = 0;
2512
+ let agentCount = 0;
2513
+ let report = null;
2514
+
2515
+ try {
2516
+ const out = execSync(`node "${instrumentPath}" report --json --all-sessions`, {
2517
+ cwd,
2518
+ encoding: 'utf-8',
2519
+ stdio: ['pipe', 'pipe', 'pipe'],
2520
+ timeout: 30000, // 30s cap — fail open if jsonl scan is slow
2521
+ });
2522
+ report = JSON.parse(out);
2523
+ spend = report.total_cost_usd ?? 0;
2524
+ agentCount = (report.agents ?? []).reduce((sum, a) => sum + (a.invocations || 0), 0);
2525
+ } catch (err) {
2526
+ // instrumentation may fail on non-Claude Code runtimes or timeout — return zero spend
2527
+ spend = 0;
2528
+ }
2529
+
2530
+ const result = {
2531
+ spend_usd: Number(spend.toFixed(4)),
2532
+ ceiling_usd: ceiling,
2533
+ agent_invocations: agentCount,
2534
+ status: 'unknown',
2535
+ percent_used: null,
2536
+ };
2537
+
2538
+ if (ceiling === null || ceiling === undefined) {
2539
+ result.status = 'no_ceiling';
2540
+ } else if (ceiling <= 0) {
2541
+ result.status = 'no_ceiling';
2542
+ } else {
2543
+ const pct = (spend / ceiling) * 100;
2544
+ result.percent_used = Number(pct.toFixed(1));
2545
+ if (pct >= 100) result.status = 'over_budget';
2546
+ else if (pct >= 90) result.status = 'critical_90';
2547
+ else if (pct >= 75) result.status = 'warning_75';
2548
+ else if (pct >= 50) result.status = 'warning_50';
2549
+ else result.status = 'under_budget';
2550
+ }
2551
+
2552
+ output(
2553
+ result,
2554
+ raw,
2555
+ ceiling
2556
+ ? `Spent: $${result.spend_usd} / $${ceiling} (${result.percent_used}%) [${result.status}]`
2557
+ : `Spent: $${result.spend_usd} (no ceiling configured)`
2558
+ );
2559
+ }
2560
+
2561
+ // =====================================================================
2562
+ // TIMEOUT COMMAND (Wave 3 — soft/idle/hard timeouts)
2563
+ // =====================================================================
2564
+
2565
+ /**
2566
+ * Compute timeout status given a start time and limits.
2567
+ *
2568
+ * Usage:
2569
+ * up-tools.cjs timeout --start <epoch> --soft <secs> --hard <secs>
2570
+ * [--idle-since <epoch>] [--idle <secs>]
2571
+ *
2572
+ * Returns JSON:
2573
+ * { elapsed_secs, status, remaining_soft, remaining_hard,
2574
+ * idle_secs, idle_status }
2575
+ *
2576
+ * Status values:
2577
+ * ok — under all limits
2578
+ * soft_warning — past soft, warn but continue
2579
+ * idle_warning — idle for too long, may be stuck
2580
+ * hard_abort — past hard, abort immediately
2581
+ *
2582
+ * Defaults match gsd-2:
2583
+ * soft=1200 (20m), hard=1800 (30m), idle=600 (10m)
2584
+ */
2585
+ function cmdTimeout(args, raw) {
2586
+ const flags = { start: null, soft: 1200, hard: 1800, idleSince: null, idle: 600 };
2587
+ for (let i = 0; i < args.length; i++) {
2588
+ const a = args[i];
2589
+ if (a === '--start') flags.start = parseInt(args[++i], 10);
2590
+ else if (a === '--soft') flags.soft = parseInt(args[++i], 10);
2591
+ else if (a === '--hard') flags.hard = parseInt(args[++i], 10);
2592
+ else if (a === '--idle-since') flags.idleSince = parseInt(args[++i], 10);
2593
+ else if (a === '--idle') flags.idle = parseInt(args[++i], 10);
2594
+ }
2595
+
2596
+ if (!flags.start) error('Usage: timeout --start <epoch> [--soft N] [--hard N] [--idle-since E] [--idle N]');
2597
+
2598
+ const now = Math.floor(Date.now() / 1000);
2599
+ const elapsed = now - flags.start;
2600
+ const remainingSoft = Math.max(0, flags.soft - elapsed);
2601
+ const remainingHard = Math.max(0, flags.hard - elapsed);
2602
+
2603
+ let status = 'ok';
2604
+ if (elapsed >= flags.hard) status = 'hard_abort';
2605
+ else if (elapsed >= flags.soft) status = 'soft_warning';
2606
+
2607
+ const result = {
2608
+ elapsed_secs: elapsed,
2609
+ status,
2610
+ remaining_soft: remainingSoft,
2611
+ remaining_hard: remainingHard,
2612
+ };
2613
+
2614
+ if (flags.idleSince) {
2615
+ const idleSecs = now - flags.idleSince;
2616
+ result.idle_secs = idleSecs;
2617
+ if (idleSecs >= flags.idle) {
2618
+ result.idle_status = 'idle_warning';
2619
+ // Idle warning escalates to hard_abort if also past soft
2620
+ if (status === 'soft_warning') {
2621
+ result.status = 'hard_abort';
2622
+ } else if (status === 'ok') {
2623
+ result.status = 'idle_warning';
2624
+ }
2625
+ } else {
2626
+ result.idle_status = 'ok';
2627
+ }
2628
+ }
2629
+
2630
+ output(result, raw, `${result.status} | elapsed=${elapsed}s soft_remaining=${remainingSoft}s hard_remaining=${remainingHard}s`);
2631
+ }
2632
+
2633
+ // =====================================================================
2634
+ // STUCK CHECK COMMAND (Wave 3 — sliding window pattern detector)
2635
+ // =====================================================================
2636
+
2637
+ /**
2638
+ * Detect repeated tool-call patterns by reading an activity log.
2639
+ *
2640
+ * Agents append events to .plano/runtime/agent-activity-<id>.log via
2641
+ * Bash echo. Each line: "<timestamp>|<tool>|<target>". This command
2642
+ * scans the last N lines and flags repetition.
2643
+ *
2644
+ * Usage:
2645
+ * up-tools.cjs stuck-check --log <path> [--window 10] [--threshold 3]
2646
+ *
2647
+ * Returns:
2648
+ * { stuck: bool, reason, repeated_pattern, count, window_size }
2649
+ *
2650
+ * Stuck = same (tool|target) appears `threshold` times within
2651
+ * `window` last entries.
2652
+ */
2653
+ function cmdStuckCheck(cwd, args, raw) {
2654
+ const flags = { log: null, window: 10, threshold: 3 };
2655
+ for (let i = 0; i < args.length; i++) {
2656
+ const a = args[i];
2657
+ if (a === '--log') flags.log = args[++i];
2658
+ else if (a === '--window') flags.window = parseInt(args[++i], 10);
2659
+ else if (a === '--threshold') flags.threshold = parseInt(args[++i], 10);
2660
+ }
2661
+
2662
+ if (!flags.log) error('Usage: stuck-check --log <path> [--window N] [--threshold N]');
2663
+
2664
+ const logPath = path.isAbsolute(flags.log) ? flags.log : path.join(cwd, flags.log);
2665
+
2666
+ let lines = [];
2667
+ try {
2668
+ const content = fs.readFileSync(logPath, 'utf-8');
2669
+ lines = content.split('\n').filter(l => l.trim() && l.includes('|'));
2670
+ } catch {
2671
+ output({ stuck: false, reason: 'no_log', repeated_pattern: null, count: 0, window_size: 0 }, raw, 'no_log');
2672
+ return;
2673
+ }
2674
+
2675
+ const window = lines.slice(-flags.window);
2676
+ const counts = new Map();
2677
+
2678
+ for (const line of window) {
2679
+ const parts = line.split('|').map(s => s.trim());
2680
+ // Treat tool|target as the pattern (skip timestamp)
2681
+ const key = parts.slice(1, 3).join('|');
2682
+ counts.set(key, (counts.get(key) || 0) + 1);
2683
+ }
2684
+
2685
+ let topPattern = null;
2686
+ let topCount = 0;
2687
+ for (const [key, count] of counts) {
2688
+ if (count > topCount) {
2689
+ topPattern = key;
2690
+ topCount = count;
2691
+ }
2692
+ }
2693
+
2694
+ const stuck = topCount >= flags.threshold;
2695
+
2696
+ output(
2697
+ {
2698
+ stuck,
2699
+ reason: stuck ? `pattern_repeated_${topCount}x` : 'ok',
2700
+ repeated_pattern: stuck ? topPattern : null,
2701
+ count: topCount,
2702
+ window_size: window.length,
2703
+ threshold: flags.threshold,
2704
+ },
2705
+ raw,
2706
+ stuck ? `STUCK: "${topPattern}" repeated ${topCount}x in last ${window.length} entries` : `ok (top pattern: ${topCount}x in ${window.length})`
2707
+ );
2708
+ }
2709
+
2710
+ // =====================================================================
2711
+ // VALIDATE-PLAN COMMAND (Wave 6 — iron rule)
2712
+ // =====================================================================
2713
+
2714
+ /**
2715
+ * Iron rule: a plan must fit in one context window. Validate that a
2716
+ * PLAN.md is decomposable enough to be executed by a single agent
2717
+ * call without losing context. Fails when:
2718
+ * - File > 25kB (default; configurable via --max-bytes)
2719
+ * - Task count > 12 (default; configurable via --max-tasks)
2720
+ * - No frontmatter (must declare type at minimum)
2721
+ * - No verification criteria (no <verification>, no must_haves)
2722
+ *
2723
+ * Usage:
2724
+ * up-tools.cjs validate-plan <plan-path> [--max-bytes N] [--max-tasks N]
2725
+ *
2726
+ * Returns:
2727
+ * { pass, plan_path, bytes, tasks, frontmatter_keys, issues[],
2728
+ * suggestions[] }
2729
+ */
2730
+ function cmdValidatePlan(cwd, args, raw) {
2731
+ const flags = { maxBytes: 25 * 1024, maxTasks: 12 };
2732
+ let planPath = null;
2733
+ for (let i = 0; i < args.length; i++) {
2734
+ const a = args[i];
2735
+ if (a === '--max-bytes') flags.maxBytes = parseInt(args[++i], 10) || flags.maxBytes;
2736
+ else if (a === '--max-tasks') flags.maxTasks = parseInt(args[++i], 10) || flags.maxTasks;
2737
+ else if (!a.startsWith('--')) planPath = a;
2738
+ }
2739
+ if (!planPath) error('Usage: validate-plan <plan-path> [--max-bytes N] [--max-tasks N]');
2740
+
2741
+ const fullPath = path.isAbsolute(planPath) ? planPath : path.join(cwd, planPath);
2742
+ let content;
2743
+ try {
2744
+ content = fs.readFileSync(fullPath, 'utf-8');
2745
+ } catch {
2746
+ error(`Cannot read plan: ${fullPath}`);
2747
+ }
2748
+
2749
+ const issues = [];
2750
+ const suggestions = [];
2751
+ const bytes = Buffer.byteLength(content, 'utf-8');
2752
+
2753
+ // Frontmatter parsing
2754
+ const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
2755
+ const fmKeys = [];
2756
+ if (fmMatch) {
2757
+ for (const line of fmMatch[1].split('\n')) {
2758
+ const m = line.match(/^(\w+)\s*:/);
2759
+ if (m) fmKeys.push(m[1]);
2760
+ }
2761
+ } else {
2762
+ issues.push('no_frontmatter');
2763
+ suggestions.push('Add YAML frontmatter with at least: phase, plan, type');
2764
+ }
2765
+
2766
+ // Task count
2767
+ // Detect tasks via multiple shapes — <task> XML tags (preferred), or
2768
+ // numbered/bulleted markdown headings/items at start of line.
2769
+ const taskMatches = (content.match(/<task\b/gi) || []).length;
2770
+ const numberedHeadings = (content.match(/^#{2,4}\s+(?:Task\s+)?\d+[.:)]?\s/gmi) || []).length;
2771
+ const numberedBullets = (content.match(/^[-*]\s+\d+[.:)]\s/gm) || []).length;
2772
+ const tasks = Math.max(taskMatches, numberedHeadings, numberedBullets);
2773
+
2774
+ // Verification criteria
2775
+ const hasVerification = /<verification|must_haves|criterios|criteria/i.test(content);
2776
+
2777
+ if (bytes > flags.maxBytes) {
2778
+ issues.push(`size_exceeds_${flags.maxBytes}_bytes`);
2779
+ suggestions.push(`Plan is ${(bytes/1024).toFixed(1)}kB (limit ${(flags.maxBytes/1024).toFixed(0)}kB). Split into 2 plans by task group.`);
2780
+ }
2781
+
2782
+ if (tasks > flags.maxTasks) {
2783
+ issues.push(`task_count_exceeds_${flags.maxTasks}`);
2784
+ suggestions.push(`Plan has ${tasks} tasks (limit ${flags.maxTasks}). Split by domain (e.g., schema separate from API separate from UI).`);
2785
+ }
2786
+
2787
+ if (tasks === 0) {
2788
+ issues.push('no_tasks_detected');
2789
+ suggestions.push('No <task> tags or numbered sections found. Use <task type="auto"> or "### 1. <action>" markers.');
2790
+ }
2791
+
2792
+ if (!hasVerification) {
2793
+ issues.push('no_verification_criteria');
2794
+ suggestions.push('Add a <verification> block or must_haves frontmatter so the verifier can check completion.');
2795
+ }
2796
+
2797
+ const pass = issues.length === 0;
2798
+
2799
+ output(
2800
+ {
2801
+ pass,
2802
+ plan_path: path.relative(cwd, fullPath),
2803
+ bytes,
2804
+ tasks,
2805
+ frontmatter_keys: fmKeys,
2806
+ has_verification: hasVerification,
2807
+ issues,
2808
+ suggestions,
2809
+ limits: { max_bytes: flags.maxBytes, max_tasks: flags.maxTasks },
2810
+ },
2811
+ raw,
2812
+ pass
2813
+ ? `validate-plan: PASS (${(bytes/1024).toFixed(1)}kB, ${tasks} tasks)`
2814
+ : `validate-plan: FAIL (${issues.length} issues) — ${issues.join(', ')}`
2815
+ );
2816
+ }
2817
+
2818
+ // =====================================================================
2819
+ // SKILL-MANIFEST COMMAND (Wave 6 — per unit-type refs)
2820
+ // =====================================================================
2821
+
2822
+ const SKILL_MANIFEST = {
2823
+ // Execution agents — fokus em principles + production reqs
2824
+ 'up-executor': ['engineering-principles-compressed'],
2825
+ 'up-frontend-specialist': ['engineering-principles-compressed', 'ui-brand', 'production-requirements-compressed'],
2826
+ 'up-backend-specialist': ['engineering-principles-compressed', 'production-requirements-compressed'],
2827
+ 'up-database-specialist': ['engineering-principles-compressed', 'production-requirements-compressed'],
2828
+ 'up-devops-agent': ['production-requirements-compressed'],
2829
+ 'up-technical-writer': [],
2830
+ 'up-depurador': ['engineering-principles-compressed'],
2831
+
2832
+ // Planning agents — fokus em arquitetura + reqs
2833
+ 'up-planejador': ['engineering-principles-compressed'],
2834
+ 'up-arquiteto': ['engineering-principles-compressed', 'production-requirements-compressed'],
2835
+ 'up-product-analyst': [],
2836
+ 'up-system-designer': ['engineering-principles-compressed', 'production-requirements-compressed'],
2837
+ 'up-roteirista': [],
2838
+ 'up-pesquisador-projeto': [],
2839
+ 'up-sintetizador': [],
2840
+ 'up-mapeador-codigo': [],
2841
+ 'up-requirements-validator': ['production-requirements-compressed'],
2842
+
2843
+ // Governance — fokus em rules + rework
2844
+ 'up-execution-supervisor': ['governance-rules-compressed', 'engineering-principles-compressed', 'rework-limits-compressed'],
2845
+ 'up-verification-supervisor': ['governance-rules-compressed', 'rework-limits-compressed'],
2846
+ 'up-planning-supervisor': ['governance-rules-compressed', 'rework-limits-compressed'],
2847
+ 'up-quality-supervisor': ['governance-rules-compressed'],
2848
+ 'up-audit-supervisor': ['governance-rules-compressed'],
2849
+ 'up-product-supervisor': ['governance-rules-compressed'],
2850
+ 'up-architecture-supervisor': ['governance-rules-compressed'],
2851
+ 'up-operations-supervisor': ['governance-rules-compressed'],
2852
+ 'up-chief-engineer': ['governance-rules-compressed', 'rework-limits-compressed'],
2853
+ 'up-chief-architect': ['governance-rules-compressed'],
2854
+ 'up-chief-quality': ['governance-rules-compressed'],
2855
+ 'up-chief-operations': ['governance-rules-compressed'],
2856
+ 'up-chief-product': ['governance-rules-compressed'],
2857
+ 'up-project-ceo': ['governance-rules-compressed'],
2858
+ 'up-delivery-auditor': ['governance-rules-compressed', 'production-requirements-compressed'],
2859
+ 'up-planning-auditor': ['governance-rules-compressed'],
2860
+
2861
+ // Review & Testing — fokus em quality
2862
+ 'up-verificador': ['production-requirements-compressed'],
2863
+ 'up-blind-validator': [],
2864
+ 'up-code-reviewer': ['engineering-principles-compressed'],
2865
+ 'up-security-reviewer': ['production-requirements-compressed'],
2866
+ 'up-visual-critic': ['ui-brand'],
2867
+ 'up-exhaustive-tester': [],
2868
+ 'up-api-tester': [],
2869
+ 'up-qa-agent': [],
2870
+ 'up-auditor-ux': ['audit-ux'],
2871
+ 'up-auditor-performance': ['audit-performance'],
2872
+ 'up-auditor-modernidade': ['audit-modernidade'],
2873
+ 'up-sintetizador-melhorias': [],
2874
+ 'up-analista-codigo': ['engineering-principles-compressed'],
2875
+ 'up-pesquisador-mercado': [],
2876
+ 'up-consolidador-ideias': [],
2877
+ };
2878
+
2879
+ /**
2880
+ * Return the list of relevant reference files for a given agent.
2881
+ * Used by workflows + the context command to inject only the refs
2882
+ * relevant to the agent's role, instead of dumping all references
2883
+ * in every prompt.
2884
+ *
2885
+ * Usage:
2886
+ * up-tools.cjs skill-manifest <agent-name>
2887
+ *
2888
+ * Output: { agent, refs: ["engineering-principles-compressed", ...] }
2889
+ *
2890
+ * Use --paths to get full paths instead of names.
2891
+ */
2892
+ function cmdSkillManifest(args, raw) {
2893
+ const agent = args[0];
2894
+ if (!agent) error('Usage: skill-manifest <agent-name> [--paths]');
2895
+ const wantPaths = args.includes('--paths');
2896
+
2897
+ const refs = SKILL_MANIFEST[agent] || [];
2898
+
2899
+ if (!wantPaths) {
2900
+ output({ agent, refs, count: refs.length }, raw, refs.join('\n'));
2901
+ return;
2902
+ }
2903
+
2904
+ const homeRefDir = path.join(require('os').homedir(), '.claude', 'up', 'references');
2905
+ const localRefDir = path.join(__dirname, '..', 'references');
2906
+ const paths = refs.map(name => {
2907
+ const installed = path.join(homeRefDir, name + '.md');
2908
+ const local = path.join(localRefDir, name + '.md');
2909
+ return fs.existsSync(installed) ? installed : local;
2910
+ });
2911
+
2912
+ output({ agent, refs, paths, count: refs.length }, raw, paths.join('\n'));
2913
+ }
2914
+
2915
+ // =====================================================================
2916
+ // CLASSIFY-TASK COMMAND (Wave 5 — complexity-based routing)
2917
+ // =====================================================================
2918
+
2919
+ /**
2920
+ * Classify a plan file by complexity. Returns complexity tier
2921
+ * (simple|standard|complex) and suggested model.
2922
+ *
2923
+ * Usage:
2924
+ * up-tools.cjs classify-task <plan-path>
2925
+ *
2926
+ * Heuristics:
2927
+ * - Frontmatter: type, tasks count, brownfield flag
2928
+ * - Content patterns: integration, refactor, schema, etc.
2929
+ * - File size
2930
+ *
2931
+ * Score buckets:
2932
+ * 0-2 -> simple -> haiku
2933
+ * 3-5 -> standard -> sonnet
2934
+ * 6+ -> complex -> opus
2935
+ */
2936
+ function cmdClassifyTask(cwd, args, raw) {
2937
+ const planPath = args[0];
2938
+ if (!planPath) error('Usage: classify-task <plan-path>');
2939
+ const fullPath = path.isAbsolute(planPath) ? planPath : path.join(cwd, planPath);
2940
+
2941
+ let content;
2942
+ try {
2943
+ content = fs.readFileSync(fullPath, 'utf-8');
2944
+ } catch (err) {
2945
+ error(`Cannot read plan: ${fullPath}`);
2946
+ }
2947
+
2948
+ const result = classifyContent(content);
2949
+ result.plan_path = path.relative(cwd, fullPath);
2950
+ output(result, raw, `${result.complexity} | ${result.suggested_model} | score=${result.score} | reasons=${result.reasons.join(',')}`);
2951
+ }
2952
+
2953
+ function classifyContent(content) {
2954
+ let score = 0;
2955
+ const reasons = [];
2956
+
2957
+ // Parse frontmatter
2958
+ const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
2959
+ const fm = {};
2960
+ if (fmMatch) {
2961
+ for (const line of fmMatch[1].split('\n')) {
2962
+ const m = line.match(/^(\w+)\s*:\s*(.+)$/);
2963
+ if (m) fm[m[1]] = m[2].trim().replace(/^["']|["']$/g, '');
2964
+ }
2965
+ }
2966
+
2967
+ const type = (fm.type || '').toLowerCase();
2968
+ const brownfield = String(fm.brownfield || '').toLowerCase() === 'true';
2969
+ const lower = content.toLowerCase();
2970
+
2971
+ // Type-based scoring
2972
+ if (type === 'refactor' || type === 'migration') {
2973
+ score += 4;
2974
+ reasons.push('type=refactor/migration');
2975
+ } else if (type === 'integration') {
2976
+ score += 3;
2977
+ reasons.push('type=integration');
2978
+ } else if (type === 'frontend' || type === 'backend') {
2979
+ score += 1;
2980
+ reasons.push(`type=${type}`);
2981
+ } else if (type === 'database') {
2982
+ score += 1;
2983
+ reasons.push('type=database');
2984
+ } else if (type === 'docs' || type === 'config') {
2985
+ // simple, no points
2986
+ reasons.push(`type=${type}-simple`);
2987
+ }
2988
+
2989
+ // Brownfield = touching existing code = harder
2990
+ if (brownfield) {
2991
+ score += 2;
2992
+ reasons.push('brownfield');
2993
+ }
2994
+
2995
+ // Content patterns
2996
+ const patterns = [
2997
+ { rx: /\b(oauth|webhook|external api|third.?party|api integration)\b/i, weight: 2, name: 'external_integration' },
2998
+ { rx: /\b(refactor|rewrite|migrate|deprecate)\b/i, weight: 2, name: 'refactor_signal' },
2999
+ { rx: /\b(transaction|distributed|race condition|concurrency|locking)\b/i, weight: 2, name: 'concurrency' },
3000
+ { rx: /\b(security|auth|authentication|authorization|csrf|xss|injection)\b/i, weight: 1, name: 'security' },
3001
+ { rx: /\b(stripe|payment|billing|invoice)\b/i, weight: 2, name: 'payment' },
3002
+ { rx: /\b(real.?time|websocket|streaming|sse)\b/i, weight: 2, name: 'realtime' },
3003
+ { rx: /\b(performance|optimi[sz]e|cache|lazy|debounce)\b/i, weight: 1, name: 'performance' },
3004
+ { rx: /\b(simple|trivial|basic|crud)\b/i, weight: -1, name: 'explicitly_simple' },
3005
+ ];
3006
+
3007
+ for (const p of patterns) {
3008
+ if (p.rx.test(content)) {
3009
+ score += p.weight;
3010
+ reasons.push(p.name + (p.weight > 0 ? `+${p.weight}` : `${p.weight}`));
3011
+ }
3012
+ }
3013
+
3014
+ // Size-based
3015
+ const bytes = Buffer.byteLength(content, 'utf-8');
3016
+ if (bytes > 20000) {
3017
+ score += 2;
3018
+ reasons.push(`size>20kB(+2)`);
3019
+ } else if (bytes > 10000) {
3020
+ score += 1;
3021
+ reasons.push(`size>10kB(+1)`);
3022
+ }
3023
+
3024
+ // Task count (count <task> tags or numbered sections)
3025
+ // Detect tasks via multiple shapes — <task> XML tags (preferred), or
3026
+ // numbered/bulleted markdown headings/items at start of line.
3027
+ const taskMatches = (content.match(/<task\b/gi) || []).length;
3028
+ const numberedHeadings = (content.match(/^#{2,4}\s+(?:Task\s+)?\d+[.:)]?\s/gmi) || []).length;
3029
+ const numberedBullets = (content.match(/^[-*]\s+\d+[.:)]\s/gm) || []).length;
3030
+ const tasks = Math.max(taskMatches, numberedHeadings, numberedBullets);
3031
+ if (tasks > 10) {
3032
+ score += 2;
3033
+ reasons.push(`tasks>10(+2)`);
3034
+ } else if (tasks > 6) {
3035
+ score += 1;
3036
+ reasons.push(`tasks>6(+1)`);
3037
+ }
3038
+
3039
+ // Clamp to 0+
3040
+ if (score < 0) score = 0;
3041
+
3042
+ let complexity, model;
3043
+ if (score <= 2) {
3044
+ complexity = 'simple';
3045
+ model = 'haiku';
3046
+ } else if (score <= 5) {
3047
+ complexity = 'standard';
3048
+ model = 'sonnet';
3049
+ } else {
3050
+ complexity = 'complex';
3051
+ model = 'opus';
3052
+ }
3053
+
3054
+ return {
3055
+ complexity,
3056
+ suggested_model: model,
3057
+ score,
3058
+ reasons,
3059
+ frontmatter_type: type || null,
3060
+ brownfield,
3061
+ bytes,
3062
+ tasks_detected: tasks,
3063
+ };
3064
+ }
3065
+
3066
+ // =====================================================================
3067
+ // RESOLVE-MODEL-FOR-PLAN (Wave 5 — combined router)
3068
+ // =====================================================================
3069
+
3070
+ /**
3071
+ * Resolve which model to use for an agent given a plan context.
3072
+ * Combines per-role routing (existing) with complexity routing (new).
3073
+ *
3074
+ * Routing modes (config.modelos.routing):
3075
+ * per-role (default) -> use existing resolveAgentModel
3076
+ * complexity -> use classify-task suggestion
3077
+ * complexity-with-cap -> use classify-task BUT cap by per-role
3078
+ * (e.g., per-role says haiku, complexity says
3079
+ * opus -> use haiku since user budget says so)
3080
+ *
3081
+ * Usage:
3082
+ * up-tools.cjs resolve-model-for-plan <plan-path> <agent-name>
3083
+ */
3084
+ function cmdResolveModelForPlan(cwd, args, raw) {
3085
+ const [planPath, agentName] = args;
3086
+ if (!planPath || !agentName) error('Usage: resolve-model-for-plan <plan-path> <agent-name>');
3087
+
3088
+ const config = loadConfig(cwd);
3089
+ const routingMode = (config.modelos && config.modelos.routing) || 'per-role';
3090
+ const perRoleModel = resolveAgentModel(cwd, agentName);
3091
+
3092
+ if (routingMode === 'per-role') {
3093
+ output({ model: perRoleModel || 'default', source: 'per-role' }, raw, perRoleModel || 'default');
3094
+ return;
3095
+ }
3096
+
3097
+ // Need to classify the plan
3098
+ const fullPath = path.isAbsolute(planPath) ? planPath : path.join(cwd, planPath);
3099
+ let content;
3100
+ try {
3101
+ content = fs.readFileSync(fullPath, 'utf-8');
3102
+ } catch {
3103
+ output({ model: perRoleModel || 'default', source: 'per-role-fallback' }, raw, perRoleModel || 'default');
3104
+ return;
3105
+ }
3106
+
3107
+ const cls = classifyContent(content);
3108
+ const complexityModel = cls.suggested_model;
3109
+
3110
+ if (routingMode === 'complexity') {
3111
+ output(
3112
+ { model: complexityModel, source: 'complexity', complexity: cls.complexity, score: cls.score },
3113
+ raw,
3114
+ complexityModel
3115
+ );
3116
+ return;
3117
+ }
3118
+
3119
+ if (routingMode === 'complexity-with-cap') {
3120
+ // Order: haiku < sonnet < opus
3121
+ const order = { haiku: 1, sonnet: 2, opus: 3 };
3122
+ const finalModel =
3123
+ perRoleModel && order[perRoleModel]
3124
+ ? (order[complexityModel] <= order[perRoleModel] ? complexityModel : perRoleModel)
3125
+ : complexityModel;
3126
+ output(
3127
+ { model: finalModel, source: 'complexity-with-cap', complexity: cls.complexity, capped_by: perRoleModel },
3128
+ raw,
3129
+ finalModel
3130
+ );
3131
+ return;
3132
+ }
3133
+
3134
+ output({ model: perRoleModel || 'default', source: 'unknown-mode-fallback' }, raw, perRoleModel || 'default');
3135
+ }
3136
+
3137
+ // =====================================================================
3138
+ // ROUTING-LOG (Wave 5 — append routing decision + outcome)
3139
+ // =====================================================================
3140
+
3141
+ /**
3142
+ * Append a routing decision to .plano/governance/routing-history.log
3143
+ * for later analysis.
3144
+ *
3145
+ * Usage:
3146
+ * up-tools.cjs routing-log --plan <path> --agent <name> --model <m>
3147
+ * --complexity <c> [--score N]
3148
+ * [--outcome success|rework|abort]
3149
+ * [--rework-cycles N]
3150
+ */
3151
+ function cmdRoutingLog(cwd, args, raw) {
3152
+ const flags = { update: false };
3153
+ for (let i = 0; i < args.length; i++) {
3154
+ if (args[i] === '--update') {
3155
+ flags.update = true;
3156
+ } else if (args[i].startsWith('--')) {
3157
+ flags[args[i].replace(/^--/, '')] = args[i + 1];
3158
+ i++;
3159
+ }
3160
+ }
3161
+
3162
+ const govDir = path.join(cwd, '.plano', 'governance');
3163
+ try { fs.mkdirSync(govDir, { recursive: true }); } catch {}
3164
+ const logPath = path.join(govDir, 'routing-history.log');
3165
+
3166
+ const ts = new Date().toISOString();
3167
+ const line = [
3168
+ ts,
3169
+ flags.plan || '',
3170
+ flags.agent || '',
3171
+ flags.model || '',
3172
+ flags.complexity || '',
3173
+ flags.score || '',
3174
+ flags.outcome || 'pending',
3175
+ flags['rework-cycles'] || '0',
3176
+ ].join(' | ');
3177
+
3178
+ // --update: find last entry for (plan, agent) and rewrite it instead of appending.
3179
+ // Used to patch outcome from "pending" to success/rework/abort once a phase finishes.
3180
+ if (flags.update && flags.plan && flags.agent) {
3181
+ let existing = '';
3182
+ try {
3183
+ existing = fs.readFileSync(logPath, 'utf-8');
3184
+ } catch {
3185
+ existing = '';
3186
+ }
3187
+ const lines = existing.split('\n').filter(Boolean);
3188
+ let lastMatchIdx = -1;
3189
+ for (let i = lines.length - 1; i >= 0; i--) {
3190
+ const parts = lines[i].split('|').map(s => s.trim());
3191
+ if (parts[1] === flags.plan && parts[2] === flags.agent) {
3192
+ lastMatchIdx = i;
3193
+ break;
3194
+ }
3195
+ }
3196
+ if (lastMatchIdx >= 0) {
3197
+ lines[lastMatchIdx] = line;
3198
+ fs.writeFileSync(logPath, lines.join('\n') + '\n');
3199
+ output({ updated: true, path: path.relative(cwd, logPath), line }, raw, line);
3200
+ return;
3201
+ }
3202
+ // No match found — fall through to append
3203
+ }
3204
+
3205
+ fs.appendFileSync(logPath, line + '\n');
3206
+ output({ logged: true, path: path.relative(cwd, logPath), line }, raw, line);
3207
+ }
3208
+
3209
+ // =====================================================================
3210
+ // ANALYZE-ROUTING (Wave 5 — post-mortem of routing decisions)
3211
+ // =====================================================================
3212
+
3213
+ /**
3214
+ * Aggregate routing-history.log to suggest classifier adjustments.
3215
+ *
3216
+ * Usage:
3217
+ * up-tools.cjs analyze-routing
3218
+ *
3219
+ * Output:
3220
+ * - per-complexity success rate
3221
+ * - which complexity buckets had high rework
3222
+ * - suggestions: bump up to bigger model, or downgrade
3223
+ */
3224
+ function cmdAnalyzeRouting(cwd, raw) {
3225
+ const logPath = path.join(cwd, '.plano', 'governance', 'routing-history.log');
3226
+ let lines = [];
3227
+ try {
3228
+ lines = fs.readFileSync(logPath, 'utf-8').split('\n').filter(l => l.includes('|'));
3229
+ } catch {
3230
+ output({ entries: 0, message: 'no_history' }, raw, 'no routing history yet');
3231
+ return;
3232
+ }
3233
+
3234
+ // Dedupe by (plan, agent) — keep latest entry. Without this, --update vs
3235
+ // append confusion or duplicate logs would inflate counts.
3236
+ const latestByKey = new Map();
3237
+ for (const line of lines) {
3238
+ const parts = line.split('|').map(s => s.trim());
3239
+ const [ts, plan, agent] = parts;
3240
+ const dedupeKey = `${plan}|${agent}`;
3241
+ latestByKey.set(dedupeKey, parts); // last-write-wins
3242
+ }
3243
+
3244
+ const stats = {};
3245
+ for (const parts of latestByKey.values()) {
3246
+ const [ts, plan, agent, model, complexity, score, outcome, rework] = parts;
3247
+ const key = `${complexity}|${model}`;
3248
+ if (!stats[key]) stats[key] = { total: 0, success: 0, rework: 0, abort: 0, pending: 0, rework_cycles_total: 0 };
3249
+ stats[key].total++;
3250
+ if (outcome === 'success') stats[key].success++;
3251
+ else if (outcome === 'rework') stats[key].rework++;
3252
+ else if (outcome === 'abort') stats[key].abort++;
3253
+ else if (outcome === 'pending') stats[key].pending++;
3254
+ stats[key].rework_cycles_total += parseInt(rework || '0', 10);
3255
+ }
3256
+
3257
+ const summary = [];
3258
+ const suggestions = [];
3259
+ for (const [key, s] of Object.entries(stats)) {
3260
+ const [complexity, model] = key.split('|');
3261
+ const successRate = s.total ? (s.success / s.total * 100).toFixed(1) : '0.0';
3262
+ const avgRework = s.total ? (s.rework_cycles_total / s.total).toFixed(2) : '0';
3263
+ summary.push({ complexity, model, total: s.total, success_rate: Number(successRate), avg_rework_cycles: Number(avgRework), aborts: s.abort });
3264
+
3265
+ if (Number(successRate) < 60 && s.total >= 3) {
3266
+ suggestions.push(`${complexity}+${model}: success rate ${successRate}% (${s.total} runs) — consider bumping to bigger model`);
3267
+ }
3268
+ if (Number(avgRework) >= 1.5 && s.total >= 3) {
3269
+ suggestions.push(`${complexity}+${model}: avg ${avgRework} rework cycles — model may be undersized for this complexity`);
3270
+ }
3271
+ if (s.abort > 0 && s.total >= 3 && (s.abort / s.total) > 0.2) {
3272
+ suggestions.push(`${complexity}+${model}: ${s.abort}/${s.total} aborts — review timeout config or upgrade model`);
3273
+ }
3274
+ }
3275
+
3276
+ output(
3277
+ { entries: lines.length, summary, suggestions },
3278
+ raw,
3279
+ `analyzed ${lines.length} entries — ${summary.length} buckets, ${suggestions.length} suggestions`
3280
+ );
3281
+ }
3282
+
3283
+ // =====================================================================
3284
+ // VERIFY-STATIC COMMAND (Wave 4 — deterministic verification ladder)
3285
+ // =====================================================================
3286
+
3287
+ /**
3288
+ * Run deterministic project checks (lint, typecheck, test, audit)
3289
+ * BEFORE invoking an LLM verifier. If everything passes, the workflow
3290
+ * can skip the LLM verification step entirely. If something fails,
3291
+ * the captured output is fed into the LLM verifier as context so it
3292
+ * can focus on what actually broke instead of running the same checks.
3293
+ *
3294
+ * Usage:
3295
+ * up-tools.cjs verify-static [--lint] [--typecheck] [--test] [--audit]
3296
+ * [--all] [--skip-missing]
3297
+ *
3298
+ * If no flags given, defaults to --all (--skip-missing).
3299
+ *
3300
+ * Returns JSON:
3301
+ * {
3302
+ * overall: "pass" | "fail" | "skip",
3303
+ * checks: [
3304
+ * { name, status: "pass"|"fail"|"skip", exit_code, summary, output_path }
3305
+ * ],
3306
+ * duration_secs
3307
+ * }
3308
+ *
3309
+ * Each check's full output is written to .plano/runtime/verify-static-<check>.log
3310
+ */
3311
+ function cmdVerifyStatic(cwd, args, raw) {
3312
+ const flags = {
3313
+ lint: false,
3314
+ typecheck: false,
3315
+ test: false,
3316
+ audit: false,
3317
+ all: false,
3318
+ skipMissing: false,
3319
+ };
3320
+ for (const a of args) {
3321
+ if (a === '--lint') flags.lint = true;
3322
+ else if (a === '--typecheck') flags.typecheck = true;
3323
+ else if (a === '--test') flags.test = true;
3324
+ else if (a === '--audit') flags.audit = true;
3325
+ else if (a === '--all') flags.all = true;
3326
+ else if (a === '--skip-missing') flags.skipMissing = true;
3327
+ }
3328
+ // Default: run all, skip if script missing
3329
+ if (!flags.lint && !flags.typecheck && !flags.test && !flags.audit && !flags.all) {
3330
+ flags.all = true;
3331
+ flags.skipMissing = true;
3332
+ }
3333
+
3334
+ const { execSync } = require('child_process');
3335
+ const runtimeDir = path.join(cwd, '.plano', 'runtime');
3336
+ try { fs.mkdirSync(runtimeDir, { recursive: true }); } catch {}
3337
+
3338
+ // Read package.json scripts to know what's available
3339
+ let scripts = {};
3340
+ try {
3341
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf-8'));
3342
+ scripts = pkg.scripts || {};
3343
+ } catch {}
3344
+
3345
+ const checks = [];
3346
+ const start = Date.now();
3347
+
3348
+ function runCheck(name, cmd, condition) {
3349
+ if (!condition) return null;
3350
+ const logPath = path.join(runtimeDir, `verify-static-${name}.log`);
3351
+ try {
3352
+ const out = execSync(cmd, {
3353
+ cwd,
3354
+ encoding: 'utf-8',
3355
+ stdio: ['pipe', 'pipe', 'pipe'],
3356
+ timeout: 300000, // 5min cap per check
3357
+ });
3358
+ fs.writeFileSync(logPath, out);
3359
+ return {
3360
+ name,
3361
+ status: 'pass',
3362
+ exit_code: 0,
3363
+ summary: `${name} passed`,
3364
+ output_path: path.relative(cwd, logPath),
3365
+ };
3366
+ } catch (err) {
3367
+ const out = (err.stdout || '') + (err.stderr || '');
3368
+ fs.writeFileSync(logPath, out);
3369
+ // Extract short summary: last few non-empty lines or error counts
3370
+ const lines = out.split('\n').filter(l => l.trim());
3371
+ const tail = lines.slice(-5).join(' | ').slice(0, 300);
3372
+ return {
3373
+ name,
3374
+ status: 'fail',
3375
+ exit_code: err.status || 1,
3376
+ summary: tail || err.message,
3377
+ output_path: path.relative(cwd, logPath),
3378
+ };
3379
+ }
3380
+ }
3381
+
3382
+ function maybeSkip(name) {
3383
+ if (flags.skipMissing) {
3384
+ return { name, status: 'skip', exit_code: null, summary: 'script not found in package.json', output_path: null };
3385
+ }
3386
+ return null;
3387
+ }
3388
+
3389
+ // Lint
3390
+ if (flags.all || flags.lint) {
3391
+ if (scripts.lint) {
3392
+ checks.push(runCheck('lint', 'npm run lint --silent', true));
3393
+ } else {
3394
+ const skip = maybeSkip('lint');
3395
+ if (skip) checks.push(skip);
3396
+ }
3397
+ }
3398
+
3399
+ // Typecheck
3400
+ if (flags.all || flags.typecheck) {
3401
+ const cmd = scripts.typecheck ? 'npm run typecheck --silent' :
3402
+ scripts['type-check'] ? 'npm run type-check --silent' :
3403
+ fs.existsSync(path.join(cwd, 'tsconfig.json')) ? 'npx tsc --noEmit' : null;
3404
+ if (cmd) {
3405
+ checks.push(runCheck('typecheck', cmd, true));
3406
+ } else {
3407
+ const skip = maybeSkip('typecheck');
3408
+ if (skip) checks.push(skip);
3409
+ }
3410
+ }
3411
+
3412
+ // Test — detect runner and pick the right non-interactive flag
3413
+ if (flags.all || flags.test) {
3414
+ if (scripts.test) {
3415
+ // Read deps to detect vitest (default watch mode needs --run to exit)
3416
+ let testCmd = 'npm test --silent';
3417
+ try {
3418
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf-8'));
3419
+ const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
3420
+ if (allDeps.vitest) testCmd = 'npm test --silent -- --run';
3421
+ // jest, mocha, jasmine, ava, tap, etc. exit on completion by default
3422
+ } catch {}
3423
+ // Allow override via env: UP_VERIFY_TEST_CMD
3424
+ if (process.env.UP_VERIFY_TEST_CMD) testCmd = process.env.UP_VERIFY_TEST_CMD;
3425
+ checks.push(runCheck('test', testCmd, true));
3426
+ } else {
3427
+ const skip = maybeSkip('test');
3428
+ if (skip) checks.push(skip);
3429
+ }
3430
+ }
3431
+
3432
+ // Audit
3433
+ if (flags.all || flags.audit) {
3434
+ // npm audit always runs if package.json exists
3435
+ if (Object.keys(scripts).length > 0 || fs.existsSync(path.join(cwd, 'package.json'))) {
3436
+ checks.push(runCheck('audit', 'npm audit --audit-level=high --silent', true));
3437
+ } else {
3438
+ const skip = maybeSkip('audit');
3439
+ if (skip) checks.push(skip);
3440
+ }
3441
+ }
3442
+
3443
+ const duration = Math.round((Date.now() - start) / 1000);
3444
+
3445
+ const failed = checks.filter(c => c.status === 'fail');
3446
+ const passed = checks.filter(c => c.status === 'pass');
3447
+ const skipped = checks.filter(c => c.status === 'skip');
3448
+
3449
+ let overall;
3450
+ if (failed.length > 0) overall = 'fail';
3451
+ else if (passed.length > 0) overall = 'pass';
3452
+ else overall = 'skip';
3453
+
3454
+ const result = {
3455
+ overall,
3456
+ checks,
3457
+ duration_secs: duration,
3458
+ counts: { passed: passed.length, failed: failed.length, skipped: skipped.length },
3459
+ };
3460
+
3461
+ output(
3462
+ result,
3463
+ raw,
3464
+ `verify-static: ${overall} (${passed.length} passed, ${failed.length} failed, ${skipped.length} skipped, ${duration}s)`
3465
+ );
3466
+ }
3467
+
3468
+ // =====================================================================
3469
+ // CONTEXT COMMAND (pre-inline helper — Wave 2)
3470
+ // =====================================================================
3471
+
3472
+ /**
3473
+ * Build a pre-inlined prompt fragment containing the contents of
3474
+ * requested files wrapped in XML tags. Used by workflows to avoid
3475
+ * forcing agents to make Read tool calls for content the orchestrator
3476
+ * already has on disk.
3477
+ *
3478
+ * Usage:
3479
+ * up-tools.cjs context --plan <path> [--state] [--config]
3480
+ * [--requirements] [--governance]
3481
+ * [--engineering-principles] [--max-bytes N]
3482
+ *
3483
+ * Outputs a single string of XML blocks to stdout. Designed to be
3484
+ * captured into a bash variable and injected into a Task/Agent prompt.
3485
+ *
3486
+ * Example:
3487
+ * CTX=$(up-tools.cjs context --plan $PLAN --state --config)
3488
+ * # then in spawn prompt:
3489
+ * # <prompt_context>
3490
+ * # {CTX}
3491
+ * # </prompt_context>
3492
+ */
3493
+ function cmdContext(cwd, args, raw) {
3494
+ const flags = {
3495
+ plan: null,
3496
+ state: false,
3497
+ config: false,
3498
+ requirements: null,
3499
+ governance: false,
3500
+ engineeringPrinciples: false,
3501
+ manifest: null,
3502
+ maxBytes: 50 * 1024, // 50kB hard cap per file by default
3503
+ };
3504
+
3505
+ for (let i = 0; i < args.length; i++) {
3506
+ const a = args[i];
3507
+ if (a === '--plan') flags.plan = args[++i];
3508
+ else if (a === '--state') flags.state = true;
3509
+ else if (a === '--config') flags.config = true;
3510
+ else if (a === '--requirements') flags.requirements = args[++i] || true;
3511
+ else if (a === '--governance') flags.governance = true;
3512
+ else if (a === '--engineering-principles' || a === '--principles') flags.engineeringPrinciples = true;
3513
+ else if (a === '--manifest') flags.manifest = args[++i];
3514
+ else if (a === '--max-bytes') flags.maxBytes = parseInt(args[++i], 10) || flags.maxBytes;
3515
+ }
3516
+
3517
+ function readCapped(filePath, maxBytes) {
3518
+ try {
3519
+ const stat = fs.statSync(filePath);
3520
+ if (stat.size <= maxBytes) {
3521
+ return fs.readFileSync(filePath, 'utf-8');
3522
+ }
3523
+ const fd = fs.openSync(filePath, 'r');
3524
+ const buf = Buffer.alloc(maxBytes);
3525
+ fs.readSync(fd, buf, 0, maxBytes, 0);
3526
+ fs.closeSync(fd);
3527
+ return buf.toString('utf-8') + `\n\n[TRUNCATED at ${maxBytes} bytes — full file at ${filePath}]\n`;
3528
+ } catch {
3529
+ return null;
3530
+ }
3531
+ }
3532
+
3533
+ const blocks = [];
3534
+
3535
+ if (flags.plan) {
3536
+ const planPath = path.isAbsolute(flags.plan) ? flags.plan : path.join(cwd, flags.plan);
3537
+ const content = readCapped(planPath, flags.maxBytes);
3538
+ if (content !== null) {
3539
+ blocks.push(`<plan_inlined path="${flags.plan}">\n${content}\n</plan_inlined>`);
3540
+ }
3541
+ }
3542
+
3543
+ if (flags.state) {
3544
+ const statePath = path.join(cwd, '.plano', 'STATE.md');
3545
+ const content = readCapped(statePath, flags.maxBytes);
3546
+ if (content !== null) {
3547
+ blocks.push(`<state_inlined path=".plano/STATE.md">\n${content}\n</state_inlined>`);
3548
+ }
3549
+ }
3550
+
3551
+ if (flags.config) {
3552
+ const configPath = path.join(cwd, '.plano', 'config.json');
3553
+ const content = readCapped(configPath, flags.maxBytes);
3554
+ if (content !== null) {
3555
+ blocks.push(`<config_inlined path=".plano/config.json">\n${content}\n</config_inlined>`);
3556
+ }
3557
+ }
3558
+
3559
+ if (flags.requirements) {
3560
+ // If a phase number was passed as value, look for REQUIREMENTS-SLICE in that phase dir
3561
+ if (typeof flags.requirements === 'string') {
3562
+ const phasesDir = path.join(cwd, '.plano', 'fases');
3563
+ try {
3564
+ const dir = fs.readdirSync(phasesDir).find(d => d.startsWith(flags.requirements));
3565
+ if (dir) {
3566
+ const slicePath = path.join(phasesDir, dir, 'REQUIREMENTS-SLICE.md');
3567
+ const content = readCapped(slicePath, flags.maxBytes);
3568
+ if (content !== null) {
3569
+ blocks.push(`<requirements_slice_inlined path="${path.relative(cwd, slicePath)}">\n${content}\n</requirements_slice_inlined>`);
3570
+ }
3571
+ }
3572
+ } catch {}
3573
+ } else {
3574
+ // --requirements alone -> full REQUIREMENTS.md
3575
+ const reqPath = path.join(cwd, '.plano', 'REQUIREMENTS.md');
3576
+ const content = readCapped(reqPath, flags.maxBytes);
3577
+ if (content !== null) {
3578
+ blocks.push(`<requirements_inlined path=".plano/REQUIREMENTS.md">\n${content}\n</requirements_inlined>`);
3579
+ }
3580
+ }
3581
+ }
3582
+
3583
+ if (flags.governance) {
3584
+ const govPath = path.join(__dirname, '..', 'references', 'governance-rules-compressed.md');
3585
+ // also try the runtime-installed location
3586
+ const installedGovPath = path.join(require('os').homedir(), '.claude', 'up', 'references', 'governance-rules-compressed.md');
3587
+ let govContent = readCapped(govPath, flags.maxBytes) || readCapped(installedGovPath, flags.maxBytes);
3588
+ if (govContent !== null) {
3589
+ blocks.push(`<governance_compressed>\n${govContent}\n</governance_compressed>`);
3590
+ }
3591
+ }
3592
+
3593
+ if (flags.engineeringPrinciples) {
3594
+ const epPath = path.join(__dirname, '..', 'references', 'engineering-principles-compressed.md');
3595
+ const installedEpPath = path.join(require('os').homedir(), '.claude', 'up', 'references', 'engineering-principles-compressed.md');
3596
+ let epContent = readCapped(epPath, flags.maxBytes) || readCapped(installedEpPath, flags.maxBytes);
3597
+ if (epContent !== null) {
3598
+ blocks.push(`<engineering_principles_compressed>\n${epContent}\n</engineering_principles_compressed>`);
3599
+ }
3600
+ }
3601
+
3602
+ // Wave 6 — skill manifest: include only refs relevant to the agent's role
3603
+ if (flags.manifest) {
3604
+ const refs = SKILL_MANIFEST[flags.manifest] || [];
3605
+ const homeRefDir = path.join(require('os').homedir(), '.claude', 'up', 'references');
3606
+ const localRefDir = path.join(__dirname, '..', 'references');
3607
+ for (const refName of refs) {
3608
+ // Skip if the ref was already added via a dedicated flag
3609
+ if (refName === 'engineering-principles-compressed' && flags.engineeringPrinciples) continue;
3610
+ if (refName === 'governance-rules-compressed' && flags.governance) continue;
3611
+
3612
+ const installed = path.join(homeRefDir, refName + '.md');
3613
+ const local = path.join(localRefDir, refName + '.md');
3614
+ const refPath = fs.existsSync(installed) ? installed : local;
3615
+ const refContent = readCapped(refPath, flags.maxBytes);
3616
+ if (refContent !== null) {
3617
+ // XML-safe tag name from the file name
3618
+ const tagName = refName.replace(/-/g, '_');
3619
+ blocks.push(`<manifest_${tagName}>\n${refContent}\n</manifest_${tagName}>`);
3620
+ }
3621
+ }
3622
+ }
3623
+
3624
+ const out = blocks.join('\n\n');
3625
+ if (raw) {
3626
+ process.stdout.write(out);
3627
+ } else {
3628
+ output({ context_blocks: blocks.length, bytes: Buffer.byteLength(out, 'utf-8'), content: out }, raw, out);
3629
+ }
3630
+ }
3631
+
3632
+ // =====================================================================
3633
+ // STATUS COMMAND (headless aggregator — no LLM needed)
3634
+ // =====================================================================
3635
+
3636
+ function cmdStatus(cwd, raw) {
3637
+ // Aggregates progress + budget + current phase + governance counts in one shot.
3638
+ // Designed for /up:progresso, /up:saude, dashboards, or external monitoring.
3639
+ const phasesDir = path.join(cwd, '.plano', 'fases');
3640
+ const phases = [];
3641
+ let totalPlans = 0;
3642
+ let totalSummaries = 0;
3643
+ let currentPhase = null;
3644
+
3645
+ try {
3646
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
3647
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
3648
+ for (const dir of dirs) {
3649
+ const dm = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
3650
+ const phaseNum = dm ? dm[1] : dir;
3651
+ const phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
3652
+ const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
3653
+ const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
3654
+ const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
3655
+ const verifications = phaseFiles.filter(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md').length;
3656
+ totalPlans += plans;
3657
+ totalSummaries += summaries;
3658
+ let status;
3659
+ if (plans === 0) status = 'pending';
3660
+ else if (summaries >= plans) status = 'complete';
3661
+ else if (summaries > 0) status = 'in_progress';
3662
+ else status = 'planned';
3663
+ const phaseEntry = { number: phaseNum, name: phaseName, plans, summaries, verifications, status };
3664
+ phases.push(phaseEntry);
3665
+ if (!currentPhase && (status === 'in_progress' || status === 'planned')) {
3666
+ currentPhase = phaseEntry;
3667
+ }
3668
+ }
3669
+ } catch {}
3670
+
3671
+ const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
3672
+
3673
+ // Governance counts
3674
+ const approvalsLog = path.join(cwd, '.plano', 'governance', 'approvals.log');
3675
+ const technicalDebtLog = path.join(cwd, '.plano', 'governance', 'technical-debt.log');
3676
+ let approvalCount = 0;
3677
+ let technicalDebtCount = 0;
3678
+ try {
3679
+ if (fs.existsSync(approvalsLog)) {
3680
+ approvalCount = fs.readFileSync(approvalsLog, 'utf-8').split('\n').filter(l => l.includes('|')).length;
3681
+ }
3682
+ if (fs.existsSync(technicalDebtLog)) {
3683
+ technicalDebtCount = fs.readFileSync(technicalDebtLog, 'utf-8').split('\n').filter(l => l.includes('|')).length;
3684
+ }
3685
+ } catch {}
3686
+
3687
+ // Budget (best-effort; never blocks status)
3688
+ let budget = { spend_usd: 0, ceiling_usd: null, status: 'no_data' };
3689
+ try {
3690
+ const { execSync } = require('child_process');
3691
+ const instrumentPath = path.join(__dirname, 'up-instrument.cjs');
3692
+ const out = execSync(`node "${instrumentPath}" report --json --all-sessions`, {
3693
+ cwd,
3694
+ encoding: 'utf-8',
3695
+ stdio: ['pipe', 'pipe', 'pipe'],
3696
+ timeout: 30000,
3697
+ });
3698
+ const report = JSON.parse(out);
3699
+ const config = loadConfig(cwd);
3700
+ const ceiling = config.budget_ceiling;
3701
+ const spend = report.total_cost_usd ?? 0;
3702
+ let bStatus = 'no_ceiling';
3703
+ let pct = null;
3704
+ if (ceiling && ceiling > 0) {
3705
+ pct = Number(((spend / ceiling) * 100).toFixed(1));
3706
+ if (pct >= 100) bStatus = 'over_budget';
3707
+ else if (pct >= 90) bStatus = 'critical_90';
3708
+ else if (pct >= 75) bStatus = 'warning_75';
3709
+ else if (pct >= 50) bStatus = 'warning_50';
3710
+ else bStatus = 'under_budget';
3711
+ }
3712
+ budget = {
3713
+ spend_usd: Number(spend.toFixed(4)),
3714
+ ceiling_usd: ceiling,
3715
+ percent_used: pct,
3716
+ status: bStatus,
3717
+ };
3718
+ } catch {}
3719
+
3720
+ const config = loadConfig(cwd);
3721
+ const result = {
3722
+ cwd,
3723
+ phases,
3724
+ progress: {
3725
+ total_plans: totalPlans,
3726
+ total_summaries: totalSummaries,
3727
+ percent,
3728
+ },
3729
+ current_phase: currentPhase,
3730
+ governance: {
3731
+ approvals_logged: approvalCount,
3732
+ technical_debt_entries: technicalDebtCount,
3733
+ },
3734
+ budget,
3735
+ config: {
3736
+ modo: config.modo,
3737
+ paralelizacao: config.paralelizacao,
3738
+ auto_advance: config.auto_advance,
3739
+ modelos_preset: config.modelos?.preset ?? 'runtime',
3740
+ instrumentation_enabled: config.instrumentation?.enabled ?? false,
3741
+ },
3742
+ timestamp: new Date().toISOString(),
3743
+ };
3744
+
3745
+ output(result, raw, JSON.stringify(result, null, 2));
3746
+ }
3747
+
2410
3748
  // =====================================================================
2411
3749
  // PROGRESS COMMAND
2412
3750
  // =====================================================================