@luanpdd/kit-mcp 1.6.0 → 1.7.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.
@@ -234,43 +234,15 @@ Se "Executar discuss-phase primeiro":
234
234
 
235
235
  ## 5. Tratar Pesquisa
236
236
 
237
- **Pular se:** flag `--gaps` ou flag `--skip-research` ou flag `--reviews`.
237
+ **Pular se:** `--gaps`, `--skip-research`, ou `--reviews`.
238
238
 
239
- **Se `has_research` for true (do init) E sem flag `--research`:** Usar existente, pular para o passo 6.
239
+ **Se `has_research` true e sem `--research`:** usar existente, ir pra passo 6.
240
240
 
241
- **Se RESEARCH.md ausente OU flag `--research`:**
241
+ **Se ausente OR `--research`:** sem flag explícita e sem `--auto`, perguntar (AskUserQuestion ou lista numerada se TEXT_MODE):
242
+ - "Pesquisar primeiro (Recomendado)" — investiga domínio/padrões/deps antes de planejar. Melhor pra features novas, integrações novas, mudanças arquiteturais.
243
+ - "Pular pesquisa" — planeja direto do contexto. Melhor pra bug fix, refactor simples, tarefas bem compreendidas.
242
244
 
243
- **Se sem flag explícita (`--research` ou `--skip-research`) e não `--auto`:**
244
- Perguntar ao usuário se deseja pesquisar, com uma recomendação contextual baseada na fase:
245
-
246
- Se `TEXT_MODE` for true, apresentar como lista numerada de texto simples:
247
- ```
248
- Pesquisar antes de planejar a Fase {X}: {phase_name}?
249
-
250
- 1. Pesquisar primeiro (Recomendado) — Investigar domínio, padrões e dependências antes do planejamento. Melhor para novas funcionalidades, integrações desconhecidas ou mudanças arquiteturais.
251
- 2. Pular pesquisa — Planejar diretamente a partir do contexto e requisitos. Melhor para correções de bugs, refatorações simples ou tarefas bem compreendidas.
252
-
253
- Digite o número:
254
- ```
255
-
256
- Caso contrário usar AskUserQuestion:
257
- ```
258
- AskUserQuestion([
259
- {
260
- question: "Pesquisar antes de planejar a Fase {X}: {phase_name}?",
261
- header: "Pesquisa",
262
- multiSelect: false,
263
- options: [
264
- { label: "Pesquisar primeiro (Recomendado)", description: "Investigar domínio, padrões e dependências antes do planejamento. Melhor para novas funcionalidades, integrações desconhecidas ou mudanças arquiteturais." },
265
- { label: "Pular pesquisa", description: "Planejar diretamente a partir do contexto e requisitos. Melhor para correções de bugs, refatorações simples ou tarefas bem compreendidas." }
266
- ]
267
- }
268
- ])
269
- ```
270
-
271
- Se o usuário selecionar "Pular pesquisa": pular para o passo 6.
272
-
273
- **Se `--auto` e `research_enabled` for false:** Pular pesquisa silenciosamente (preserva comportamento automatizado).
245
+ Se "Pular": passo 6. Se `--auto` e `research_enabled=false`: pular silenciosamente.
274
246
 
275
247
  Exibir banner:
276
248
  ```
@@ -365,51 +337,16 @@ test -f "${PHASE_DIR}/${PADDED_PHASE}-VALIDATION.md" && echo "VALIDATION_CREATED
365
337
  > Pular se `workflow.ui_phase` for explicitamente `false` E `workflow.ui_safety_gate` for explicitamente `false` em `.planning/config.json`. Se as chaves estiverem ausentes, tratar como habilitado.
366
338
 
367
339
  ```bash
