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.
- package/package.json +9 -9
- package/{agents → up/agents}/up-architecture-supervisor.md +1 -1
- package/{agents → up/agents}/up-audit-supervisor.md +1 -1
- package/{agents → up/agents}/up-backend-specialist.md +13 -3
- package/{agents → up/agents}/up-chief-architect.md +1 -1
- package/{agents → up/agents}/up-chief-engineer.md +1 -1
- package/{agents → up/agents}/up-chief-operations.md +2 -2
- package/{agents → up/agents}/up-chief-product.md +1 -1
- package/{agents → up/agents}/up-chief-quality.md +1 -1
- package/{agents → up/agents}/up-code-reviewer.md +2 -2
- package/{agents → up/agents}/up-database-specialist.md +13 -3
- package/{agents → up/agents}/up-execution-supervisor.md +4 -4
- package/{agents → up/agents}/up-executor.md +107 -9
- package/{agents → up/agents}/up-frontend-specialist.md +13 -3
- package/{agents → up/agents}/up-operations-supervisor.md +1 -1
- package/{agents → up/agents}/up-planejador.md +17 -1
- package/{agents → up/agents}/up-planning-supervisor.md +3 -3
- package/{agents → up/agents}/up-product-supervisor.md +1 -1
- package/{agents → up/agents}/up-project-ceo.md +2 -2
- package/{agents → up/agents}/up-quality-supervisor.md +1 -1
- package/{agents → up/agents}/up-system-designer.md +2 -2
- package/{agents → up/agents}/up-verification-supervisor.md +1 -1
- package/{agents → up/agents}/up-visual-critic.md +1 -1
- package/{bin → up/bin}/lib/core.cjs +132 -0
- package/{bin → up/bin}/up-tools.cjs +1341 -3
- package/up/commands/adicionar-fase.md +47 -0
- package/up/commands/configurar.md +219 -0
- package/{commands → up/commands}/depurar.md +1 -1
- package/{commands → up/commands}/planejar-fase.md +4 -2
- package/up/templates/config.json +8 -0
- package/up/workflows/build.md +650 -0
- package/{workflows → up/workflows}/builder.md +766 -35
- package/{workflows → up/workflows}/executar-plano.md +69 -0
- package/{workflows → up/workflows}/governance.md +3 -3
- package/{workflows → up/workflows}/plan.md +214 -29
- package/{workflows → up/workflows}/planejar-fase.md +63 -5
- package/commands/adicionar-fase.md +0 -33
- package/commands/configurar.md +0 -106
- package/templates/config.json +0 -6
- package/workflows/build.md +0 -431
- /package/{agents → up/agents}/up-analista-codigo.md +0 -0
- /package/{agents → up/agents}/up-api-tester.md +0 -0
- /package/{agents → up/agents}/up-arquiteto.md +0 -0
- /package/{agents → up/agents}/up-auditor-modernidade.md +0 -0
- /package/{agents → up/agents}/up-auditor-performance.md +0 -0
- /package/{agents → up/agents}/up-auditor-ux.md +0 -0
- /package/{agents → up/agents}/up-blind-validator.md +0 -0
- /package/{agents → up/agents}/up-clone-crawler.md +0 -0
- /package/{agents → up/agents}/up-clone-design-extractor.md +0 -0
- /package/{agents → up/agents}/up-clone-feature-mapper.md +0 -0
- /package/{agents → up/agents}/up-clone-prd-writer.md +0 -0
- /package/{agents → up/agents}/up-clone-verifier.md +0 -0
- /package/{agents → up/agents}/up-consolidador-ideias.md +0 -0
- /package/{agents → up/agents}/up-delivery-auditor.md +0 -0
- /package/{agents → up/agents}/up-depurador.md +0 -0
- /package/{agents → up/agents}/up-devops-agent.md +0 -0
- /package/{agents → up/agents}/up-exhaustive-tester.md +0 -0
- /package/{agents → up/agents}/up-mapeador-codigo.md +0 -0
- /package/{agents → up/agents}/up-pesquisador-mercado.md +0 -0
- /package/{agents → up/agents}/up-pesquisador-projeto.md +0 -0
- /package/{agents → up/agents}/up-planning-auditor.md +0 -0
- /package/{agents → up/agents}/up-product-analyst.md +0 -0
- /package/{agents → up/agents}/up-qa-agent.md +0 -0
- /package/{agents → up/agents}/up-requirements-validator.md +0 -0
- /package/{agents → up/agents}/up-roteirista.md +0 -0
- /package/{agents → up/agents}/up-security-reviewer.md +0 -0
- /package/{agents → up/agents}/up-sintetizador-melhorias.md +0 -0
- /package/{agents → up/agents}/up-sintetizador.md +0 -0
- /package/{agents → up/agents}/up-technical-writer.md +0 -0
- /package/{agents → up/agents}/up-verificador.md +0 -0
- /package/{bin → up/bin}/install.js +0 -0
- /package/{bin → up/bin}/up-instrument.cjs +0 -0
- /package/{commands → up/commands}/adicionar-testes.md +0 -0
- /package/{commands → up/commands}/ajuda.md +0 -0
- /package/{commands → up/commands}/atualizar.md +0 -0
- /package/{commands → up/commands}/build.md +0 -0
- /package/{commands → up/commands}/clone-builder.md +0 -0
- /package/{commands → up/commands}/custos.md +0 -0
- /package/{commands → up/commands}/dashboard.md +0 -0
- /package/{commands → up/commands}/discutir-fase.md +0 -0
- /package/{commands → up/commands}/executar-fase.md +0 -0
- /package/{commands → up/commands}/ideias.md +0 -0
- /package/{commands → up/commands}/iniciar.md +0 -0
- /package/{commands → up/commands}/mapear-codigo.md +0 -0
- /package/{commands → up/commands}/melhorias.md +0 -0
- /package/{commands → up/commands}/mobile-first.md +0 -0
- /package/{commands → up/commands}/modo-builder.md +0 -0
- /package/{commands → up/commands}/novo-projeto.md +0 -0
- /package/{commands → up/commands}/onboard.md +0 -0
- /package/{commands → up/commands}/pausar.md +0 -0
- /package/{commands → up/commands}/plan.md +0 -0
- /package/{commands → up/commands}/progresso.md +0 -0
- /package/{commands → up/commands}/rapido.md +0 -0
- /package/{commands → up/commands}/remover-fase.md +0 -0
- /package/{commands → up/commands}/resetar.md +0 -0
- /package/{commands → up/commands}/retomar.md +0 -0
- /package/{commands → up/commands}/saude.md +0 -0
- /package/{commands → up/commands}/testar.md +0 -0
- /package/{commands → up/commands}/ux-tester.md +0 -0
- /package/{commands → up/commands}/verificar-trabalho.md +0 -0
- /package/{hooks → up/hooks}/up-context-monitor.js +0 -0
- /package/{hooks → up/hooks}/up-statusline.js +0 -0
- /package/{references → up/references}/audit-modernidade.md +0 -0
- /package/{references → up/references}/audit-performance.md +0 -0
- /package/{references → up/references}/audit-ux.md +0 -0
- /package/{references → up/references}/blueprints/audit.md +0 -0
- /package/{references → up/references}/blueprints/booking.md +0 -0
- /package/{references → up/references}/blueprints/community.md +0 -0
- /package/{references → up/references}/blueprints/crm.md +0 -0
- /package/{references → up/references}/blueprints/dashboard.md +0 -0
- /package/{references → up/references}/blueprints/data-management.md +0 -0
- /package/{references → up/references}/blueprints/ecommerce.md +0 -0
- /package/{references → up/references}/blueprints/marketplace.md +0 -0
- /package/{references → up/references}/blueprints/notifications.md +0 -0
- /package/{references → up/references}/blueprints/saas-users.md +0 -0
- /package/{references → up/references}/blueprints/settings.md +0 -0
- /package/{references → up/references}/checkpoints.md +0 -0
- /package/{references → up/references}/engineering-principles-compressed.md +0 -0
- /package/{references → up/references}/engineering-principles.md +0 -0
- /package/{references → up/references}/git-integration.md +0 -0
- /package/{references → up/references}/governance-rules-compressed.md +0 -0
- /package/{references → up/references}/governance-rules.md +0 -0
- /package/{references → up/references}/production-requirements-compressed.md +0 -0
- /package/{references → up/references}/production-requirements.md +0 -0
- /package/{references → up/references}/questioning.md +0 -0
- /package/{references → up/references}/rework-limits-compressed.md +0 -0
- /package/{references → up/references}/rework-limits.md +0 -0
- /package/{references → up/references}/severity-levels.md +0 -0
- /package/{references → up/references}/state-persistence.md +0 -0
- /package/{references → up/references}/ui-brand.md +0 -0
- /package/{templates → up/templates}/audit-plan.md +0 -0
- /package/{templates → up/templates}/audit-report.md +0 -0
- /package/{templates → up/templates}/builder-defaults.md +0 -0
- /package/{templates → up/templates}/checklist.md +0 -0
- /package/{templates → up/templates}/continue-here.md +0 -0
- /package/{templates → up/templates}/delivery.md +0 -0
- /package/{templates → up/templates}/design-tokens.md +0 -0
- /package/{templates → up/templates}/owner-profile.md +0 -0
- /package/{templates → up/templates}/owner.md +0 -0
- /package/{templates → up/templates}/pending.md +0 -0
- /package/{templates → up/templates}/plan-ready.md +0 -0
- /package/{templates → up/templates}/project.md +0 -0
- /package/{templates → up/templates}/report.md +0 -0
- /package/{templates → up/templates}/requirements.md +0 -0
- /package/{templates → up/templates}/roadmap.md +0 -0
- /package/{templates → up/templates}/state.md +0 -0
- /package/{templates → up/templates}/suggestion.md +0 -0
- /package/{templates → up/templates}/summary.md +0 -0
- /package/{workflows → up/workflows}/adicionar-fase.md +0 -0
- /package/{workflows → up/workflows}/builder-e2e.md +0 -0
- /package/{workflows → up/workflows}/ceo-intake.md +0 -0
- /package/{workflows → up/workflows}/ceo-updates.md +0 -0
- /package/{workflows → up/workflows}/clone-builder.md +0 -0
- /package/{workflows → up/workflows}/dcrv.md +0 -0
- /package/{workflows → up/workflows}/discutir-fase.md +0 -0
- /package/{workflows → up/workflows}/executar-fase.md +0 -0
- /package/{workflows → up/workflows}/ideias.md +0 -0
- /package/{workflows → up/workflows}/iniciar.md +0 -0
- /package/{workflows → up/workflows}/mapear-codigo.md +0 -0
- /package/{workflows → up/workflows}/melhorias.md +0 -0
- /package/{workflows → up/workflows}/mobile-first.md +0 -0
- /package/{workflows → up/workflows}/novo-projeto.md +0 -0
- /package/{workflows → up/workflows}/onboarding.md +0 -0
- /package/{workflows → up/workflows}/pausar.md +0 -0
- /package/{workflows → up/workflows}/progresso.md +0 -0
- /package/{workflows → up/workflows}/rapido.md +0 -0
- /package/{workflows → up/workflows}/remover-fase.md +0 -0
- /package/{workflows → up/workflows}/resetar.md +0 -0
- /package/{workflows → up/workflows}/retomar.md +0 -0
- /package/{workflows → up/workflows}/ux-tester.md +0 -0
- /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,
|
|
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
|
// =====================================================================
|