@payconductor/houndsman 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +25 -39
  2. package/package.json +1 -1
  3. package/src/cli/commands/context-command.ts +11 -9
  4. package/src/cli/commands/doctor-command.ts +5 -0
  5. package/src/cli/commands/findings-command.ts +13 -3
  6. package/src/cli/commands/review-command.ts +1 -2
  7. package/src/cli/commands/status-command.ts +46 -0
  8. package/src/cli/help.ts +8 -3
  9. package/src/cli/index.ts +5 -2
  10. package/src/core/commands/context.ts +29 -13
  11. package/src/core/commands/findings.ts +216 -125
  12. package/src/core/commands/review.ts +7 -8
  13. package/src/core/commands/scaffold.ts +15 -16
  14. package/src/core/commands/status.ts +63 -0
  15. package/src/core/config.ts +16 -7
  16. package/src/core/engine/houndsman-engine.ts +15 -1
  17. package/src/core/engine/review-scope.ts +26 -8
  18. package/src/core/engine/review-session.ts +37 -9
  19. package/src/core/findings-state.ts +53 -75
  20. package/src/core/github-review.ts +620 -57
  21. package/src/core/jev/jev-candidates.ts +49 -35
  22. package/src/core/orchestrator.ts +271 -134
  23. package/src/core/paths.ts +26 -17
  24. package/src/core/pr-context.ts +19 -5
  25. package/src/core/project-config.ts +4 -5
  26. package/src/core/renderers/manifest-writer.ts +29 -18
  27. package/src/core/renderers/resume-renderer.ts +151 -0
  28. package/src/core/report-builder.ts +7 -4
  29. package/src/core/run-layout.ts +4 -104
  30. package/src/core/run-status.ts +59 -50
  31. package/src/core/services/git-repository.ts +16 -0
  32. package/src/core/services/project-files.ts +13 -1
  33. package/src/core/storage/factory.ts +24 -0
  34. package/src/core/storage/file/file-storage.ts +271 -0
  35. package/src/core/storage/paths.ts +64 -0
  36. package/src/core/storage/repo-identity.ts +70 -0
  37. package/src/core/storage/sqlite/mappers.ts +162 -0
  38. package/src/core/storage/sqlite/schema.ts +116 -0
  39. package/src/core/storage/sqlite/sqlite-storage.ts +431 -0
  40. package/src/core/storage/types.ts +152 -0
  41. package/src/core/storage/work-area.ts +191 -0
  42. package/src/index.ts +27 -6
  43. package/src/server/artifacts.ts +8 -61
  44. package/src/server/client/App.tsx +26 -9
  45. package/src/server/client/ArtifactsPanel.tsx +144 -23
  46. package/src/server/client/AutoRefresh.tsx +1 -1
  47. package/src/server/client/Filters.tsx +7 -2
  48. package/src/server/client/FindingCard.tsx +50 -15
  49. package/src/server/client/FindingsList.tsx +2 -2
  50. package/src/server/client/MarkdownText.tsx +181 -0
  51. package/src/server/client/ResumePanel.tsx +368 -0
  52. package/src/server/client/RunList.tsx +3 -3
  53. package/src/server/client/SubmitModal.tsx +58 -10
  54. package/src/server/client/SummaryBar.tsx +1 -1
  55. package/src/server/client/icons.tsx +17 -0
  56. package/src/server/client/utils/code-highlight.ts +422 -0
  57. package/src/server/client/utils/constants.ts +226 -0
  58. package/src/server/client/utils/json-highlight.ts +55 -0
  59. package/src/server/client/utils/markdown.ts +319 -0
  60. package/src/server/client/{types.ts → utils/types.ts} +81 -0
  61. package/src/server/index.ts +154 -16
  62. package/src/server/run-repository.ts +153 -136
  63. package/src/skill/OUTPUT-FORMAT.md +6 -4
  64. package/src/skill/README.md +22 -14
  65. package/src/skill/SETUP.md +4 -4
  66. package/src/skill/example-review.md +22 -14
  67. package/src/skill/fix.md +5 -5
  68. package/src/skill/hunt.md +55 -32
  69. package/src/skill/train.md +1 -1
  70. package/src/skill/verify.md +7 -6
  71. package/src/tests/__snapshots__/baseline.test.ts.snap +74 -61
  72. package/src/tests/baseline.test.ts +79 -34
  73. package/src/tests/cli.test.ts +40 -19
  74. package/src/tests/code-highlight.test.ts +123 -0
  75. package/src/tests/domain.test.ts +1 -1
  76. package/src/tests/findings-commands.test.ts +153 -131
  77. package/src/tests/findings-state.test.ts +56 -5
  78. package/src/tests/fixtures/fake-project.ts +0 -1
  79. package/src/tests/github-review.test.ts +409 -60
  80. package/src/tests/infrastructure.test.ts +33 -38
  81. package/src/tests/markdown.test.ts +149 -0
  82. package/src/tests/review-resume.test.ts +101 -0
  83. package/src/tests/review-scope.test.ts +302 -0
  84. package/src/tests/review-scripts.test.ts +81 -5
  85. package/src/tests/run-status-view.test.ts +94 -0
  86. package/src/tests/run-status.test.ts +86 -23
  87. package/src/tests/server.test.ts +184 -96
  88. package/src/tests/storage.test.ts +449 -0
  89. package/src/server/client/constants.ts +0 -91
  90. /package/src/server/client/{api.ts → utils/api.ts} +0 -0
  91. /package/src/server/client/{format.ts → utils/format.ts} +0 -0