368
- UI_PHASE_CFG=$(node "./.claude/framework/bin/tools.cjs" config-get workflow.ui_phase 2>/dev/null || echo "true")
369
- UI_GATE_CFG=$(node "./.claude/framework/bin/tools.cjs" config-get workflow.ui_safety_gate 2>/dev/null || echo "true")
370
- ```
371
-
372
- **Se ambos forem `false`:** Pular para o passo 6.
373
-
374
- Verificar se a fase tem indicadores de frontend:
375
-
376
- ```bash
377
- PHASE_SECTION=$(node "./.claude/framework/bin/tools.cjs" roadmap get-phase "${PHASE}" 2>/dev/null)
378
- echo "$PHASE_SECTION" | grep -iE "UI|interface|frontend|component|layout|page|screen|view|form|dashboard|widget" > /dev/null 2>&1
379
- HAS_UI=$?
380
- ```
381
-
382
- **Se `HAS_UI` for 0 (indicadores de frontend encontrados):**
383
-
384
- Verificar UI-SPEC existente:
385
- ```bash
386
- UI_SPEC_FILE=$(ls "${PHASE_DIR}"/*-UI-SPEC.md 2>/dev/null | head -1)
387
- ```
388
-
389
- **Se UI-SPEC.md encontrado:** Definir `UI_SPEC_PATH=$UI_SPEC_FILE`. Exibir: `Usando contrato de design de UI: ${UI_SPEC_PATH}`
390
-
391
- **Se UI-SPEC.md ausente E `UI_GATE_CFG` for `true`:**
392
-
393
- Se `TEXT_MODE` for true, apresentar como lista numerada de texto simples:
394
- ```
395
- A Fase {N} tem indicadores de frontend mas sem UI-SPEC.md. Gerar um contrato de design antes do planejamento?
340
+ `UI_PHASE_CFG` / `UI_GATE_CFG` (default true). Se ambos false, pular pra passo 6.
396
341
 
397
- 1. Gerar UI-SPEC primeiro Execute /fase-ui {N} então re-execute /planejar-fase {N}
398
- 2. Continuar sem UI-SPEC
399
- 3. Não é uma fase de frontend
342
+ **Detecção:** grep `-iE "UI|interface|frontend|component|layout|page|screen|view|form|dashboard|widget"` na descrição da fase. Se sem match, pular silenciosamente.
400
343
 
401
- Digite o número:
402
- ```
403
-
404
- Caso contrário usar AskUserQuestion:
405
- - header: "Contrato de Design de UI"
406
- - question: "A Fase {N} tem indicadores de frontend mas sem UI-SPEC.md. Gerar um contrato de design antes do planejamento?"
407
- - options:
408
- - "Gerar UI-SPEC primeiro" → Exibir: "Execute `/fase-ui {N} ${WS}` então re-execute `/planejar-fase {N} ${WS}`". Sair do workflow.
409
- - "Continuar sem UI-SPEC" → Continuar para o passo 6.
410
- - "Não é uma fase de frontend" → Continuar para o passo 6.
411
-
412
- **Se `HAS_UI` for 1 (sem indicadores de frontend):** Pular silenciosamente para o passo 6.
344
+ **Se match encontrado:**
345
+ - UI-SPEC.md existe → usar (`UI_SPEC_PATH`); exibir confirmação
346
+ - UI-SPEC.md ausente E `UI_GATE_CFG=true` → AskUserQuestion (ou lista numerada se TEXT_MODE):
347
+ - "Gerar UI-SPEC primeiro" → exibir `/fase-ui {N} ${WS}` e sair do workflow
348
+ - "Continuar sem UI-SPEC" passo 6
349
+ - "Não é frontend" passo 6
413
350
 
414
351
  ## 6. Verificar Planos Existentes
415
352
 
@@ -511,28 +448,13 @@ Output consumed by /execute-phase. Plans need:
511
448
 
512
449
  Every task MUST include these fields — they are NOT optional:
513
450
 
514
- 1. **`<read_first>`** — Files the executor MUST read before touching anything. Always include:
515
- - The file being modified (so executor sees current state, not assumptions)
516
- - Any "source of truth" file referenced in CONTEXT.md (reference implementations, existing patterns, config files, schemas)
517
- - Any file whose patterns, signatures, types, or conventions must be replicated or respected
518
-
519
- 2. **`<acceptance_criteria>`** — Verifiable conditions that prove the task was done correctly. Rules:
520
- - Every criterion must be checkable with grep, file read, test command, or CLI output
521
- - NEVER use subjective language ("looks correct", "properly configured", "consistent with")
522
- - ALWAYS include exact strings, patterns, values, or command outputs that must be present
523
- - Examples:
524
- - Code: `auth.py contains def verify_token(` / `test_auth.py exits 0`
525
- - Config: `.env.example contains DATABASE_URL=` / `Dockerfile contains HEALTHCHECK`
526
- - Docs: `README.md contains '## Installation'` / `API.md lists all endpoints`
527
- - Infra: `deploy.yml has rollback step` / `docker-compose.yml has healthcheck for db`
528
-
529
- 3. **`<action>`** — Must include CONCRETE values, not references. Rules:
530
- - NEVER say "align X with Y", "match X to Y", "update to be consistent" without specifying the exact target state
531
- - ALWAYS include the actual values: config keys, function signatures, SQL statements, class names, import paths, env vars, etc.
532
- - If CONTEXT.md has a comparison table or expected values, copy them into the action verbatim
533
- - The executor should be able to complete the task from the action text alone, without needing to read CONTEXT.md or reference files (read_first is for verification, not discovery)
534
-
535
- **Why this matters:** Executor agents work from the plan text. Vague instructions like "update the config to match production" produce shallow one-line changes. Concrete instructions like "add DATABASE_URL=postgresql://... , set POOL_SIZE=20, add REDIS_URL=redis://..." produce complete work. The cost of verbose plans is far less than the cost of re-doing shallow execution.
451
+ 1. **`<read_first>`** — files o executor DEVE ler antes de tocar em qualquer coisa: o arquivo sendo modificado, "source of truth" do CONTEXT.md, qualquer arquivo cujas convenções/tipos/assinaturas precisem ser replicados.
452
+
453
+ 2. **`<acceptance_criteria>`** condições verificáveis com grep/file read/test command/CLI output. NUNCA linguagem subjetiva ("looks correct"); SEMPRE strings/patterns exatos. Ex: `auth.py contains "def verify_token("`, `test_auth.py exits 0`, `.env.example contains "DATABASE_URL="`.
454
+
455
+ 3. **`<action>`** — valores CONCRETOS, nunca referências. NUNCA "align X with Y"; SEMPRE valores reais (config keys, function signatures, SQL, imports, env vars). Se CONTEXT.md tem tabela de comparação, copie no `<action>` literal. Executor deve completar só com texto do action.
456
+
457
+ **Por quê:** instruções vagas ("update config to match production") geram one-line changes; instruções concretas ("add DATABASE_URL=..., POOL_SIZE=20, REDIS_URL=...") geram trabalho completo. Custo de plano verboso é ínfimo vs custo de redo de execução shallow.
536
458
  </deep_work_rules>
537
459
 
538
460
  <quality_gate>
@@ -725,58 +647,17 @@ Rotear para `<offer_next>` OU `auto_advance` dependendo de flags/config.
725
647
 
726
648
  Verificar gatilho de avanço automático:
727
649
 
728
- 1. Analisar flag `--auto` de $ARGUMENTS
729
- 2. **Sincronizar flag de cadeia com intenção** — se o usuário invocou manualmente (sem `--auto`), limpar a flag de cadeia efêmera de qualquer cadeia `--auto` anterior interrompida. Isso NÃO toca em `workflow.auto_advance` (preferência persistente do usuário):
730
- ```bash
731
- if [[ ! "$ARGUMENTS" =~ --auto ]]; then
732
- node "./.claude/framework/bin/tools.cjs" config-set workflow._auto_chain_active false 2>/dev/null
733
- fi
734
- ```
735
- 3. Ler tanto a flag de cadeia quanto a preferência do usuário:
736
- ```bash
737
- AUTO_CHAIN=$(node "./.claude/framework/bin/tools.cjs" config-get workflow._auto_chain_active 2>/dev/null || echo "false")
738
- AUTO_CFG=$(node "./.claude/framework/bin/tools.cjs" config-get workflow.auto_advance 2>/dev/null || echo "false")
739
- ```
740
-
741
- **Se flag `--auto` presente OU `AUTO_CHAIN` for true OU `AUTO_CFG` for true:**
742
-
743
- Exibir banner:
744
- ```
745
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
746
- framework ► AVANÇANDO AUTOMATICAMENTE PARA EXECUÇÃO
747
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
748
-
749
- Planos prontos. Iniciando execute-phase...
750
- ```
751
-
752
- Iniciar execute-phase usando a ferramenta Skill para evitar sessões Task aninhadas (que causam freezes de runtime devido ao aninhamento profundo de agentes):
753
- ```
754
- Skill(skill="framework:executar-fase", args="${PHASE} --auto --no-transition ${WS}")
755
- ```
756
-
757
- A flag `--no-transition` diz ao execute-phase para retornar status após verificação em vez de encadear mais. Isso mantém a cadeia de avanço automático plana — cada fase roda no mesmo nível de aninhamento em vez de criar agentes Task mais profundos.
650
+ **Detecção:** flag `--auto` em $ARGUMENTS, OR `workflow._auto_chain_active=true`, OR `workflow.auto_advance=true`.
758
651
 
759
- **Lidar com retorno do execute-phase:**
760
- - **FASE CONCLUÍDA** → Exibir resumo final:
761
- ```
762
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
763
- framework ► FASE ${PHASE} CONCLUÍDA ✓
764
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
652
+ **Sync de cadeia:** se invocação manual (sem `--auto`), zere `workflow._auto_chain_active` (não toque `workflow.auto_advance`).
765
653
 
766
- Pipeline de avanço automático finalizado.
654
+ **Quando ativo:** dispare `Skill(skill="framework:executar-fase", args="${PHASE} --auto --no-transition ${WS}")`. A flag `--no-transition` diz pra execute-phase retornar status após verificação (não encadear), mantendo cadeia plana.
767
655
 
768
- Próximo: /discutir-fase ${NEXT_PHASE} --auto ${WS}
769
- ```
770
- - **LACUNAS ENCONTRADAS / VERIFICAÇÃO FALHOU**Exibir resultado, parar cadeia:
771
- ```
772
- Avanço automático parado: Execução precisa de revisão.
773
-
774
- Revisar a saída acima e continuar manualmente:
775
- /executar-fase ${PHASE} ${WS}
776
- ```
656
+ **Roteamento de retorno:**
657
+ - `FASE CONCLUÍDA` → próximo: `/discutir-fase ${NEXT_PHASE} --auto ${WS}` (após `/clear`)
658
+ - `LACUNAS ENCONTRADAS` / `VERIFICAÇÃO FALHOU` → parar cadeia. Continuar: `/executar-fase ${PHASE} ${WS}`
777
659
 
778
- **Se nem `--auto` nem config habilitado:**
779
- Rotear para `<offer_next>` (comportamento existente).
660
+ **Quando inativo:** rotear para `<offer_next>`.
780
661
 
781
662
  </process>
782
663
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luanpdd/kit-mcp",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Generic infrastructure to ship YOUR personal kit of agents/commands/skills as an MCP server, with cross-IDE sync (Claude Code, Cursor, Codex, Gemini, Windsurf, Antigravity, Copilot, Trae).",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/index.js CHANGED
@@ -33,7 +33,10 @@ import { createServer } from '../ui/server.js';
33
33
  import { readLock, lockPathFor } from '../ui/lockfile.js';
34
34
  import { wrapProgressForUi } from '../ui/wrapper.js';
35
35
  import { openBrowser } from '../ui/browser.js';
36
+ import { checkUpgrade, getLocalVersion } from './upgrade-check.js';
36
37
  import http from 'node:http';
38
+ import fs from 'node:fs';
39
+ import os from 'node:os';
37
40
 
38
41
  // Read package.json version at boot so `--version` is always accurate. Falls
39
42
  // back to a string if the file lookup fails (e.g. unusual install layout).
@@ -391,6 +394,14 @@ ui.command('start')
391
394
  const url = `http://127.0.0.1:${actualPort}/`;
392
395
  process.stderr.write(`${c.cyan(icons.info)} kit-mcp ui listening on ${url}\n`);
393
396
  process.stderr.write(`${c.dim(` project: ${projectRoot}`)}\n`);
397
+ // U4: non-blocking upgrade check. Warns if local install is behind npm latest.
398
+ // Cached for 24h via ~/.kit-mcp/version-check.json so we don't hit npm on every start.
399
+ checkUpgrade().then((info) => {
400
+ if (info?.behind) {
401
+ process.stderr.write(`${c.yellow(icons.warn)} kit-mcp v${info.local} → v${info.latest} disponível\n`);
402
+ process.stderr.write(`${c.dim(' atualize com: npm i -g @luanpdd/kit-mcp@latest')}\n`);
403
+ }
404
+ }).catch(() => { /* offline / silent */ });
394
405
  if (opts.open !== false) {
395
406
  await openBrowser(url);
396
407
  }
