@payconductor/houndsman 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +39 -78
  2. package/assets/logo-legacy.png +0 -0
  3. package/assets/logo.png +0 -0
  4. package/package.json +3 -2
  5. package/src/cli/index.ts +89 -9
  6. package/src/core/assets.ts +8 -0
  7. package/src/core/brief.ts +25 -2
  8. package/src/core/commands/findings.ts +390 -0
  9. package/src/core/commands/scaffold.ts +20 -0
  10. package/src/core/config.ts +42 -0
  11. package/src/core/diff-source.ts +140 -0
  12. package/src/core/file-classifier.ts +50 -2
  13. package/src/core/finding-anchor.ts +147 -0
  14. package/src/core/findings-state.ts +538 -0
  15. package/src/core/jev/jev-client.ts +72 -11
  16. package/src/core/jev/jev-detector.ts +21 -19
  17. package/src/core/orchestrator.ts +169 -17
  18. package/src/core/paths.ts +3 -0
  19. package/src/core/registry-check.ts +15 -0
  20. package/src/core/report-builder.ts +113 -9
  21. package/src/core/types.ts +10 -0
  22. package/src/index.ts +78 -2
  23. package/src/server/artifacts.ts +66 -0
  24. package/src/server/client/App.tsx +158 -0
  25. package/src/server/client/ArtifactsPanel.tsx +48 -0
  26. package/src/server/client/Filters.tsx +83 -0
  27. package/src/server/client/FindingCard.tsx +122 -0
  28. package/src/server/client/FindingsList.tsx +54 -0
  29. package/src/server/client/RunList.tsx +43 -0
  30. package/src/server/client/SummaryBar.tsx +21 -0
  31. package/src/server/client/api.ts +37 -0
  32. package/src/server/client/constants.ts +63 -0
  33. package/src/server/client/index.tsx +12 -0
  34. package/src/server/client/types.ts +48 -0
  35. package/src/server/client-build.ts +47 -0
  36. package/src/server/index.ts +365 -0
  37. package/src/skill/README.md +14 -6
  38. package/src/skill/SETUP.md +5 -3
  39. package/src/skill/houndsman-update.md +1 -1
  40. package/src/skill/houndsman.md +87 -10
  41. package/src/tests/findings-anchor.test.ts +120 -0
  42. package/src/tests/findings-commands.test.ts +226 -0
  43. package/src/tests/findings-state.test.ts +274 -0
  44. package/src/tests/review-scripts.test.ts +299 -0
  45. package/src/tests/server.test.ts +212 -0
package/README.md CHANGED
@@ -12,7 +12,8 @@ relatório único.
12
12
 
13
13
  **Terminologia:** **houndsman** = orquestrador/treinador (CLI + motor) · **watchdog** = cada
14
14
  agente especialista por domínio que audita o diff · **review/revisão** = o processo ·
15
- **Jev** = pré-análise opcional.
15
+ **Jev** = pré-análise opcional. O vocabulário completo (brief, findings, manifest, budget,
16
+ overflow, churn, candidato…) está em [`docs/GLOSSARY.md`](docs/GLOSSARY.md).
16
17
 
17
18
  ---
18
19
 
@@ -25,8 +26,8 @@ diferente das PRs. O motor só coordena a caçada — as regras são problema se
25
26
 
26
27
  - **Bun** — runtime obrigatório.
27
28
  - **git** — o diff usa `git diff <base>...<head>`.
28
- - Opcionais, ambos **falha-aberto**: **`gh`** (contexto da PR) e **`TYPESAFE_API_KEY`**
29
- (pré-análise Jev). Sem eles, a revisão roda igual.
29
+ - Opcionais: **`gh`** (contexto da PR) e **`TYPESAFE_API_KEY`** (pré-análise Jev). A revisão
30
+ funciona normalmente sem eles; o `houndsman doctor` valida a credencial Jev quando presente.
30
31
 
31
32
  ### 2. Instale o CLI (global)
32
33
 
