@spec-wave/cli 0.32.0 → 0.34.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spec-wave/cli",
3
- "version": "0.32.0",
3
+ "version": "0.34.0",
4
4
  "description": "Setup spec-driven GitHub workflow with Projects v2, labels, issue templates, and AI-powered Actions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -528,3 +528,31 @@ export async function linkProjectToRepo(token, projectId, repositoryId) {
528
528
  }
529
529
  `, { projectId, repositoryId });
530
530
  }
531
+
532
+ /**
533
+ * Cota do GraphQL restante (item 5 do rfc/plano-hardening-agentes-2026-08.md).
534
+ *
535
+ * `rateLimit` é o único campo da API que consultar **não custa pontos** — é
536
+ * assim que o `doctor` consegue medir ANTES de gastar, em vez de descobrir o
537
+ * esgotamento no meio de uma varredura de sub-issues.
538
+ *
539
+ * @returns {Promise<{limit:number, remaining:number, resetAt:string}|null>}
540
+ * null em qualquer falha (rede, token sem escopo) — best-effort.
541
+ */
542
+ export async function getRateLimit(token) {
543
+ const client = makeClient(token);
544
+ try {
545
+ const result = await client(`
546
+ query RateLimit {
547
+ rateLimit {
548
+ limit
549
+ remaining
550
+ resetAt
551
+ }
552
+ }
553
+ `);
554
+ return result.rateLimit || null;
555
+ } catch {
556
+ return null;
557
+ }
558
+ }
@@ -359,6 +359,19 @@ export async function deleteLabel(token, owner, repo, name) {
359
359
  }
360
360
  }
361
361
 
362
+ // Quantas issues ABERTAS têm uma label — usado pelo `update` para não apagar
363
+ // uma label `spec-wave:*` órfã que ainda está em uso (item 7 do
364
+ // rfc/plano-hardening-agentes-2026-08.md): apagar uma label no GitHub a
365
+ // remove de TODA issue que a tem, o que é irreversível. PRs entram na mesma
366
+ // listagem de issues da API REST — filtrados fora, não são o que se quer contar.
367
+ export async function countOpenIssuesWithLabel(token, owner, repo, labelName) {
368
+ const octokit = makeOctokit(token);
369
+ const issues = await octokit.paginate('GET /repos/{owner}/{repo}/issues', {
370
+ owner, repo, labels: labelName, state: 'open', per_page: 100,
371
+ });
372
+ return issues.filter(i => !i.pull_request).length;
373
+ }
374
+
362
375
  /**
363
376
  * Apaga a ref de uma branch.
364
377
  *
@@ -515,6 +528,17 @@ export async function getPR(token, owner, repo, prNumber) {
515
528
  return res.data;
516
529
  }
517
530
 
531
+ // Mensagens de commit (assunto + corpo) de um PR — usado por `--verify-commits`
532
+ // (implement) e pela conferência do `merge` para ler os git trailer de
533
+ // Story/Task sem precisar de checkout local (lib/commit-trailers.mjs).
534
+ export async function listPullRequestCommitMessages(token, owner, repo, prNumber) {
535
+ const octokit = makeOctokit(token);
536
+ const commits = await octokit.paginate(octokit.rest.pulls.listCommits, {
537
+ owner, repo, pull_number: prNumber, per_page: 100,
538
+ });
539
+ return commits.map(c => c.commit.message);
540
+ }
541
+
518
542
  // `ref` é opcional (compatível com os chamadores de 4 argumentos): o modo PR do
519
543
  // `update` precisa ler o .spec-wave.json na BASE, não no que a API escolher.
520
544
  export async function getFileContent(token, owner, repo, path, ref) {
package/src/cli.mjs CHANGED
@@ -213,6 +213,7 @@ export function buildProgram() {
213
213
  .option('--no-skill-in-pr', 'Força manter a skill dos agentes fora do Pull Request')
214
214
  .option('--dry-run', 'Mostra o que seria atualizado sem alterar nada')
215
215
  .option('--yes', 'Aplica sem pedir confirmação')
216
+ .option('--force-labels', 'Remove labels spec-wave:* órfãs mesmo com issues abertas usando-as (padrão: mantém)')
216
217
  .action(async (options) => {
217
218
  const { update } = await import('./commands/update.mjs');
218
219
  await update(options).catch(err => { console.error(err.message); process.exit(1); });
@@ -423,6 +424,7 @@ Docs: https://astratech-net-br.github.io/spec-wave-cli/guia/qa/`);
423
424
  .option('--feature-dir <path>', 'Caminho do docs/features/<slug> (sobrescreve a resolução automática)')