@@ -457,6 +468,174 @@ ui.command('open')
457
468
  }
458
469
  });
459
470
 
471
+ // --- doctor (DX diagnostic) ---
472
+ program.command('doctor')
473
+ .description('Diagnose kit-mcp setup: version, sidecar, hook, settings.json, lockfile, .planning/.')
474
+ .option('--project-root <path>', 'Project to diagnose (default: cwd)')
475
+ .action(async (opts) => {
476
+ const projectRoot = opts.projectRoot || process.cwd();
477
+ const checks = await runDoctorChecks(projectRoot);
478
+ const failed = checks.filter(c => c.status === 'fail').length;
479
+ const warned = checks.filter(c => c.status === 'warn').length;
480
+
481
+ if (program.opts().json) {
482
+ out({ checks, failed, warned }, () => '');
483
+ process.exit(failed > 0 ? 1 : 0);
484
+ }
485
+
486
+ process.stdout.write(`\n${c.bold('kit-mcp doctor')} — ${projectRoot}\n\n`);
487
+ for (const check of checks) {
488
+ const sym = check.status === 'pass' ? c.green(icons.check)
489
+ : check.status === 'warn' ? c.yellow(icons.warn)
490
+ : c.red(icons.cross);
491
+ process.stdout.write(`${sym} ${c.bold(check.label)}\n`);
492
+ if (check.detail) process.stdout.write(` ${c.dim(check.detail)}\n`);
493
+ if (check.fix) process.stdout.write(` ${c.cyan('fix:')} ${check.fix}\n`);
494
+ }
495
+ process.stdout.write('\n');
496
+ if (failed > 0) {
497
+ process.stdout.write(`${c.red(icons.cross)} ${failed} check(s) failed\n`);
498
+ process.exit(1);
499
+ } else if (warned > 0) {
500
+ process.stdout.write(`${c.yellow(icons.warn)} ${warned} warning(s) — kit-mcp is functional\n`);
501
+ } else {
502
+ process.stdout.write(`${c.green(icons.check)} all checks passed\n`);
503
+ }
504
+ });
505
+
506
+ async function runDoctorChecks(projectRoot) {
507
+ const checks = [];
508
+
509
+ // 1. Version + upgrade availability
510
+ const upgrade = await checkUpgrade();
511
+ if (!upgrade) {
512
+ checks.push({ label: 'version', status: 'fail',
513
+ detail: 'could not read local package.json',
514
+ fix: 'reinstall via `npm i -g @luanpdd/kit-mcp@latest`' });
515
+ } else if (upgrade.latest === null) {
516
+ checks.push({ label: 'version', status: 'warn',
517
+ detail: `local v${upgrade.local} (offline — could not check npm)` });
518
+ } else if (upgrade.behind) {
519
+ checks.push({ label: 'version', status: 'warn',
520
+ detail: `local v${upgrade.local}, latest v${upgrade.latest}`,
521
+ fix: 'npm i -g @luanpdd/kit-mcp@latest' });
522
+ } else {
523
+ checks.push({ label: 'version', status: 'pass',
524
+ detail: `v${upgrade.local} (latest)` });
525
+ }
526
+
527
+ // 2. Sidecar lockfile + healthz
528
+ const lock = readLock(projectRoot);
529
+ if (!lock) {
530
+ checks.push({ label: 'sidecar', status: 'warn',
531
+ detail: 'not running for this project',
532
+ fix: 'kit ui start (or omit if you don\'t need the live viewer)' });
533
+ } else {
534
+ try {
535
+ await getHealthz(lock.port);
536
+ checks.push({ label: 'sidecar', status: 'pass',
537
+ detail: `running on port ${lock.port} (pid ${lock.pid})` });
538
+ } catch (err) {
539
+ checks.push({ label: 'sidecar', status: 'fail',
540
+ detail: `lockfile says port ${lock.port} but unreachable: ${err.message}`,
541
+ fix: 'kit ui stop && kit ui start' });
542
+ }
543
+ }
544
+
545
+ // 3. ~/.claude/settings.json — exists + valid JSON + hooks present?
546
+ const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
547
+ let settings = null;
548
+ try {
549
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
550
+ checks.push({ label: 'settings.json', status: 'pass',
551
+ detail: settingsPath });
552
+ } catch (err) {
553
+ if (err.code === 'ENOENT') {
554
+ checks.push({ label: 'settings.json', status: 'warn',
555
+ detail: 'not found (expected for fresh Claude Code)',
556
+ fix: 'will be created automatically by Claude Code' });
557
+ } else {
558
+ checks.push({ label: 'settings.json', status: 'fail',
559
+ detail: `invalid JSON at ${settingsPath}: ${err.message}`,
560
+ fix: 'edit the file or restore from .claude/settings.json.bak' });
561
+ }
562
+ }
563
+
564
+ // 4. Hook installed?
565
+ if (settings) {
566
+ const hooks = settings.hooks?.PostToolUse;
567
+ const hasHook = Array.isArray(hooks) && hooks.some((h) =>
568
+ Array.isArray(h.hooks) && h.hooks.some((cmd) =>
569
+ typeof cmd.command === 'string' && cmd.command.includes('sidecar-tool-publisher')));
570
+ if (hasHook) {
571
+ checks.push({ label: 'observability hook', status: 'pass',
572
+ detail: 'sidecar-tool-publisher registered as PostToolUse' });
573
+ } else {
574
+ checks.push({ label: 'observability hook', status: 'warn',
575
+ detail: 'sidecar-tool-publisher not registered',
576
+ fix: 'see kit/hooks/sidecar-tool-publisher.js for installation snippet' });
577
+ }
578
+ }
579
+
580
+ // 5. Bundled kit dirs exist
581
+ const here = path.dirname(fileURLToPath(import.meta.url));
582
+ const kitRoot = path.resolve(here, '..', '..', 'kit');
583
+ const expected = ['agents', 'commands', 'skills'];
584
+ const missing = expected.filter((d) => {
585
+ try { return !fs.statSync(path.join(kitRoot, d)).isDirectory(); }
586
+ catch { return true; }
587
+ });
588
+ if (missing.length === 0) {
589
+ checks.push({ label: 'bundled kit', status: 'pass',
590
+ detail: `agents/, commands/, skills/ found in ${kitRoot}` });
591
+ } else {
592
+ checks.push({ label: 'bundled kit', status: 'fail',
593
+ detail: `missing: ${missing.join(', ')} in ${kitRoot}`,
594
+ fix: 'reinstall via `npm i -g @luanpdd/kit-mcp@latest`' });
595
+ }
596
+
597
+ // 6. .planning/ in projectRoot — only warn if absent (not all projects use the framework)
598
+ const planningDir = path.join(projectRoot, '.planning');
599
+ if (fs.existsSync(planningDir)) {
600
+ const stateOk = fs.existsSync(path.join(planningDir, 'STATE.md'));
601
+ const roadmapOk = fs.existsSync(path.join(planningDir, 'ROADMAP.md'));
602
+ if (stateOk && roadmapOk) {
603
+ checks.push({ label: '.planning/', status: 'pass',
604
+ detail: 'STATE.md + ROADMAP.md present' });
605
+ } else {
606
+ checks.push({ label: '.planning/', status: 'warn',
607
+ detail: `present but missing ${[!stateOk && 'STATE.md', !roadmapOk && 'ROADMAP.md'].filter(Boolean).join(', ')}`,
608
+ fix: 'run `kit saude` to repair, or `/novo-marco` if mid-cycle' });
609
+ }
610
+ } else {
611
+ checks.push({ label: '.planning/', status: 'warn',
612
+ detail: 'no framework state in this project',
613
+ fix: 'run `/novo-projeto` to bootstrap, or skip if not using the framework' });
614
+ }
615
+
616
+ // 7. Stale lockfile cleanup hint
617
+ try {
618
+ const tmpdir = os.tmpdir();
619
+ const orphans = fs.readdirSync(tmpdir).filter(n => /^kit-mcp-ui-[0-9a-f]{16}\.lock$/.test(n));
620
+ const stale = [];
621
+ for (const name of orphans) {
622
+ try {
623
+ const lock = JSON.parse(fs.readFileSync(path.join(tmpdir, name), 'utf8'));
624
+ try { process.kill(lock.pid, 0); } catch (err) {
625
+ if (err.code === 'ESRCH') stale.push(name);
626
+ }
627
+ } catch { /* skip unreadable */ }
628
+ }
629
+ if (stale.length > 0) {
630
+ checks.push({ label: 'orphan lockfiles', status: 'warn',
631
+ detail: `${stale.length} stale lockfile(s) in ${tmpdir}`,
632
+ fix: stale.map(n => `rm "${path.join(tmpdir, n)}"`).join(' && ') });
633
+ }
634
+ } catch { /* tmpdir scan is best-effort */ }
635
+
636
+ return checks;
637
+ }
638
+
460
639
  // Helpers for kit ui (live in cli/ — stdout/console allowed here)