@@ -51,15 +52,13 @@ Cria a camada `.houndsman/`, que é **autocontida** (não instala nada no seu pr
51
52
  | `.houndsman/exclusions.ts` | você | Arquivos que nunca entram na revisão |
52
53
  | `.houndsman/doc-mappings.ts` | você | path → documentação de contexto |
53
54
  | `.houndsman/core/**` | auto | Tipos + docs do skill (sincronizados por `houndsman update`) |
54
- | `.agents/` · `.claude/` | auto | Ponteiros de skill dos harnesses |
55
- | `package.json` | — | Adiciona o script `"houndsman"` |
56
55
 
57
56
  > **Não existe "modo completo".** O `setup` já cria a camada inicial com um watchdog de
58
57
  > exemplo — troque o exemplo pelas regras reais do seu projeto (ou use `/houndsman-update`).
59
58
 
60
59
  ### 4. Escreva suas regras
61
60
 
62
- Edite `.houndsman/watchdogs/` — veja [Escrevendo boas regras](#escrevendo-boas-regras).
61
+ Edite `.houndsman/watchdogs/` — veja [Escrevendo boas regras](docs/AVANCADO.md#escrevendo-boas-regras).
63
62
 
64
63
  ### 5. Valide
65
64
 
@@ -87,7 +86,8 @@ Não precisa rodar nenhum script à mão.
87
86
  ├─ Passo 0 contexto da PR (gh, opcional)
88
87
  ├─ Passo 1 classifica o diff → briefs por watchdog
89
88
  ├─ Passo 2 N subagentes revisam em paralelo
90
- └─ Passo 3 consolida em review.md
89
+ ├─ Passo 3 consolida em review.md + state.json
90
+ └─ Passo 4 triagem (UI local ou agente)
91
91
  ```
92
92
 
93
93
  - Revisão normal: só `/houndsman` (a branch é detectada; base default `main`).
@@ -98,6 +98,32 @@ Não precisa rodar nenhum script à mão.
98
98
 
99
99
  ---
100
100
 
101
+ ## Triando os findings (opcional)
102
+
103
+ Depois do `--finalize`, cada finding tem um `id` curto e um `fingerprint` de **conteúdo**
104
+ no `.houndsman/.runs/<branch-slug>/state.json`. Você decide o que fazer com cada um — pela
105
+ **UI local** ou pelo **agente** (auto-fix) — sem abrir o JSON à mão:
106
+
107
+ ```bash
108
+ bun run houndsman server --open # UI: triar, comentar, marcar feito, ler artefatos
109
+ ```
110
+
111
+ - **decision:** `pending` · `execute` · `ignore` (+ comentário).
112
+ - **execState:** `pending` · `in_progress` · `done` · `stale`.
113
+
114
+ **Auto-fix pelo agente** — as linhas mudam depois do fix, e o motor reancora por conteúdo:
115
+
116
+ ```bash
117
+ bun run houndsman findings list --decision execute --json
118
+ bun run houndsman findings sync
119
+ bun run houndsman findings done <id>
120
+ ```
121
+
122
+ > A identidade do finding é o **conteúdo** (fingerprint), não a linha: itens deslocados
123
+ > pelo fix não se perdem. O estado é local e mora em `.runs/<slug>/state.json` (gitignored).
124
+
125
+ ---
126
+
101
127
  ## Mantendo a configuração
102
128
 
103
129
  - **Alterar regras** → use o skill **`/houndsman-update`**. Ele prova no código que o padrão
@@ -116,80 +142,15 @@ Não precisa rodar nenhum script à mão.
116
142
 
117
143
  ---
118
144
 
119
- ## Avançado
120
-
121
- ### Comandos do dia a dia
122
-
123
- | Comando | Função |
124
- |---|---|
125
- | `houndsman setup` | Cria a configuração inicial do projeto |
126
- | `houndsman update` | Sincroniza os arquivos auto-gerados (`.houndsman/core`) |
127
- | `houndsman doctor` | Valida a configuração e o ambiente (anti-drift) |
128
- | `houndsman context` | Passo 0 — contexto da PR via `gh` |
129
- | `houndsman --base main` | Passo 1 — prepara os briefs |
130
- | `houndsman --finalize` | Passo 3 — gera o relatório |
131
- | `houndsman watchdog <id>` | Contexto de um watchdog (`--rules`, `--json`) |
132
-
133
- Flags: `--base <branch>` (default `main`) · `--head <ref>` (default `HEAD`) ·
134
- `--branch <nome>` · `--dir <dir>` · `--config <path>` · `--clean` · `--no-jev` ·
135
- `--fail-on-blocking` (com `--finalize`).
136
-
137
- ### Escrevendo boas regras
138
-
139
- O texto da regra é renderizado **literalmente** no brief do subagente — ambiguidade vira
140
- ruído na revisão. Toda regra começa com um **prefixo de polaridade**:
141
-
142
- - `Promover:` → algo que **DEVE** acontecer (o padrão precisa existir no código).
143
- - `Evitar:` → algo que **NÃO DEVE** acontecer. `Não` e `Nunca` no início são equivalentes.
144
-
145
- Boas práticas:
146
-
147
- 1. **Só regra que o projeto aplica hoje** — prove com evidência (`arquivo:linha` para
148
- afirmativas; `0 ocorrências via grep` do anti-padrão para negativas). Regra afirmativa sem
149
- aplicação real gera falso positivo.
150
- 2. **Geral e reutilizável** — descreva um padrão do domínio, não uma regra de negócio
151
- amarrada a uma única classe/use-case (isso é papel de teste automatizado).
152
- 3. **Condição inequívoca e critério concreto** — duas IAs chegam à mesma conclusão; use
153
- limite/lista explícita ou símbolo real do código.
154
- 4. **Sem jargão vago nem qualificador vazio** — nada de "o antigo", "isso", "apenas por
155
- compatibilidade".
156
- 5. **Declare o `classify`** do watchdog — regex de path (`filename`) ou de conteúdo
157
- (`content`) para o motor achar os arquivos do domínio.
158
- 6. **Prefira editar via `/houndsman-update`** — ele aplica a rubrica completa e roda as
159
- verificações (`doctor`, `test`).
160
-
161
- > `houndsman doctor` emite um aviso (não bloqueante) para regra sem prefixo de polaridade.
162
-
163
- Exemplo:
164
-
165
- ```ts
166
- { severity: "blocking", text: "Evitar: handler de webhook processa o payload antes de validar a assinatura do provider." }
167
- { severity: "attention", text: "Promover: rotas validam a entrada com o schema do domínio." }
168
- ```
169
-
170
- ### Estrutura gerada por `setup`
171
-
172
- ```
173
- houndsman.config.ts
174
- .houndsman/
175
- ├── .gitignore # AUTO
176
- ├── core/ # AUTO — sincronizado por `update`
177
- │ ├── types.ts
178
- │ └── skill/*.md
179
- ├── watchdogs/ # você — 1 módulo .ts por watchdog
180
- │ └── index.ts
181
- ├── exclusions.ts # você
182
- └── doc-mappings.ts # você
183
- ```
184
-
185
- Os artefatos de cada review ficam em `.houndsman/.runs/<branch-slug>/` (auto-ignorados).
186
-
187
- ---
188
-
189
145
  ## Documentação
190
146
 
191
- - **`docs/CONTRIBUTING.md`** — arquitetura, fluxos internos, princípios e desenvolvimento da
147
+ - `docs/AVANCADO.md` — como funciona por dentro (diagrama do fluxo e onde o Jev entra),
148
+ referência de comandos e boas práticas para escrever regras.
149
+ - `docs/GLOSSARY.md` — glossário do vocabulário de domínio (brief, findings, manifest,
150
+ budget, overflow, churn, candidato, Jev…): o que cada termo significa **neste** projeto.
151
+ - `docs/CONTRIBUTING.md` — arquitetura, fluxos internos, princípios e desenvolvimento da
192
152
  lib (para quem vai contribuir).
153
+
193
154
  - `src/skill/SETUP.md` — referência de configuração passo a passo.
194
155
  - `src/skill/houndsman.md` — orquestração do skill `/houndsman`.
195
156
  - `src/skill/houndsman-update.md` — manutenção das regras dos watchdogs.
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payconductor/houndsman",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Orquestrador de revisão de Pull Requests: classifica o diff, despacha watchdogs especializados por domínio e consolida o relatório.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -54,7 +54,8 @@
54
54
  "ci:format:fix": "biome format --fix"
55
55
  },
56
56
  "dependencies": {
57
- "axios": "^1.12.2"
57
+ "axios": "^1.12.2",
58
+ "preact": "^11.0.1"
58
59
  },
59
60
  "devDependencies": {
60
61
  "@biomejs/biome": "2.2.4",
package/src/cli/index.ts CHANGED
@@ -11,9 +11,11 @@
11
11
  * (default) Passo 1 — preparar (classificação + briefs + manifesto)
12
12
  * --finalize Passo 3 — consolidar os findings em review.md
13
13
  * context Passo 0 — coletar o contexto da PR via `gh`
14
+ * findings <sub> Triagem dos findings (list/set/done/sync/...)
15
+ * server Sobe a UI local de triagem (Bun.serve)
14
16
  * setup Cria a configuração inicial do projeto (config + skills)
15
17
  * update Sincroniza os arquivos auto-gerados (.houndsman/core)
16
- * doctor Valida a configuração e o ambiente (anti-drift)
18
+ * doctor Valida a configuração, o ambiente e a credencial Jev (anti-drift)
17
19
  * test Roda a suíte de testes da lib
18
20
  * watchdog <id> Contexto de um watchdog (`--rules`, `--json`)
19
21
  *
@@ -22,6 +24,7 @@
22
24
  */
23
25
 
24
26
  import { runContextCommand } from "../core/commands/context";
27
+ import { runFindingsCommand } from "../core/commands/findings";
25
28
  import { parseReviewArgs } from "../core/commands/review";
26
29
  import { runSetupCommand } from "../core/commands/setup";
27
30
  import { runLibTests } from "../core/commands/test";
@@ -29,9 +32,11 @@ import { runUpdateCommand } from "../core/commands/update";
29
32
  import { renderWatchdogContext } from "../core/commands/watchdog";
30
33
  import { type HoundsmanConfig, resolveArtifactDir } from "../core/config";
31
34
  import { loadConfig } from "../core/config-loader";
35
+ import { JevClient } from "../core/jev";
32
36
  import { finalize, prepare, resolveBranch } from "../core/orchestrator";
33
37
  import { relFromRoot } from "../core/paths";
34
38
  import { checkRegistry } from "../core/registry-check";
39
+ import { startServer } from "../server";
35
40
 
36
41
  const HELP = `Uso: houndsman [comando] [flags]
37
42
 
@@ -39,9 +44,11 @@ Comandos:
39
44
  (nenhum) Prepara a revisão (classification + briefs + manifesto)
40
45
  --finalize Consolida os findings em review.md
41
46
  context Coleta o contexto da PR via gh (Passo 0)
47
+ findings <sub> Triagem dos findings (list · show · set · done · reset · sync · summary · path)
48
+ server Sobe a UI local de triagem (--port, --host, --open)
42
49
  setup Cria a configuração inicial do projeto (config + skills)
43
50
  update Sincroniza os arquivos auto-gerados (.houndsman/core)
44
- doctor Valida a configuração e o ambiente (anti-drift)
51
+ doctor Valida a configuração, o ambiente e a credencial Jev
45
52
  test Roda a suíte de testes da lib
46
53
  watchdog <id> Contexto de um watchdog (--rules, --json)
47
54
 
@@ -53,7 +60,18 @@ Flags:
53
60
  --dir <dir> Diretório dos artefatos
54
61
  --clean Descarta findings anteriores
55
62
  --no-jev Desativa a pré-análise Jev
56
- --fail-on-blocking Exit 1 se houver bloqueantes (com --finalize)`;
63
+ --fail-on-blocking Exit 1 se houver bloqueantes (com --finalize)
64
+
65
+ Flags de findings:
66
+ --decision <d> pending | execute | ignore
67
+ --exec <e> pending | in_progress | done | stale
68
+ --comment <text> Comentário/instrução adicional
69
+ --watchdog <id> Filtra por watchdog (list)
70
+ --severity <s> Filtra por severidade (list)
71
+ --file <text> Filtra por trecho do caminho (list)
72
+ --all Inclui findings ausentes do último relatório
73
+ --json Saída JSON
74
+ --limit <n> Limita o nº de itens (list)`;
57
75
 
58
76
  function readFlag(args: string[], flag: string): string | undefined {
59
77
  const index = args.indexOf(flag);
@@ -88,8 +106,8 @@ function runContext(config: HoundsmanConfig, args: string[]): void {
88
106
  }
89
107
  }
90
108
 
91
- /** Valida a configuração do projeto e o ambiente (anti-drift). */
92
- function runDoctor(config: HoundsmanConfig): void {
109
+ /** Valida a configuração do projeto e o ambiente. */
110
+ async function runDoctor(config: HoundsmanConfig): Promise<void> {
93
111
  console.log("→ Validando configuração do projeto...");
94
112
  const { problems, warnings, jevQuestions } = checkRegistry(config);
95
113
 
@@ -110,9 +128,27 @@ function runDoctor(config: HoundsmanConfig): void {
110
128
  console.log("\n→ Ambiente:");
111
129
  console.log(` ${Bun.which("git") ? "✅" : "❌"} git`);
112
130
  console.log(` ${Bun.which("gh") ? "✅" : "⚠️ "} gh (opcional — contexto da PR)`);
113
- console.log(
114
- ` ${process.env.TYPESAFE_API_KEY ? "✅" : "⚠️ "} TYPESAFE_API_KEY (opcional — pré-análise Jev)`,
115
- );
131
+ await reportJevCredential();
132
+ }
133
+
134
+ /**
135
+ * Reporta o estado da credencial Jev: presença e validade (chamada mínima).
136
+ * Fail-open — nunca altera o exit code (`doctor` é diagnóstico).
137
+ */
138
+ async function reportJevCredential(): Promise<void> {
139
+ const client = new JevClient();
140
+ if (!client.isConfigured()) {
141
+ console.log(" ⚠️ TYPESAFE_API_KEY (opcional — pré-análise Jev)");
142
+ return;
143
+ }
144
+ const check = await client.validateCredential();
145
+ if (check.ok) {
146
+ console.log(` ✅ Jev: credencial válida (modelo ${check.model}).`);
147
+ } else if (check.kind === "invalid") {
148
+ console.log(` ❌ Jev: credencial inválida — ${check.detail}`);
149
+ } else {
150
+ console.log(` ⚠️ Jev: não foi possível validar — ${check.detail}`);
151
+ }
116
152
  }
117
153
 
118
154
  /** Contexto de um watchdog. */
@@ -137,6 +173,44 @@ function runWatchdog(config: HoundsmanConfig, args: string[]): void {
137
173
  );
138
174
  }
139
175
 
176
+ /**
177
+ * Subcomando `findings` — triagem dos findings (list/show/set/done/reset/
178
+ * sync/summary/path). Deixa o agente interagir com o estado sem abrir o JSON.
179
+ */
180
+ function runFindings(config: HoundsmanConfig, args: string[]): void {
181
+ const result = runFindingsCommand(config, args);
182
+ if (result.exitCode !== 0) {
183
+ console.error(result.output);
184
+ process.exit(result.exitCode);
185
+ }
186
+ console.log(result.output);
187
+ }
188
+
189
+ /** Abre a URL no navegador padrão (best-effort, por plataforma). */
190
+ function openBrowser(url: string): void {
191
+ try {
192
+ const command =
193
+ process.platform === "win32"
194
+ ? ["cmd", "/c", "start", "", url]
195
+ : process.platform === "darwin"
196
+ ? ["open", url]
197
+ : ["xdg-open", url];
198
+ Bun.spawn(command, { stdout: "ignore", stderr: "ignore" });
199
+ } catch {
200
+ // best-effort — se não abrir, o usuário copia a URL.
201
+ }
202
+ }
203
+
204
+ /** Subcomando `server` — sobe a UI local de triagem. */
205
+ function runServer(config: HoundsmanConfig, args: string[]): void {
206
+ const port = Number(readFlag(args, "--port")) || 7331;
207
+ const host = readFlag(args, "--host") ?? "127.0.0.1";
208
+ const { url } = startServer({ config, port, host });
209
+ console.log(`[houndsman] servidor em ${url}`);
210
+ console.log("[houndsman] Ctrl+C para encerrar.");
211
+ if (args.includes("--open")) openBrowser(url);
212
+ }
213
+
140
214
  /** Passo 1/3 — preparar ou finalizar. */
141
215
  async function runReview(config: HoundsmanConfig, args: string[]): Promise<void> {
142
216
  const opts = parseReviewArgs(args, resolveArtifactDir(config));
@@ -187,11 +261,17 @@ export async function main(): Promise<void> {
187
261
  return;
188
262
  case "doctor":
189
263
  case "check":
190
- runDoctor(config);
264
+ await runDoctor(config);
191
265
  return;
192
266
  case "watchdog":
193
267
  runWatchdog(config, rest);
194
268
  return;
269
+ case "findings":
270
+ runFindings(config, rest);
271
+ return;
272
+ case "server":
273
+ runServer(config, rest);
274
+ return;
195
275
  case undefined:
196
276
  await runReview(config, rest);
197
277
  return;
@@ -11,6 +11,9 @@ import { join } from "node:path";
11
11
  /** Diretório de assets markdown embarcados no pacote. */
12
12
  export const SKILL_DIR = join(import.meta.dir, "..", "skill");
13
13
 
14
+ /** Diretório de assets de mídia do pacote (root `assets/` — logo etc.). */
15
+ export const MEDIA_DIR = join(import.meta.dir, "..", "..", "assets");
16
+
14
17
  /** Nome do arquivo de schema de output dos watchdogs. */
15
18
  export const OUTPUT_FORMAT_FILE = "OUTPUT-FORMAT.md";
16
19
 
@@ -18,3 +21,8 @@ export const OUTPUT_FORMAT_FILE = "OUTPUT-FORMAT.md";
18
21
  export function assetPath(fileName: string): string {
19
22
  return join(SKILL_DIR, fileName);
20
23
  }
24
+
25
+ /** Caminho absoluto de um asset de mídia em `assets/` (ex.: a logo). */
26
+ export function mediaPath(fileName: string): string {
27
+ return join(MEDIA_DIR, fileName);
28
+ }
package/src/core/brief.ts CHANGED
@@ -78,14 +78,35 @@ function optionalList(title: string, items: readonly string[] | undefined): stri
78
78
  return [`## ${title}`, "", ...items.map((item) => `- ${item}`), ""];
79
79
  }
80
80
 
81
- /** Lista de arquivos do domínio com status. */
81
+ /** Lista de arquivos do domínio com status (e churn, quando disponível). */
82
82
  function fileItems(args: WatchdogRenderArgs): string[] {
83
83
  return args.files.map((file) => {
84
84
  const status = args.statuses[file]?.status ?? "modified";
85
- return `- \`${file}\` (${status})`;
85
+ const churn = args.churn?.[file];
86
+ const delta = churn ? `, +${churn.additions}/−${churn.deletions}` : "";
87
+ return `- \`${file}\` (${status}${delta})`;
86
88
  });
87
89
  }
88
90
 
91
+ /**
92
+ * Arquivos do domínio fora do orçamento (triage de PRs grandes): o watchdog NÃO
93
+ * deve lê-los, mas precisa registrá-los em `skipped` — cobertura honesta.
94
+ */
95
+ function overflowSection(args: WatchdogRenderArgs): string[] {
96
+ const overflow = args.overflow ?? [];
97
+ if (overflow.length === 0) return [];
98
+ return [
99
+ "## Fora do orçamento (NÃO revisar)",
100
+ "",
101
+ `> ${overflow.length} arquivo(s) do seu domínio ficaram fora do orçamento`,
102
+ "> (menor churn). Não os leia: registre-os em `skipped` com o motivo",
103
+ '> "budget" para que a cobertura do relatório fique honesta.',
104
+ "",
105
+ ...overflow.map(codeItem),
106
+ "",
107
+ ];
108
+ }
109
+
89
110
  /** Seção de deletados (breaking change) — só p/ watchdogs com `breakingChange`. */
90
111
  function deletedSection(args: WatchdogRenderArgs, def: WatchdogDefinition): string[] {
91
112
  if (!def.breakingChange || args.deleted.length === 0) return [];
@@ -141,6 +162,8 @@ export function defaultRenderBrief(
141
162
  "",
142
163
  );
143
164
 
165
+ lines.push(...overflowSection(args));
166
+
144
167
  if (args.candidates && args.candidates.length > 0) {
145
168
  lines.push(...renderBriefCandidates(args.candidates));
146
169
  }