424
425
  .option('--dry-run', 'Monta o contexto e imprime o comando sem executar o spec-kit')
425
426
  .option('--refresh', 'Ignora o cache local (sub-issues/dependências) e reconsulta a API')
427
+ .option('--verify-commits', 'Confere se os commits recentes trazem o trailer Spec-Wave-Story/Tasks (best-effort, nunca bloqueia)')
426
428
  .action(async (issue, options) => {
427
429
  const { implement } = await import('./commands/implement.mjs');
428
430
  await implement({ issue, ...options }).catch(err => { console.error(err.message); process.exit(1); });
@@ -521,6 +523,7 @@ para incluí-lo na consulta, ou --sync para gravá-lo em definitivo nos artefato
521
523
  .option('--dry-run', 'Mostra o que seria instalado sem gravar')
522
524
  .option('--force', 'Reinstala o binário e regrava a config')
523
525
  .option('--yes', 'Modo não-interativo')
526
+ .option('--queue <nome>', 'No --install/--build: fila deste daemon (padrão: dev-agent). Para uma trilha extra, declare-a em devAgent.queues do .spec-wave.json antes — senão update/doctor não reconhecem a label')
524
527
  .action(async (options) => {
525
528
  const { devAgent } = await import('./commands/dev-agent.mjs');
526
529
  await devAgent(options).catch(err => { console.error(err.message); process.exit(1); });
@@ -529,9 +532,10 @@ para incluí-lo na consulta, ou --sync para gravá-lo em definitivo nos artefato
529
532
  program
530
533
  .command('doctor')
531
534
  .description('Diagnostica a configuração do spec-wave no repositório atual')
532
- .action(async () => {
535
+ .option('--deep', 'Confere também as Tasks de cada decomposição aplicada (1 chamada GraphQL a mais por Story; padrão: só Stories)')
536
+ .action(async (options) => {
533
537
  const { doctor } = await import('./commands/doctor.mjs');
534
- await doctor().catch(err => { console.error(err.message); process.exit(1); });
538
+ await doctor(options).catch(err => { console.error(err.message); process.exit(1); });
535
539
  });
536
540
 
537
541
  // O mapa por tema no fim do --help: 29 comandos em lista plana dizem O QUE
@@ -144,7 +144,11 @@ export async function audit({ milestone: milestoneArg, critique = false, json =
144
144
  for (const issue of targetIssues) {
145
145
  if (detectIssueType(issue) !== 'Feature' || issue.state === 'closed') continue;
146
146
  const docs = featureDocPaths(root, issue, 'Feature');
147
- const { content } = await loadArtifact({
147
+ // `state` distingue "não existe" ('missing') de "não deu para verificar
148
+ // agora" ('unknown' — rede/permissão) — item 8 do rfc/plano-hardening-
149
+ // agentes-2026-08.md: descartar isso é a mesma lacuna que o preflight já
150
+ // corrigiu (PRs #65/#66) e que este comando herdou sem herdar a distinção.
151
+ const { content, state } = await loadArtifact({
148
152
  token, owner, repo, root, base,
149
153
  pathRel: docs.spec.rel, doc: 'spec', issueNumber: issue.number,
150
154
  });
@@ -154,6 +158,7 @@ export async function audit({ milestone: milestoneArg, critique = false, json =
154
158
  slug: slugify(issue.title),
155
159
  milestone: { number: milestone.number, title: milestone.title, due_on: milestone.due_on ?? null },
156
160
  spec: content,
161
+ specState: state,
157
162
  });
158
163
  }
159
164
 
@@ -257,7 +262,11 @@ export async function audit({ milestone: milestoneArg, critique = false, json =
257
262
  return saida;
258
263
  }
259
264
 
265
+ // 'sem-alvo' ganha marca própria: sem ela, `dependsOn: []` fica idêntico ao
266
+ // de uma Feature 'vazia' (que diz "Nenhuma" de propósito) — item 3a do rfc/
267
+ // plano-hardening-agentes-2026-08.md.
260
268
  const grafoLinhas = resultado.grafo.map(g => {
269
+ if (g.secao === 'sem-alvo') return ` #${g.number} ⟨sem arestas legíveis⟩`;
261
270
  const deps = g.dependsOn.length ? ` ← depende de ${g.dependsOn.map(d => `#${d}`).join(', ')}` : '';
262
271
  return ` #${g.number}${deps}`;
263
272
  });
@@ -76,13 +76,21 @@ function render(template, vars) {
76
76
 
77
77
  /**
78
78
  * config.toml do agente a partir do .spec-wave.json. Nunca escreve token.
79
+ *
80
+ * `queue` é o SUFIXO da trilha (sem `spec-wave:`) — `dev-agent` (default) ou
81
+ * uma trilha extra (`dev-agent-b`, `dev-agent-c`, …) para rodar mais de um
82
+ * daemon em paralelo. A label correspondente só é criada/preservada pelo
83
+ * `update`/`doctor` se estiver declarada em `devAgent.queues` do
84
+ * `.spec-wave.json` — ver `queueLabels` em `config.mjs` e o item 7 do
85
+ * rfc/plano-hardening-agentes-2026-08.md.
79
86
  */
80
- export function renderAgentConfig({ owner, repo }) {
87
+ export function renderAgentConfig({ owner, repo, queue = 'dev-agent' }) {
88
+ const queueLabel = queue === 'dev-agent' ? LABEL_DEV_AGENT : `spec-wave:${queue}`;
81
89
  return `# spec-wave-agent — gerado por \`spec-wave dev-agent --install\`
82
90
  # Schema completo: https://github.com/${AGENT_REPO}#configuração
83
91
 
84
92
  repo = "${owner}/${repo}"
85
- queue_label = "${LABEL_DEV_AGENT}"
93
+ queue_label = "${queueLabel}"
86
94
 
87
95
  # Defaults do agente (descomente para ajustar):
88
96
  #poll_interval_secs = 60 # consulta à fila quando ocioso
@@ -362,6 +370,14 @@ async function install(options) {
362
370
 
363
371
  /** Config + serviço + avisos finais — comum a --install e --build. */
364
372
  async function finishSetup({ cfg, home, binPath, configPath, options, configAction, servicePath }) {
373
+ const queue = options.queue || 'dev-agent';
374
+ if (queue !== 'dev-agent' && !(cfg.devAgent?.queues || []).includes(queue)) {
375
+ p.log.warn(
376
+ `Fila "${queue}" não está em devAgent.queues no ${CONFIG_FILE} deste repo — ` +
377
+ `\`spec-wave update\`/\`doctor\` não vão reconhecer a label spec-wave:${queue} e um ` +
378
+ 'update de rotina pode removê-la, esvaziando esta trilha. Adicione-a e rode `spec-wave update` antes.'
379
+ );
380
+ }
365
381
  if (configAction === 'manter') {
366
382
  p.log.info(`Config mantida: ${configPath} já existe (use --force para regerar).`);
367
383
  } else if (existsSync(configPath) && !options.yes) {
@@ -370,12 +386,12 @@ async function finishSetup({ cfg, home, binPath, configPath, options, configActi
370
386
  p.log.info('Config mantida.');
371
387
  } else {
372
388
  mkdirSync(path.dirname(configPath), { recursive: true });
373
- writeFileSync(configPath, renderAgentConfig(cfg), 'utf-8');
389
+ writeFileSync(configPath, renderAgentConfig({ ...cfg, queue: options.queue || 'dev-agent' }), 'utf-8');
374
390
  p.log.success(`Config regravada em ${configPath}.`);
375
391
  }
376
392
  } else {
377
393
  mkdirSync(path.dirname(configPath), { recursive: true });
378
- writeFileSync(configPath, renderAgentConfig(cfg), 'utf-8');
394
+ writeFileSync(configPath, renderAgentConfig({ ...cfg, queue: options.queue || 'dev-agent' }), 'utf-8');
379
395
  p.log.success(`Config criada em ${chalk.cyan(configPath)} (repo ${cfg.owner}/${cfg.repo}).`);
380
396
  }
381
397
 
@@ -14,16 +14,20 @@ import {
14
14
  resolveToken, verifyTokenScopes, describeTokenSource, activeGhAccount,
15
15
  tokenMismatchWarning, parseActiveAccount,
16
16
  } from '../api/auth.mjs';
17
- import { getProjectSnapshot, listSubIssues, listProjectItems } from '../api/github-graphql.mjs';
17
+ import { getProjectSnapshot, listProjectItems, getRateLimit } from '../api/github-graphql.mjs';
18
18
  import { getRepoVariable } from '../api/github-rest.mjs';
19
19
  import {
20
20
  CONFIG_FILE, WORKFLOW_FILES, ARTIFACT_WORKFLOW_FILES, getProvider, DEFAULT_PROVIDER,
21
21
  AI_PROVIDERS, STATUS_OPTIONS,
22
22
  RETIRED_STAGES, ALL_LABELS, allLabelsFor, LABEL_NEEDS_HUMAN, LABEL_QA_READY, MODEL_LABEL_PREFIX,
23
+ DEV_AGENT_QUEUE_PREFIX,
23
24
  DEFAULT_MAX_CRITIQUE_ATTEMPTS, STAGE_TRACKS, AI_ACTIONS, recommendedModelAliases,
24
25
  modelLabels, STAGE_QA,
25
26
  } from '../config.mjs';
26
27
  import { slugify } from '../lib/slugify.mjs';
28
+ import { cachedSubIssues } from '../lib/story-graph.mjs';
29
+ import { affordCheck } from '../lib/rate-budget.mjs';
30
+ import { resolveCacheTtl } from '../lib/net-cache.mjs';
27
31
  import { QA_PLAN_FILE } from '../lib/qa-plan-doc.mjs';
28
32
  import { findConfigPath } from '../lib/project-root.mjs';
29
33
  import { configuredMode, describeModeState, EXECUTION_VARIABLE } from '../lib/execution-mode.mjs';
@@ -98,6 +102,31 @@ async function checkToken(ctx) {
98
102
  }
99
103
  }
100
104
 
105
+ // Item 5 do rfc/plano-hardening-agentes-2026-08.md: medir a cota ANTES de
106
+ // gastar. `rateLimit` não custa pontos, então isto é de graça — e é o que
107
+ // falta para o doctor explicar "pulei este check por causa da cota" em vez de
108
+ // piorar um esgotamento em silêncio (seis "!" sem dizer a causa comum).
109
+ // Guardada em `ctx.rateLimit` para os checks caros consultarem depois.
110
+ async function checkRateLimit(ctx) {
111
+ const name = 'Cota da API (GraphQL)';
112
+ if (!ctx.token) {
113
+ return { name, status: 'warn', detail: 'Sem token — cota não verificável.' };
114
+ }
115
+ ctx.rateLimit = await getRateLimit(ctx.token);
116
+ if (!ctx.rateLimit) {
117
+ return { name, status: 'warn', detail: 'Não foi possível consultar a cota agora.' };
118
+ }
119
+ const { remaining, limit, resetAt } = ctx.rateLimit;
120
+ const reset = new Date(resetAt).toLocaleTimeString('pt-BR');
121
+ const status = remaining < 500 ? 'warn' : 'ok';
122
+ return {
123
+ name,
124
+ status,
125
+ detail: `${remaining}/${limit} pontos restantes (reset ${reset}).` +
126
+ (status === 'warn' ? ' Cota baixa — checks caros do doctor podem ser pulados; veja as notas deles.' : ''),
127
+ };
128
+ }
129
+
101
130
  async function checkScopes(ctx) {
102
131
  const name = 'Escopos do token';
103
132
  if (!ctx.token) {
@@ -355,7 +384,9 @@ export function suggestModelAliases({ provider = undefined, aliases = null } = {
355
384
  return { missing, snippet: `"modelAliases": ${JSON.stringify(merged, null, 2)}` };
356
385
  }
357
386
 
358
- export function inspectBoardHygiene({ boardStages = null, repoLabels = null, modelAliases = null } = {}) {
387
+ export function inspectBoardHygiene({
388
+ boardStages = null, repoLabels = null, modelAliases = null, devAgentQueues = null,
389
+ } = {}) {
359
390
  const canonical = STATUS_OPTIONS.map(s => s.name);
360
391
  const known = new Set(canonical);
361
392
  const retired = new Map(RETIRED_STAGES.map(s => [s.name, s]));
@@ -377,11 +408,21 @@ export function inspectBoardHygiene({ boardStages = null, repoLabels = null, mod
377
408
  // ai.modelAliases — aí o aviso é verdadeiro (label aponta para apelido que não
378
409
  // existe mais). Sem config (doctor sem acesso ao arquivo), nenhuma label de
379
410
  // modelo é acusada: melhor calar do que mandar apagar o que funciona.
380
- const wantedLabels = allLabelsFor(modelAliases ? { modelAliases } : undefined);
411
+ //
412
+ // Mesma regra para `spec-wave:dev-agent-<sufixo>` (trilha extra do
413
+ // dev-agent, item 7 do rfc/plano-hardening-agentes-2026-08.md): sem
414
+ // devAgentQueues em mãos, nenhuma label desse padrão é acusada — apagá-la
415
+ // esvazia a fila inteira. A fila DEFAULT (`spec-wave:dev-agent`, sem
416
+ // sufixo) já está em ALL_LABELS e nunca cai neste ramo.
417
+ const wantedLabels = allLabelsFor(
418
+ modelAliases ? { modelAliases } : undefined,
419
+ devAgentQueues ? { queues: devAgentQueues } : undefined
420
+ );
381
421
  const knownLabels = new Set(wantedLabels.map(l => l.name));
382
422
  const orphanLabels = repoLabels
383
423
  ? repoLabels.filter(n => n.startsWith('spec-wave:') && !knownLabels.has(n)
384
- && (modelAliases !== null || !n.startsWith(MODEL_LABEL_PREFIX)))
424
+ && (modelAliases !== null || !n.startsWith(MODEL_LABEL_PREFIX))
425
+ && (devAgentQueues !== null || !n.startsWith(DEV_AGENT_QUEUE_PREFIX)))
385
426
  : [];
386
427
  // Ausentes saem em duas listas: uma label do fluxo que falta é um repo
387
428
  // desatualizado; uma `spec-wave:model:<apelido>` que falta é um apelido
@@ -457,7 +498,11 @@ async function checkBoardHygiene(ctx) {
457
498
  }
458
499
 
459
500
  const { unknownStages, retiredStages, missingStages, orphanLabels, missingLabels, missingModelLabels } =
460
- inspectBoardHygiene({ boardStages, repoLabels, modelAliases: cfg?.ai?.modelAliases ?? null });
501
+ inspectBoardHygiene({
502
+ boardStages, repoLabels,
503
+ modelAliases: cfg?.ai?.modelAliases ?? null,
504
+ devAgentQueues: cfg?.devAgent?.queues ?? null,
505
+ });
461
506
 
462
507
  const notes = [];
463
508
  let status = 'ok';
@@ -831,6 +876,26 @@ export function decompositionDrift(doc, atual = new Map()) {
831
876
  return out;
832
877
  }
833
878
 
879
+ /**
880
+ * Prepara o doc para `decompositionDrift` quando a árvore fetchada não desceu
881
+ * ao nível de Task (função PURA — item 5 do rfc/plano-hardening-agentes-
882
+ * 2026-08.md, a checagem sem `--deep` só busca as Stories).
883
+ *
884
+ * Comparar um doc com Tasks contra uma árvore sem Tasks reportaria CADA Task
885
+ * como "rescopada ou apagada" por engano — o dado não foi buscado, não é que
886
+ * ele sumiu. Tirar as Tasks dos dois lados evita o falso positivo sem mentir
887
+ * sobre profundidade: quem lê a nota do doctor sabe que só Stories foram
888
+ * conferidas.
889
+ *
890
+ * @param {object} doc documento parseado (parseDecompositionDoc)
891
+ * @param {{deep: boolean}} params
892
+ * @returns {object} doc, ou uma cópia com `stories[].tasks` esvaziado
893
+ */
894
+ export function decompositionDocForDrift(doc, { deep }) {
895
+ if (deep) return doc;
896
+ return { ...doc, stories: (doc.stories || []).map(s => ({ ...s, tasks: [] })) };
897
+ }
898
+
834
899
  // Lê os decomposition.md aplicados e confere se ainda descrevem a árvore real.
835
900
  // Best-effort e sempre warn: divergência aqui é informação para o humano, não
836
901
  // falha de configuração — rescopar issues à mão é legítimo, esquecer de dizer
@@ -876,16 +941,43 @@ async function checkDecompositions(ctx) {
876
941
  continue;
877
942
  }
878
943
 
879
- // Monta a árvore real: sub-issues da Feature + as de cada Story.
944
+ // Orçamento: 1 GraphQL para as Stories da Feature + (só com --deep) 1 por
945
+ // Story, para as Tasks. Sem isso este check sozinho podia custar 1 REST +
946
+ // 1 GraphQL por Feature + 1 GraphQL POR STORY da milestone inteira, sem
947
+ // teto nem cache — o padrão que dependency-map.mjs existe para eliminar.
948
+ const storyCount = (doc.stories || []).length;
949
+ const estimated = 1 + (ctx.deep ? storyCount : 0);
950
+ const budget = affordCheck({
951
+ remaining: ctx.rateLimit?.remaining ?? null,
952
+ estimated,
953
+ resetAt: ctx.rateLimit?.resetAt ?? null,
954
+ });
955
+ if (!budget.run) {
956
+ status = 'warn';
957
+ notes.push(`${rel}: árvore de #${doc.issueNumber} não verificada — ${budget.motivo}`);
958
+ continue;
959
+ }
960
+
961
+ // Monta a árvore real: sub-issues da Feature (cacheadas, TTL de
962
+ // cache.ttlSec) e, só com --deep, as de cada Story também.
880
963
  const atual = new Map();
881
964
  try {
882
965
  const issue = await makeOctokit(ctx.token).rest.issues.get({
883
966
  owner: ctx.cfg.owner, repo: ctx.cfg.repo, issue_number: doc.issueNumber,
884
967
  });
885
- const subs = await listSubIssues(ctx.token, issue.data.node_id);
968
+ const ttlSec = resolveCacheTtl(ctx.cfg);
969
+ const { subs } = await cachedSubIssues({
970
+ token: ctx.token, owner: ctx.cfg.owner, repo: ctx.cfg.repo, root, ttlSec,
971
+ parent: { number: doc.issueNumber, nodeId: issue.data.node_id },
972
+ });
886
973
  for (const s of subs) {
887
974
  atual.set(s.number, { title: s.title, state: s.state, stateReason: s.stateReason });
888
- for (const t of await listSubIssues(ctx.token, s.nodeId).catch(() => [])) {
975
+ if (!ctx.deep) continue;
976
+ const { subs: tasks } = await cachedSubIssues({
977
+ token: ctx.token, owner: ctx.cfg.owner, repo: ctx.cfg.repo, root, ttlSec,
978
+ parent: { number: s.number, nodeId: s.nodeId },
979
+ }).catch(() => ({ subs: [] }));
980
+ for (const t of tasks) {
889
981
  atual.set(t.number, { title: t.title, state: t.state, stateReason: t.stateReason });
890
982
  }
891
983
  }
@@ -894,14 +986,15 @@ async function checkDecompositions(ctx) {
894
986
  continue;
895
987
  }
896
988
 
897
- const drift = decompositionDrift(doc, atual);
989
+ const drift = decompositionDrift(decompositionDocForDrift(doc, { deep: ctx.deep }), atual);
990
+ const profundidade = ctx.deep ? '' : ' (só Stories — rode com --deep para conferir as Tasks também)';
898
991
  if (drift.length === 0) {
899
- notes.push(`${rel}: confere com as ${atual.size} issue(s) de #${doc.issueNumber}.`);
992
+ notes.push(`${rel}: confere com as ${atual.size} issue(s) de #${doc.issueNumber}${profundidade}.`);
900
993
  continue;
901
994
  }
902
995
  status = 'warn';
903
996
  notes.push(
904
- `${rel} descreve um desenho diferente do que existe hoje:\n` +
997
+ `${rel} descreve um desenho diferente do que existe hoje${profundidade}:\n` +
905
998
  drift.map(d => ` - ${d}`).join('\n') + '\n' +
906
999
  ' Atualize o arquivo ou marque-o como histórico — aplicado ele é registro, e registro ' +
907
1000
  'errado parece confiável.'
@@ -1427,11 +1520,11 @@ async function checkWorkflows(ctx) {
1427
1520
 
1428
1521
  // ── Comando ─────────────────────────────────────────────────────────────────
1429
1522
 
1430
- export async function doctor() {
1523
+ export async function doctor(options = {}) {
1431
1524
  p.intro(chalk.bold('spec-wave doctor'));
1432
1525
 
1433
1526
  // Contexto compartilhado entre os checks (token, config, resultados parciais).
1434
- const ctx = { cwd: process.cwd() };
1527
+ const ctx = { cwd: process.cwd(), deep: !!options.deep };
1435
1528
  // Procurado subindo na árvore: rodar o doctor de um subdiretório deve
1436
1529
  // diagnosticar o repositório, não reportar "não inicializado".
1437
1530
  ctx.configPath = findConfigPath(ctx.cwd);
@@ -1448,6 +1541,7 @@ export async function doctor() {
1448
1541
 
1449
1542
  const checks = [
1450
1543
  checkToken,
1544
+ checkRateLimit,
1451
1545
  checkScopes,
1452
1546
  checkGhAccount,
1453
1547
  checkConfig,