@@ -1,24 +1,18 @@
1
1
  /**
2
2
  * Subcomando `findings` — triagem dos findings da revisão (biblioteca).
3
3
  *
4
- * Lê/grava o `state.json` da run (`findings-state.ts`). Devolve o texto pronto
5
- * (markdown ou JSON) + exit code; o CLI decide o log/exit. O agente do usuário
6
- * usa `list`/`set`/`done` (e `batch` para lotes de correção) para interagir sem abrir o JSON à mão.
4
+ * Lê/grava o estado de triagem no storage (SQLite por default). Devolve o texto
5
+ * pronto (markdown ou JSON) + exit code; o CLI decide o log/exit. O agente do
6
+ * usuário usa `list`/`set`/`done` (e `batch` para lotes de correção) para
7
+ * interagir sem abrir o banco à mão.
7
8
  *
8
- * Se o `state.json` não existir, é re-construído a partir de
9
- * `findings/*.json` + `classification.json` (não exige `--finalize`).
9
+ * Ids aceitam prefixo único (exato primeiro) — útil porque o id curto convive
10
+ * com o UUID interno no banco.
10
11
  */
11
12
 
12
- import { writeFileSync } from "node:fs";
13
- import { join } from "node:path";
14
- import {
15
- type HoundsmanConfig,
16
- resolveArtifactDir,
17
- resolveJevConcurrency,
18
- } from "../config";
13
+ import { type HoundsmanConfig, resolveJevConcurrency } from "../config";
19
14
  import {
20
15
  applyFindingUpdate,
21
- buildStateFromRunDir,
22
16
  FINDING_DECISIONS,
23
17
  FINDING_EXEC_STATES,
24
18
  type FindingDecision,
@@ -26,11 +20,11 @@ import {
26
20
  type FindingState,
27
21
  type FindingsFilter,
28
22
  type FindingsStateFile,
29
- FindingsStateStore,
30
23
  type FindingUpdate,
31
24
  fileReader,
32
25
  filterFindings,
33
- runDir,
26
+ reorder,
27
+ resolveFindingId,
34
28
  summarize,
35
29
  syncState,
36
30
  } from "../findings-state";
@@ -42,15 +36,19 @@ import {
42
36
  type SubmitPreview,
43
37
  type SubmitSelection,
44
38
  submitFindings,
39
+ syncResolvedThreads,
45
40
  } from "../github-review";
46
- import { relFromRoot, VERIFY_FILE } from "../paths";
41
+ import { VERIFY_FILE } from "../paths";
47
42
  import { currentBranch } from "../pr-context";
48
43
  import { BatchPresenter } from "../renderers/batch-presenter";
49
44
  import { FindingsPresenter } from "../renderers/findings-presenter";
50
45
  import { VerifyPresenter } from "../renderers/verify-presenter";
51
46
  import { pingFindingsStatus } from "../run-status";
47
+ import { systemClock } from "../services/clock";
52
48
  import { FixBatchBuilder } from "../services/fix-batch-builder";
53
49
  import { Verifier } from "../services/verifier";
50
+ import type { RepoIdentity, ReviewRecord, StorageProvider } from "../storage/types";
51
+ import { ReviewWorkArea } from "../storage/work-area";
54
52
  import { SEVERITY_ORDER, type Severity } from "../types";
55
53
 
56
54
  export type FindingsCommandResult = {
@@ -65,7 +63,6 @@ const VALUE_FLAGS = new Set([
65
63
  "--severity",
66
64
  "--file",
67
65
  "--branch",
68
- "--dir",
69
66
  "--limit",
70
67
  "--comment",
71
68
  "--by",
@@ -85,6 +82,11 @@ function parseArgs(args: string[]): ParsedArgs {
85
82
  const parsed: ParsedArgs = { positionals: [], values: {}, bools: new Set() };
86
83
  for (let i = 0; i < args.length; i++) {
87
84
  const arg = args[i];
85
+ if (arg === "--dir") {
86
+ throw new Error(
87
+ "--dir foi removido; configure `storage` no houndsman.config.ts.",
88
+ );
89
+ }
88
90
  if (arg.startsWith("--")) {
89
91
  if (VALUE_FLAGS.has(arg)) {
90
92
  const value = args[i + 1];
@@ -101,36 +103,63 @@ function parseArgs(args: string[]): ParsedArgs {
101
103
  return parsed;
102
104
  }
103
105
 
104
- /** Diretório da run + branch resolvida. */
105
- function resolveDir(
106
- config: HoundsmanConfig,
106
+ /** Contexto de uma review (storage + registro + staging). */
107
+ type CommandContext = {
108
+ storage: StorageProvider;
109
+ review: ReviewRecord;
110
+ work: ReviewWorkArea;
111
+ };
112
+
113
+ /** Resolve a review da branch atual (explícita ou via git). */
114
+ function openContext(
115
+ storage: StorageProvider,
116
+ repo: RepoIdentity,
107
117
  parsed: ParsedArgs,
108
- ): { dir: string; branch: string } {
109
- return {
110
- dir: parsed.values["--dir"] ?? resolveArtifactDir(config),
111
- branch: parsed.values["--branch"] ?? currentBranch(),
112
- };
118
+ ): CommandContext {
119
+ const branch = parsed.values["--branch"] ?? currentBranch();
120
+ const review = storage.findReview(repo.key, branch);
121
+ if (!review) {
122
+ throw new Error(
123
+ `Nenhuma revisão para '${branch}'. Rode \`houndsman --finalize\` primeiro.`,
124
+ );
125
+ }
126
+ return { storage, review, work: new ReviewWorkArea(review) };
113
127
  }
114
128
 
115
- /** Lê o estado da run; reconstrói dos findings se ausente. Lança se impossível. */
116
- function loadState(config: HoundsmanConfig, parsed: ParsedArgs): FindingsStateFile {
117
- const { dir, branch } = resolveDir(config, parsed);
118
- const store = FindingsStateStore.at(dir, branch);
119
- const existing = store.read();
120
- if (existing) return existing;
121
-
122
- const built = buildStateFromRunDir(
123
- runDir(dir, branch),
124
- branch,
125
- fileReader(process.cwd()),
126
- );
127
- if (!built) {
129
+ /** Lê o estado de triagem; lança se a review ainda não tem findings. */
130
+ function loadState(ctx: CommandContext): FindingsStateFile {
131
+ const state = ctx.storage.readFindings(ctx.review.id);
132
+ if (!state) {
128
133
  throw new Error(
129
- `Nenhum estado de triagem para '${branch}'. Rode \`houndsman --finalize\` primeiro.`,
134
+ `Nenhum estado de triagem para '${ctx.review.branch}'. Rode \`houndsman --finalize\` primeiro.`,
130
135
  );
131
136
  }
132
- store.write(built);
133
- return built;
137
+ return state;
138
+ }
139
+
140
+ /** Lê-modifica-grava o estado, atualizando `updatedAt` e a ordem. */
141
+ function mutateState(
142
+ ctx: CommandContext,
143
+ mutator: (state: FindingsStateFile) => void,
144
+ ): FindingsStateFile {
145
+ const state = loadState(ctx);
146
+ mutator(state);
147
+ state.updatedAt = systemClock();
148
+ reorder(state);
149
+ ctx.storage.writeFindings(ctx.review.id, state);
150
+ return state;
151
+ }
152
+
153
+ /** Resolve um id (exato ou prefixo único), lançando erro explicativo. */
154
+ function requireFindingId(state: FindingsStateFile, input: string): string {
155
+ const resolution = resolveFindingId(state, input);
156
+ if (resolution.status === "found") return resolution.id;
157
+ if (resolution.status === "ambiguous") {
158
+ throw new Error(
159
+ `Id '${input}' ambíguo — candidatos: ${resolution.matches.join(", ")}.`,
160
+ );
161
+ }
162
+ throw new Error(`Finding '${input}' não encontrado.`);
134
163
  }
135
164
 
136
165
  // ---------------------------------------------------------------------------
@@ -181,64 +210,71 @@ function filterFrom(parsed: ParsedArgs): FindingsFilter {
181
210
  };
182
211
  }
183
212
 
184
- /** Faz ping no `run.json` (status derivado do estado atual) após uma mutação. */
185
- function pingStatus(config: HoundsmanConfig, parsed: ParsedArgs): void {
186
- const { dir, branch } = resolveDir(config, parsed);
187
- const state = FindingsStateStore.at(dir, branch).read();
188
- if (state) pingFindingsStatus(runDir(dir, branch), state);
213
+ /** Faz ping no status da review (derivado do estado atual) após uma mutação. */
214
+ function pingStatus(ctx: CommandContext, state: FindingsStateFile): void {
215
+ pingFindingsStatus(ctx.storage, ctx.review.id, state);
216
+ }
217
+
218
+ /**
219
+ * Resolve no GitHub as threads dos findings recém-resolvidos localmente.
220
+ * Fail-open (`gh` ausente/sem PR ⇒ só a mensagem). Devolve a linha para o
221
+ * output ou `null` quando não há vínculo remoto pendente.
222
+ */
223
+ function syncThreadsOutput(ctx: CommandContext, ids: readonly string[]): string | null {
224
+ if (ids.length === 0) return null;
225
+ const state = loadState(ctx);
226
+ const outcome = syncResolvedThreads({ state, ids });
227
+ if (outcome.checked === 0) return null;
228
+ if (outcome.updated.length > 0) ctx.storage.writeFindings(ctx.review.id, state);
229
+ if (!outcome.available) {
230
+ return `GitHub: threads não sincronizadas (${outcome.reason}).`;
231
+ }
232
+ const failures =
233
+ outcome.failed.length > 0 ? `, ${outcome.failed.length} falha(s)` : "";
234
+ return `GitHub: ${outcome.resolved.length} thread(s) resolvida(s), ${outcome.skipped.length} já resolvida(s)/pulada(s)${failures}.`;
189
235
  }
190
236
 
191
237
  /** Aplica uma atualização a um finding e persiste. */
192
238
  function updateOne(
193
- config: HoundsmanConfig,
194
- parsed: ParsedArgs,
195
- id: string,
239
+ ctx: CommandContext,
240
+ input: string,
196
241
  update: FindingUpdate,
197
242
  ): FindingState {
198
- const { dir, branch } = resolveDir(config, parsed);
199
- const store = FindingsStateStore.at(dir, branch);
200
243
  let updated: FindingState | undefined;
201
- store.update((state) => {
202
- const finding = state.findings[id];
203
- if (!finding) return;
204
- updated = applyFindingUpdate(finding, update);
244
+ mutateState(ctx, (state) => {
245
+ const id = requireFindingId(state, input);
246
+ updated = applyFindingUpdate(state.findings[id], update);
205
247
  });
206
- if (!updated) throw new Error(`Finding '${id}' não encontrado.`);
248
+ if (!updated) throw new Error(`Finding '${input}' não encontrado.`);
207
249
  return updated;
208
250
  }
209
251
 
210
252
  /** Aplica a mesma atualização a vários ids (done/reset). */
211
253
  function updateMany(
212
- config: HoundsmanConfig,
213
- parsed: ParsedArgs,
214
- ids: string[],
254
+ ctx: CommandContext,
255
+ inputs: string[],
215
256
  update: FindingUpdate,
216
257
  ): FindingState[] {
217
- const { dir, branch } = resolveDir(config, parsed);
218
- const store = FindingsStateStore.at(dir, branch);
219
258
  const updated: FindingState[] = [];
220
- store.update((state) => {
259
+ mutateState(ctx, (state) => {
260
+ const ids = inputs.map((input) => requireFindingId(state, input));
221
261
  for (const id of ids) {
222
- const finding = state.findings[id];
223
- if (!finding) continue;
224
- updated.push(applyFindingUpdate(finding, update));
262
+ updated.push(applyFindingUpdate(state.findings[id], update));
225
263
  }
226
264
  });
227
- const missing = ids.filter((id) => !updated.some((f) => f.id === id));
228
- if (missing.length > 0) {
229
- throw new Error(`Findings não encontrados: ${missing.join(", ")}.`);
230
- }
231
265
  return updated;
232
266
  }
233
267
 
234
268
  /** Executa um subcomando de `findings` e devolve o texto + exit code. */
235
269
  export function runFindingsCommand(
236
270
  config: HoundsmanConfig,
271
+ storage: StorageProvider,
272
+ repo: RepoIdentity,
237
273
  args: string[],
238
274
  ): FindingsCommandResult {
239
- const parsed = parseArgs(args);
240
275
  try {
241
- return execute(config, parsed);
276
+ const parsed = parseArgs(args);
277
+ return execute(config, storage, repo, parsed);
242
278
  } catch (error) {
243
279
  return {
244
280
  output: error instanceof Error ? error.message : String(error),
@@ -247,10 +283,16 @@ export function runFindingsCommand(
247
283
  }
248
284
  }
249
285
 
250
- function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandResult {
286
+ function execute(
287
+ _config: HoundsmanConfig,
288
+ storage: StorageProvider,
289
+ repo: RepoIdentity,
290
+ parsed: ParsedArgs,
291
+ ): FindingsCommandResult {
292
+ const ctx = openContext(storage, repo, parsed);
251
293
  switch (parsed.sub ?? "list") {
252
294
  case "list": {
253
- const state = loadState(config, parsed);
295
+ const state = loadState(ctx);
254
296
  const filtered = filterFindings(state, filterFrom(parsed));
255
297
  const limit = Number(parsed.values["--limit"]);
256
298
  const findings =
@@ -263,11 +305,11 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
263
305
  };
264
306
  }
265
307
  case "show": {
266
- const state = loadState(config, parsed);
267
- const id = parsed.positionals[0];
268
- if (!id) throw new Error("Informe o id: houndsman findings show <id>");
308
+ const state = loadState(ctx);
309
+ const input = parsed.positionals[0];
310
+ if (!input) throw new Error("Informe o id: houndsman findings show <id>");
311
+ const id = requireFindingId(state, input);
269
312
  const finding = state.findings[id];
270
- if (!finding) throw new Error(`Finding '${id}' não encontrado.`);
271
313
  return {
272
314
  output: parsed.bools.has("--json")
273
315
  ? JSON.stringify(finding, null, 2)
@@ -276,56 +318,71 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
276
318
  };
277
319
  }
278
320
  case "set": {
279
- const id = parsed.positionals[0];
280
- if (!id) throw new Error("Informe o id: houndsman findings set <id> [flags]");
321
+ const input = parsed.positionals[0];
322
+ if (!input)
323
+ throw new Error("Informe o id: houndsman findings set <id> [flags]");
281
324
  const update = updateFrom(parsed);
282
325
  if (!hasUpdate(update)) {
283
326
  throw new Error(
284
327
  "Nada para atualizar (use --decision, --exec ou --comment).",
285
328
  );
286
329
  }
287
- const finding = updateOne(config, parsed, id, update);
288
- pingStatus(config, parsed);
330
+ const finding = updateOne(ctx, input, update);
331
+ pingStatus(ctx, loadState(ctx));
332
+ const threads =
333
+ update.execState === "done" ? syncThreadsOutput(ctx, [finding.id]) : null;
289
334
  return {
290
335
  output: parsed.bools.has("--json")
291
336
  ? JSON.stringify(finding, null, 2)
292
- : `Atualizado: \`${finding.id}\` → ${finding.decision}/${finding.execState}.`,
337
+ : [
338
+ `Atualizado: \`${finding.id}\` → ${finding.decision}/${finding.execState}.`,
339
+ threads,
340
+ ]
341
+ .filter(Boolean)
342
+ .join("\n"),
293
343
  exitCode: 0,
294
344
  };
295
345
  }
296
346
  case "done": {
297
347
  const ids = requireIds(parsed, "done");
298
- const updated = updateMany(config, parsed, ids, { execState: "done" });
299
- pingStatus(config, parsed);
348
+ const updated = updateMany(ctx, ids, { execState: "done" });
349
+ pingStatus(ctx, loadState(ctx));
350
+ const threads = syncThreadsOutput(
351
+ ctx,
352
+ updated.map((finding) => finding.id),
353
+ );
300
354
  return {
301
- output: `Marcados como done: ${updated.map((f) => f.id).join(", ")}.`,
355
+ output: [
356
+ `Marcados como done: ${updated.map((f) => f.id).join(", ")}.`,
357
+ threads,
358
+ ]
359
+ .filter(Boolean)
360
+ .join("\n"),
302
361
  exitCode: 0,
303
362
  };
304
363
  }
305
364
  case "reset": {
306
365
  const ids = requireIds(parsed, "reset");
307
- const updated = updateMany(config, parsed, ids, {
366
+ const updated = updateMany(ctx, ids, {
308
367
  decision: "pending",
309
368
  execState: "pending",
310
369
  comment: "",
311
370
  });
312
- pingStatus(config, parsed);
371
+ pingStatus(ctx, loadState(ctx));
313
372
  return {
314
373
  output: `Triagem reiniciada: ${updated.map((f) => f.id).join(", ")}.`,
315
374
  exitCode: 0,
316
375
  };
317
376
  }
318
377
  case "sync": {
319
- const { dir, branch } = resolveDir(config, parsed);
320
- const store = FindingsStateStore.at(dir, branch);
321
- const state = loadState(config, parsed);
378
+ const state = loadState(ctx);
322
379
  const result = syncState(state, fileReader(process.cwd()));
323
- store.write(state);
324
- pingStatus(config, parsed);
380
+ ctx.storage.writeFindings(ctx.review.id, state);
381
+ pingStatus(ctx, state);
325
382
  const output = parsed.bools.has("--json")
326
383
  ? JSON.stringify(result, null, 2)
327
384
  : [
328
- `Sync — ${branch}: ${result.ok} ok, ${result.moved.length} movido(s), ${result.stale.length} stale.`,
385
+ `Sync — ${ctx.review.branch}: ${result.ok} ok, ${result.moved.length} movido(s), ${result.stale.length} stale.`,
329
386
  result.moved.length > 0
330
387
  ? `Movidos: ${result.moved.join(", ")}`
331
388
  : "",
@@ -338,7 +395,7 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
338
395
  return { output, exitCode: 0 };
339
396
  }
340
397
  case "summary": {
341
- const state = loadState(config, parsed);
398
+ const state = loadState(ctx);
342
399
  const summary = summarize(state);
343
400
  return {
344
401
  output: parsed.bools.has("--json")
@@ -348,12 +405,13 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
348
405
  };
349
406
  }
350
407
  case "path": {
351
- const { dir, branch } = resolveDir(config, parsed);
352
- const path = FindingsStateStore.at(dir, branch).filePath;
353
- return { output: relFromRoot(path), exitCode: 0 };
408
+ return {
409
+ output: ctx.storage.location(ctx.review.id),
410
+ exitCode: 0,
411
+ };
354
412
  }
355
413
  case "batch": {
356
- const state = loadState(config, parsed);
414
+ const state = loadState(ctx);
357
415
  const severity = parsed.values["--severity"];
358
416
  const offset = Number(parsed.values["--offset"]);
359
417
  const result = fixBatchBuilder.build(state, {
@@ -412,11 +470,13 @@ function requireIds(parsed: ParsedArgs, sub: string): string[] {
412
470
  */
413
471
  export async function runVerifyCommand(
414
472
  config: HoundsmanConfig,
473
+ storage: StorageProvider,
474
+ repo: RepoIdentity,
415
475
  args: string[],
416
476
  ): Promise<FindingsCommandResult> {
417
- const parsed = parseArgs(args);
418
477
  try {
419
- return await executeVerify(config, parsed);
478
+ const parsed = parseArgs(args);
479
+ return await executeVerify(config, storage, repo, parsed);
420
480
  } catch (error) {
421
481
  return {
422
482
  output: error instanceof Error ? error.message : String(error),
@@ -427,11 +487,12 @@ export async function runVerifyCommand(
427
487
 
428
488
  async function executeVerify(
429
489
  config: HoundsmanConfig,
490
+ storage: StorageProvider,
491
+ repo: RepoIdentity,
430
492
  parsed: ParsedArgs,
431
493
  ): Promise<FindingsCommandResult> {
432
- const { dir, branch } = resolveDir(config, parsed);
433
- const store = FindingsStateStore.at(dir, branch);
434
- const state = loadState(config, parsed);
494
+ const ctx = openContext(storage, repo, parsed);
495
+ const state = loadState(ctx);
435
496
 
436
497
  // Lazy: `../jev` (axios) só entra no caminho do verify.
437
498
  const verifier = new Verifier();
@@ -471,23 +532,34 @@ async function executeVerify(
471
532
  }
472
533
 
473
534
  const summary = verifier.summarize(entries);
474
- const reportPath = join(runDir(dir, branch), VERIFY_FILE);
475
- writeFileSync(
476
- reportPath,
477
- `${presenters.verify.render(state, entries, summary, { jevChecked, jevReason })}\n`,
478
- );
535
+ const location = ctx.storage.location(ctx.review.id);
536
+ ctx.storage.writeArtifact(ctx.review.id, {
537
+ name: VERIFY_FILE,
538
+ kind: "verify",
539
+ content: `${presenters.verify.render(state, entries, summary, { jevChecked, jevReason })}\n`,
540
+ });
479
541
 
480
542
  let applied: string[] = [];
481
543
  if (parsed.bools.has("--apply")) {
482
- store.update((current) => {
483
- applied = verifier.applyResolutions(current, entries);
544
+ const current = mutateState(ctx, (draft) => {
545
+ applied = verifier.applyResolutions(draft, entries);
484
546
  });
485
- pingStatus(config, parsed);
547
+ pingStatus(ctx, current);
486
548
  }
487
549
 
550
+ // Resolvidos localmente ⇒ resolve as conversas correspondentes no PR.
551
+ const threads = syncThreadsOutput(ctx, applied);
552
+
488
553
  const output = parsed.bools.has("--json")
489
554
  ? JSON.stringify(
490
- { branch, summary, jevChecked, jevReason, applied, entries },
555
+ {
556
+ branch: ctx.review.branch,
557
+ summary,
558
+ jevChecked,
559
+ jevReason,
560
+ applied,
561
+ entries,
562
+ },
491
563
  null,
492
564
  2,
493
565
  )
@@ -497,10 +569,11 @@ async function executeVerify(
497
569
  jevReason,
498
570
  }),
499
571
  "",
500
- `Relatório: ${relFromRoot(reportPath)}`,
572
+ `Relatório: ${location} (${VERIFY_FILE})`,
501
573
  parsed.bools.has("--apply")
502
574
  ? `Aplicados (done): ${applied.length > 0 ? applied.join(", ") : "—"}.`
503
575
  : "",
576
+ threads,
504
577
  ]
505
578
  .filter(Boolean)
506
579
  .join("\n");
@@ -570,6 +643,21 @@ function renderSubmitOutcome(outcome: SubmitOutcome, json: boolean): string {
570
643
  "",
571
644
  `Enviados: ${outcome.posted.length} · Falhas: ${outcome.failed.length} · Pulados: ${outcome.skipped.length}`,
572
645
  ];
646
+ const review = outcome.review;
647
+ if (review?.id !== undefined) {
648
+ const target = review.url ? ` — ${review.url}` : "";
649
+ if (review.created) {
650
+ const status = review.finalized
651
+ ? `finalizada (${review.event})`
652
+ : "criada, mas não finalizada";
653
+ lines.push(`Revisão #${review.id} ${status}${target}`);
654
+ } else {
655
+ lines.push(
656
+ `Comentários adicionados à revisão pendente #${review.id}${target}`,
657
+ );
658
+ }
659
+ }
660
+ if (review?.error) lines.push(`Revisão: ❌ ${review.error}`);
573
661
  if (outcome.general) {
574
662
  lines.push(
575
663
  `Comentário geral: ${outcome.general.url ?? outcome.general.commentId} (${outcome.general.ids.length})`,
@@ -582,16 +670,19 @@ function renderSubmitOutcome(outcome: SubmitOutcome, json: boolean): string {
582
670
  }
583
671
 
584
672
  /**
585
- * Subcomando `findings submit` — envia os findings selecionados como comentários
586
- * na PR (via `gh`). `--dry-run` mostra a prévia sem postar. Assíncrono (gh).
673
+ * Subcomando `findings submit` — envia os findings selecionados numa revisão da
674
+ * PR (via `gh`): reaproveita a revisão pendente ou cria/finaliza uma nova.
675
+ * `--dry-run` mostra a prévia sem postar. Assíncrono (gh).
587
676
  */
588
677
  export async function runSubmitCommand(
589
678
  config: HoundsmanConfig,
679
+ storage: StorageProvider,
680
+ repo: RepoIdentity,
590
681
  args: string[],
591
682
  ): Promise<FindingsCommandResult> {
592
- const parsed = parseArgs(args);
593
683
  try {
594
- return await executeSubmit(config, parsed);
684
+ const parsed = parseArgs(args);
685
+ return await executeSubmit(config, storage, repo, parsed);
595
686
  } catch (error) {
596
687
  return {
597
688
  output: error instanceof Error ? error.message : String(error),
@@ -601,10 +692,13 @@ export async function runSubmitCommand(
601
692
  }
602
693
 
603
694
  async function executeSubmit(
604
- config: HoundsmanConfig,
695
+ _config: HoundsmanConfig,
696
+ storage: StorageProvider,
697
+ repo: RepoIdentity,
605
698
  parsed: ParsedArgs,
606
699
  ): Promise<FindingsCommandResult> {
607
- const state = loadState(config, parsed);
700
+ const ctx = openContext(storage, repo, parsed);
701
+ const state = loadState(ctx);
608
702
  const selection = submitSelection(parsed);
609
703
  const target = resolvePrTarget(defaultGhRunner, state.branch);
610
704
  const preview = buildSubmitPreview(state, target, selection);
@@ -621,14 +715,11 @@ async function executeSubmit(
621
715
  runner: defaultGhRunner,
622
716
  selection,
623
717
  });
624
- FindingsStateStore.at(
625
- parsed.values["--dir"] ?? resolveArtifactDir(config),
626
- state.branch,
627
- ).write(state);
628
- pingStatus(config, parsed);
718
+ ctx.storage.writeFindings(ctx.review.id, state);
719
+ pingStatus(ctx, state);
629
720
 
630
721
  return {
631
722
  output: renderSubmitOutcome(outcome, json),
632
- exitCode: outcome.failed.length > 0 ? 1 : 0,
723
+ exitCode: outcome.failed.length > 0 || Boolean(outcome.review?.error) ? 1 : 0,
633
724
  };
634
725
  }
@@ -6,24 +6,23 @@
6
6
 
7
7
  import { ArgParser } from "../arg-parser";
8
8
  import { defaultOptions, type ReviewOptions } from "../orchestrator";
9
- import { ARTIFACT_DIR } from "../paths";
10
9
 
11
10
  /** Converte argv (sem o subcomando) nas opções de prepare/finalize. */
12
- export function parseReviewArgs(
13
- args: string[],
14
- artifactDir: string = ARTIFACT_DIR,
15
- ): ReviewOptions {
11
+ export function parseReviewArgs(args: string[]): ReviewOptions {
16
12
  const parser = ArgParser.from(args);
17
- const opts = defaultOptions(artifactDir);
13
+ if (parser.has("--dir")) {
14
+ throw new Error(
15
+ "--dir foi removido; configure `storage` no houndsman.config.ts.",
16
+ );
17
+ }
18
+ const opts = defaultOptions();
18
19
 
19
20
  const base = parser.value("--base");
20
21
  const head = parser.value("--head");
21
22
  const branch = parser.value("--branch");
22
- const dir = parser.value("--dir");
23
23
  if (base) opts.base = base;
24
24
  if (head) opts.head = head;
25
25
  if (branch) opts.branch = branch;
26
- if (dir) opts.dir = dir;
27
26
 
28
27
  if (parser.has("--finalize") || parser.has("--report-only")) opts.finalize = true;
29
28
  if (parser.has("--fail-on-blocking")) opts.failOnBlocking = true;