461
640
  async function postShutdown(port) {
462
641
  return new Promise((resolve, reject) => {
@@ -0,0 +1,135 @@
1
+ // upgrade-check.js — non-blocking check for newer kit-mcp on npm.
2
+ //
3
+ // Both `kit doctor` (U1) and `kit ui start` (U4) call this. Result is cached
4
+ // to ~/.kit-mcp/version-check.json for 24h so we don't hit the npm registry on
5
+ // every boot. Falls back gracefully when offline or when the request fails.
6
+
7
+ import fs from 'node:fs/promises';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import https from 'node:https';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+
16
+ const PACKAGE_NAME = '@luanpdd/kit-mcp';
17
+ const CHECK_TTL_MS = 24 * 60 * 60 * 1000; // 24h
18
+ const REQUEST_TIMEOUT_MS = 1500;
19
+
20
+ function cacheFile() {
21
+ return path.join(os.homedir(), '.kit-mcp', 'version-check.json');
22
+ }
23
+
24
+ async function readCache() {
25
+ try {
26
+ const raw = await fs.readFile(cacheFile(), 'utf8');
27
+ const obj = JSON.parse(raw);
28
+ if (typeof obj.checkedAt !== 'number' || typeof obj.latest !== 'string') return null;
29
+ if (Date.now() - obj.checkedAt > CHECK_TTL_MS) return null;
30
+ return obj;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ async function writeCache(obj) {
37
+ try {
38
+ await fs.mkdir(path.dirname(cacheFile()), { recursive: true });
39
+ await fs.writeFile(cacheFile(), JSON.stringify(obj), 'utf8');
40
+ } catch {
41
+ /* cache failures are silent — not critical */
42
+ }
43
+ }
44
+
45
+ function fetchLatest() {
46
+ // Use the registry's package endpoint. Falls back to gracefully on any error.
47
+ return new Promise((resolve) => {
48
+ const req = https.request({
49
+ method: 'GET',
50
+ hostname: 'registry.npmjs.org',
51
+ path: `/${encodeURIComponent(PACKAGE_NAME)}/latest`,
52
+ headers: { 'accept': 'application/json' },
53
+ timeout: REQUEST_TIMEOUT_MS,
54
+ }, (res) => {
55
+ if (res.statusCode !== 200) { res.resume(); resolve(null); return; }
56
+ let body = '';
57
+ res.setEncoding('utf8');
58
+ res.on('data', (c) => { body += c; });
59
+ res.on('end', () => {
60
+ try {
61
+ const j = JSON.parse(body);
62
+ resolve(typeof j.version === 'string' ? j.version : null);
63
+ } catch {
64
+ resolve(null);
65
+ }
66
+ });
67
+ });
68
+ req.on('error', () => resolve(null));
69
+ req.on('timeout', () => { try { req.destroy(); } catch { /* noop */ } resolve(null); });
70
+ req.end();
71
+ });
72
+ }
73
+
74
+ export async function getLocalVersion() {
75
+ // Read package.json from the kit-mcp install root (parent of src/).
76
+ try {
77
+ const pkgPath = path.resolve(__dirname, '../../package.json');
78
+ const raw = await fs.readFile(pkgPath, 'utf8');
79
+ const j = JSON.parse(raw);
80
+ return typeof j.version === 'string' ? j.version : null;
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+
86
+ // Compare semver-like x.y.z lexically by component. Returns -1/0/1.
87
+ // Missing components default to 0 ("1.5" === "1.5.0").
88
+ function compareVersions(a, b) {
89
+ const parse = (v) => {
90
+ const parts = v.split('.').map((n) => Number.parseInt(n, 10) || 0);
91
+ while (parts.length < 3) parts.push(0);
92
+ return parts;
93
+ };
94
+ const [a1, a2, a3] = parse(a);
95
+ const [b1, b2, b3] = parse(b);
96
+ if (a1 !== b1) return a1 < b1 ? -1 : 1;
97
+ if (a2 !== b2) return a2 < b2 ? -1 : 1;
98
+ if (a3 !== b3) return a3 < b3 ? -1 : 1;
99
+ return 0;
100
+ }
101
+
102
+ // checkUpgrade({ force }): returns { local, latest, behind, source } or null on failure.
103
+ // force=true bypasses the 24h cache.
104
+ export async function checkUpgrade({ force = false } = {}) {
105
+ const local = await getLocalVersion();
106
+ if (!local) return null;
107
+
108
+ if (!force) {
109
+ const cached = await readCache();
110
+ if (cached?.latest) {
111
+ return {
112
+ local,
113
+ latest: cached.latest,
114
+ behind: compareVersions(local, cached.latest) < 0,
115
+ source: 'cache',
116
+ };
117
+ }
118
+ }
119
+
120
+ const latest = await fetchLatest();
121
+ if (!latest) {
122
+ // Network failed; surface what we have.
123
+ return { local, latest: null, behind: false, source: 'offline' };
124
+ }
125
+
126
+ await writeCache({ checkedAt: Date.now(), latest });
127
+ return {
128
+ local,
129
+ latest,
130
+ behind: compareVersions(local, latest) < 0,
131
+ source: 'network',
132
+ };
133
+ }
134
+
135
+ export const __test = { compareVersions, PACKAGE_NAME, CHECK_TTL_MS };
package/src/core/gates.js CHANGED
@@ -22,7 +22,19 @@ const __filename = fileURLToPath(import.meta.url);
22
22
  const __dirname = path.dirname(__filename);
23
23
  export const DEFAULT_GATES_ROOT = path.resolve(__dirname, '../../gates');
24
24
 
25
+ // P2: TTL cache for listGates (mirrors PERF-01 in kit.js). Gates change rarely;
26
+ // inside a single Claude Code session we may call listGates → getGate → gatesForStage
27
+ // in sequence — without cache, that's 3 full directory walks of the gates dir.
28
+ const GATES_CACHE_TTL_MS = 30_000;
29
+ const gatesCache = new Map(); // gatesRoot -> { value, ts }
30
+
31
+ export function clearGatesCache() { gatesCache.clear(); }
32
+
25
33
  export async function listGates(gatesRoot = DEFAULT_GATES_ROOT) {
34
+ const cached = gatesCache.get(gatesRoot);
35
+ if (cached && Date.now() - cached.ts < GATES_CACHE_TTL_MS) {
36
+ return cached.value;
37
+ }
26
38
  let entries;
27
39
  try { entries = await fs.readdir(gatesRoot, { withFileTypes: true }); }
28
40
  catch { return []; }
@@ -40,7 +52,9 @@ export async function listGates(gatesRoot = DEFAULT_GATES_ROOT) {
40
52
  absPath: abs,
41
53
  });
42
54
  }
43
- return out.sort((a, b) => a.id.localeCompare(b.id));
55
+ const value = out.sort((a, b) => a.id.localeCompare(b.id));
56
+ gatesCache.set(gatesRoot, { value, ts: Date.now() });
57
+ return value;
44
58
  }
45
59
 
46
60
  export async function getGate(id, gatesRoot = DEFAULT_GATES_ROOT) {