@payconductor/houndsman 0.12.0 → 0.14.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 (119) hide show
  1. package/README.md +23 -2
  2. package/package.json +1 -1
  3. package/src/cli/commands/compare-command.ts +112 -0
  4. package/src/cli/commands/context-command.ts +1 -0
  5. package/src/cli/commands/doctor-command.ts +22 -0
  6. package/src/cli/commands/findings-command.ts +11 -1
  7. package/src/cli/commands/hunt-command.ts +479 -0
  8. package/src/cli/commands/review-command.ts +58 -2
  9. package/src/cli/commands/status-command.ts +3 -2
  10. package/src/cli/help.ts +20 -3
  11. package/src/cli/index.ts +20 -3
  12. package/src/cli/progress.ts +50 -0
  13. package/src/core/ai/ai-client.ts +38 -2
  14. package/src/core/ai/ai-sdk-model-provider.ts +39 -0
  15. package/src/core/ai/features/brief-enricher.ts +1 -1
  16. package/src/core/ai/features/business-analyzer.ts +61 -0
  17. package/src/core/ai/features/business-summarizer.ts +44 -0
  18. package/src/core/ai/index.ts +14 -1
  19. package/src/core/ai/prompts/brief.ts +33 -7
  20. package/src/core/ai/prompts/business.ts +76 -0
  21. package/src/core/ai/provider-capabilities.ts +12 -0
  22. package/src/core/ai/schemas.ts +70 -2
  23. package/src/core/ai/types.ts +2 -0
  24. package/src/core/ai/usage-recorder.ts +53 -1
  25. package/src/core/brief.ts +246 -12
  26. package/src/core/builtins.ts +108 -0
  27. package/src/core/business-summary.ts +583 -0
  28. package/src/core/commands/compare.ts +233 -0
  29. package/src/core/commands/context.ts +6 -2
  30. package/src/core/commands/findings.ts +38 -30
  31. package/src/core/commands/review.ts +6 -0
  32. package/src/core/commands/scaffold.ts +38 -0
  33. package/src/core/commands/status.ts +9 -4
  34. package/src/core/compare.ts +243 -0
  35. package/src/core/concurrency.ts +30 -0
  36. package/src/core/config.ts +57 -1
  37. package/src/core/defaults.ts +63 -2
  38. package/src/core/domain/budget.ts +62 -4
  39. package/src/core/domain/classification.ts +12 -2
  40. package/src/core/engine/carryover.ts +4 -0
  41. package/src/core/engine/finalize-pipeline.ts +347 -0
  42. package/src/core/engine/prepare-pipeline.ts +1294 -0
  43. package/src/core/engine/review-context.ts +51 -0
  44. package/src/core/engine/review-options.ts +47 -0
  45. package/src/core/engine/review-scope.ts +106 -26
  46. package/src/core/engine/review-session.ts +18 -16
  47. package/src/core/engine/review-workspace.ts +14 -0
  48. package/src/core/engine/run-artifacts.ts +196 -0
  49. package/src/core/engine/run-format.ts +4 -0
  50. package/src/core/env.ts +14 -0
  51. package/src/core/findings-state.ts +13 -1
  52. package/src/core/github-review.ts +5 -2
  53. package/src/core/hunt/hunt-runner.ts +418 -0
  54. package/src/core/hunt/index.ts +61 -0
  55. package/src/core/hunt/maki-install.ts +331 -0
  56. package/src/core/hunt/maki-intent-prompt.ts +50 -0
  57. package/src/core/hunt/maki-model.ts +101 -0
  58. package/src/core/hunt/maki-prompt.ts +83 -0
  59. package/src/core/hunt/maki-release.ts +104 -0
  60. package/src/core/hunt/maki-runner.ts +276 -0
  61. package/src/core/hunt/pool.ts +8 -0
  62. package/src/core/logging/index.ts +7 -0
  63. package/src/core/logging/logger.ts +19 -0
  64. package/src/core/logging/progress.ts +366 -0
  65. package/src/core/orchestrator.ts +33 -1180
  66. package/src/core/paths.ts +6 -3
  67. package/src/core/pr-context.ts +2 -4
  68. package/src/core/prepare-report.ts +61 -0
  69. package/src/core/project-config.ts +46 -1
  70. package/src/core/propagation.ts +127 -0
  71. package/src/core/registry-check.ts +60 -21
  72. package/src/core/renderers/manifest-writer.ts +13 -0
  73. package/src/core/renderers/resume-renderer.ts +151 -11
  74. package/src/core/report-builder.ts +108 -278
  75. package/src/core/storage/file/file-storage.ts +7 -3
  76. package/src/core/storage/sqlite/mappers.ts +10 -0
  77. package/src/core/storage/sqlite/schema.ts +104 -23
  78. package/src/core/storage/sqlite/sqlite-storage.ts +22 -9
  79. package/src/core/storage/types.ts +17 -4
  80. package/src/core/storage/work-area.ts +59 -1
  81. package/src/core/types.ts +31 -0
  82. package/src/core/watchdog-registry.ts +21 -3
  83. package/src/index.ts +78 -4
  84. package/src/server/client/ArtifactsPanel.tsx +4 -2
  85. package/src/server/client/FindingCard.tsx +21 -1
  86. package/src/server/client/RunsSection.tsx +5 -0
  87. package/src/server/client/utils/constants.ts +21 -11
  88. package/src/server/client/utils/markdown.ts +1 -1
  89. package/src/server/client/utils/types.ts +14 -2
  90. package/src/server/run-repository.ts +35 -14
  91. package/src/server/styles.css +2 -0
  92. package/src/skill/OUTPUT-FORMAT.md +36 -10
  93. package/src/skill/README.md +30 -14
  94. package/src/skill/SETUP.md +22 -1
  95. package/src/skill/SKILL.md +17 -4
  96. package/src/skill/example-resume.md +43 -0
  97. package/src/skill/hunt.md +181 -58
  98. package/src/tests/__snapshots__/baseline.test.ts.snap +206 -178
  99. package/src/tests/ai-core.test.ts +84 -1
  100. package/src/tests/ai-features.test.ts +95 -4
  101. package/src/tests/baseline.test.ts +29 -121
  102. package/src/tests/brief-scope.test.ts +250 -0
  103. package/src/tests/business-summary.test.ts +258 -0
  104. package/src/tests/cli.test.ts +1 -0
  105. package/src/tests/compare.test.ts +233 -0
  106. package/src/tests/fixtures/fake-model-provider.ts +2 -1
  107. package/src/tests/hunt-cli.test.ts +185 -0
  108. package/src/tests/hunt-install.test.ts +153 -0
  109. package/src/tests/hunt-maki.test.ts +525 -0
  110. package/src/tests/markdown.test.ts +3 -3
  111. package/src/tests/progress.test.ts +145 -0
  112. package/src/tests/propagation.test.ts +119 -0
  113. package/src/tests/review-resume.test.ts +60 -1
  114. package/src/tests/review-scope.test.ts +90 -19
  115. package/src/tests/review-scripts.test.ts +106 -218
  116. package/src/tests/server.test.ts +7 -4
  117. package/src/tests/storage.test.ts +194 -9
  118. package/src/core/renderers/report-renderer.ts +0 -66
  119. package/src/skill/example-review.md +0 -66
@@ -22,6 +22,7 @@ export type ReviewRow = {
22
22
  repo_root: string;
23
23
  repo_remote: string | null;
24
24
  branch: string;
25
+ variant: string;
25
26
  slug: string;
26
27
  base: string | null;
27
28
  head: string | null;
@@ -50,6 +51,10 @@ export type FindingRow = {
50
51
  message: string;
51
52
  action: string;
52
53
  evidence: string | null;
54
+ scenario: string | null;
55
+ impact: string | null;
56
+ confidence: number | null;
57
+ basis: string | null;
53
58
  anchor: string | null;
54
59
  found_at_sha: string | null;
55
60
  count: number;
@@ -91,6 +96,7 @@ export function reviewFromRow(row: ReviewRow): ReviewRecord {
91
96
  repoRoot: row.repo_root,
92
97
  repoRemote: row.repo_remote ?? undefined,
93
98
  branch: row.branch,
99
+ variant: row.variant || undefined,
94
100
  slug: row.slug,
95
101
  base: row.base ?? undefined,
96
102
  head: row.head ?? undefined,
@@ -129,6 +135,10 @@ export function findingFromRow(row: FindingRow, history: HistoryRow[]): FindingS
129
135
  message: row.message,
130
136
  action: row.action,
131
137
  evidence: row.evidence ?? undefined,
138
+ scenario: row.scenario ?? undefined,
139
+ impact: row.impact ?? undefined,
140
+ confidence: row.confidence === null ? undefined : Number(row.confidence),
141
+ basis: (row.basis as FindingState["basis"]) ?? undefined,
132
142
  anchor: parseJson<FindingAnchor>(row.anchor) ?? null,
133
143
  foundAtSha: row.found_at_sha ?? undefined,
134
144
  count: Number(row.count),
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { Database } from "bun:sqlite";
9
9
 
10
- export const SCHEMA_VERSION = 3;
10
+ export const SCHEMA_VERSION = 5;
11
11
 
12
12
  const DDL = `
13
13
  CREATE TABLE IF NOT EXISTS reviews (
@@ -16,6 +16,7 @@ CREATE TABLE IF NOT EXISTS reviews (
16
16
  repo_root TEXT NOT NULL,
17
17
  repo_remote TEXT,
18
18
  branch TEXT NOT NULL,
19
+ variant TEXT NOT NULL DEFAULT '',
19
20
  slug TEXT NOT NULL,
20
21
  base TEXT,
21
22
  head TEXT,
@@ -29,7 +30,7 @@ CREATE TABLE IF NOT EXISTS reviews (
29
30
  pr_number INTEGER,
30
31
  created_at TEXT NOT NULL,
31
32
  updated_at TEXT NOT NULL,
32
- UNIQUE(repo_key, branch)
33
+ UNIQUE(repo_key, branch, variant)
33
34
  );
34
35
 
35
36
  CREATE INDEX IF NOT EXISTS idx_reviews_repo_updated
@@ -48,6 +49,10 @@ CREATE TABLE IF NOT EXISTS findings (
48
49
  message TEXT NOT NULL,
49
50
  action TEXT NOT NULL,
50
51
  evidence TEXT,
52
+ scenario TEXT,
53
+ impact TEXT,
54
+ confidence REAL,
55
+ basis TEXT,
51
56
  anchor TEXT,
52
57
  found_at_sha TEXT,
53
58
  count INTEGER NOT NULL DEFAULT 1,
@@ -93,7 +98,6 @@ CREATE INDEX IF NOT EXISTS idx_artifacts_review ON artifacts(review_id);
93
98
 
94
99
  /** Aplica pragmas e cria o schema quando necessário. */
95
100
  export function migrate(db: Database, path: string): void {
96
- db.exec("PRAGMA foreign_keys = ON;");
97
101
  db.exec("PRAGMA busy_timeout = 5000;");
98
102
  db.exec("PRAGMA synchronous = NORMAL;");
99
103
  if (path !== ":memory:") db.exec("PRAGMA journal_mode = WAL;");
@@ -102,25 +106,102 @@ export function migrate(db: Database, path: string): void {
102
106
  user_version?: number;
103
107
  } | null;
104
108
  const version = Number(row?.user_version ?? 0);
105
- if (version >= SCHEMA_VERSION) return;
109
+ if (version >= SCHEMA_VERSION) {
110
+ db.exec("PRAGMA foreign_keys = ON;");
111
+ return;
112
+ }
106
113
 
107
- db.transaction(() => {
108
- db.exec(DDL);
109
- // v2: heartbeat explícito da execução (bancos v1 não tinham a coluna).
110
- // v3: worktree/pr da revisão (revisões paralelas).
111
- const columns = db.query("PRAGMA table_info(reviews)").all() as {
112
- name: string;
113
- }[];
114
- const missing: [string, string][] = [
115
- ["heartbeat_at", "TEXT"],
116
- ["worktree_path", "TEXT"],
117
- ["pr_number", "INTEGER"],
118
- ];
119
- for (const [name, type] of missing) {
120
- if (!columns.some((column) => column.name === name)) {
121
- db.exec(`ALTER TABLE reviews ADD COLUMN ${name} ${type}`);
122
- }
123
- }
124
- db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
125
- })();
114
+ // O rebuild de `reviews` (v5) troca a chave única e precisa de FKs desligadas:
115
+ // com elas ligadas, `DROP TABLE reviews` cascatearia e apagaria os filhos.
116
+ db.exec("PRAGMA foreign_keys = OFF;");
117
+ try {
118
+ db.transaction(() => {
119
+ db.exec(DDL);
120
+ // v2: heartbeat explícito da execução; v3: worktree/pr da revisão;
121
+ // v4: cenário/impacto/confiança/origem do finding.
122
+ const addMissing = (table: string, columns: [string, string][]): void => {
123
+ const info = db.query(`PRAGMA table_info(${table})`).all() as {
124
+ name: string;
125
+ }[];
126
+ for (const [name, type] of columns) {
127
+ if (!info.some((column) => column.name === name)) {
128
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${type}`);
129
+ }
130
+ }
131
+ };
132
+ addMissing("reviews", [
133
+ ["heartbeat_at", "TEXT"],
134
+ ["worktree_path", "TEXT"],
135
+ ["pr_number", "INTEGER"],
136
+ ]);
137
+ addMissing("findings", [
138
+ ["scenario", "TEXT"],
139
+ ["impact", "TEXT"],
140
+ ["confidence", "REAL"],
141
+ ["basis", "TEXT"],
142
+ ]);
143
+ // v5: variante A/B na identidade da review.
144
+ rebuildReviewsWithVariant(db);
145
+ db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
146
+ })();
147
+ } finally {
148
+ db.exec("PRAGMA foreign_keys = ON;");
149
+ }
150
+ const violations = db.query("PRAGMA foreign_key_check").all();
151
+ if (violations.length > 0) {
152
+ throw new Error(
153
+ `Migração do banco deixou ${violations.length} referência(s) inválida(s).`,
154
+ );
155
+ }
156
+ }
157
+
158
+ /**
159
+ * v5 — adiciona `variant` e troca `UNIQUE(repo_key, branch)` por
160
+ * `UNIQUE(repo_key, branch, variant)`.
161
+ *
162
+ * SQLite não remove constraint inline: recria a tabela preservando os ids (as
163
+ * FKs dos filhos continuam válidas) e recria o índice.
164
+ */
165
+ function rebuildReviewsWithVariant(db: Database): void {
166
+ const columns = db.query("PRAGMA table_info(reviews)").all() as {
167
+ name: string;
168
+ }[];
169
+ if (columns.some((column) => column.name === "variant")) return;
170
+
171
+ db.exec(`
172
+ CREATE TABLE reviews_v5 (
173
+ id TEXT PRIMARY KEY,
174
+ repo_key TEXT NOT NULL,
175
+ repo_root TEXT NOT NULL,
176
+ repo_remote TEXT,
177
+ branch TEXT NOT NULL,
178
+ variant TEXT NOT NULL DEFAULT '',
179
+ slug TEXT NOT NULL,
180
+ base TEXT,
181
+ head TEXT,
182
+ head_sha TEXT,
183
+ reviewed_sha TEXT,
184
+ reviewed_at TEXT,
185
+ status TEXT,
186
+ started_at TEXT,
187
+ heartbeat_at TEXT,
188
+ worktree_path TEXT,
189
+ pr_number INTEGER,
190
+ created_at TEXT NOT NULL,
191
+ updated_at TEXT NOT NULL,
192
+ UNIQUE(repo_key, branch, variant)
193
+ );
194
+ INSERT INTO reviews_v5
195
+ (id, repo_key, repo_root, repo_remote, branch, variant, slug, base, head,
196
+ head_sha, reviewed_sha, reviewed_at, status, started_at, heartbeat_at,
197
+ worktree_path, pr_number, created_at, updated_at)
198
+ SELECT id, repo_key, repo_root, repo_remote, branch, '', slug, base, head,
199
+ head_sha, reviewed_sha, reviewed_at, status, started_at, heartbeat_at,
200
+ worktree_path, pr_number, created_at, updated_at
201
+ FROM reviews;
202
+ DROP TABLE reviews;
203
+ ALTER TABLE reviews_v5 RENAME TO reviews;
204
+ CREATE INDEX IF NOT EXISTS idx_reviews_repo_updated
205
+ ON reviews(repo_key, updated_at DESC);
206
+ `);
126
207
  }
@@ -69,7 +69,7 @@ export class SQLiteStorageProvider implements StorageProvider {
69
69
  // -----------------------------------------------------------------------
70
70
 
71
71
  getOrCreateReview(input: CreateReviewInput): ReviewRecord {
72
- const existing = this.findReview(input.repo.key, input.branch);
72
+ const existing = this.findReview(input.repo.key, input.branch, input.variant);
73
73
  if (existing) {
74
74
  // O repo pode ter sido movido; a identidade continua a mesma.
75
75
  const moved =
@@ -102,6 +102,7 @@ export class SQLiteStorageProvider implements StorageProvider {
102
102
  repoRoot: input.repo.root,
103
103
  repoRemote: input.repo.remote,
104
104
  branch: input.branch,
105
+ variant: input.variant?.trim() || undefined,
105
106
  slug: input.slug,
106
107
  base: input.base,
107
108
  head: input.head,
@@ -113,15 +114,16 @@ export class SQLiteStorageProvider implements StorageProvider {
113
114
  };
114
115
  this.db.run(
115
116
  `INSERT INTO reviews
116
- (id, repo_key, repo_root, repo_remote, branch, slug, base, head,
117
+ (id, repo_key, repo_root, repo_remote, branch, variant, slug, base, head,
117
118
  worktree_path, pr_number, created_at, updated_at)
118
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
119
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
119
120
  [
120
121
  record.id,
121
122
  record.repoKey,
122
123
  record.repoRoot,
123
124
  record.repoRemote ?? null,
124
125
  record.branch,
126
+ record.variant ?? "",
125
127
  record.slug,
126
128
  record.base ?? null,
127
129
  record.head ?? null,
@@ -134,10 +136,12 @@ export class SQLiteStorageProvider implements StorageProvider {
134
136
  return record;
135
137
  }
136
138
 
137
- findReview(repoKey: string, branch: string): ReviewRecord | null {
139
+ findReview(repoKey: string, branch: string, variant?: string): ReviewRecord | null {
138
140
  const row = this.db
139
- .query("SELECT * FROM reviews WHERE repo_key = ? AND branch = ?")
140
- .get(repoKey, branch) as ReviewRow | null;
141
+ .query(
142
+ "SELECT * FROM reviews WHERE repo_key = ? AND branch = ? AND variant = ?",
143
+ )
144
+ .get(repoKey, branch, variant?.trim() ?? "") as ReviewRow | null;
141
145
  return row ? reviewFromRow(row) : null;
142
146
  }
143
147
 
@@ -253,9 +257,10 @@ export class SQLiteStorageProvider implements StorageProvider {
253
257
  const upsert = this.db.query(
254
258
  `INSERT INTO findings
255
259
  (id, review_id, short_id, fingerprint, watchdog_ids, rule, severity,
256
- file, line, message, action, evidence, anchor, found_at_sha, count,
257
- present, decision, comment, exec_state, github, created_at, updated_at)
258
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
260
+ file, line, message, action, evidence, scenario, impact, confidence, basis,
261
+ anchor, found_at_sha, count, present, decision, comment, exec_state, github,
262
+ created_at, updated_at)
263
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
259
264
  ON CONFLICT(id) DO UPDATE SET
260
265
  short_id = excluded.short_id,
261
266
  fingerprint = excluded.fingerprint,
@@ -267,6 +272,10 @@ export class SQLiteStorageProvider implements StorageProvider {
267
272
  message = excluded.message,
268
273
  action = excluded.action,
269
274
  evidence = excluded.evidence,
275
+ scenario = excluded.scenario,
276
+ impact = excluded.impact,
277
+ confidence = excluded.confidence,
278
+ basis = excluded.basis,
270
279
  anchor = excluded.anchor,
271
280
  found_at_sha = excluded.found_at_sha,
272
281
  count = excluded.count,
@@ -325,6 +334,10 @@ export class SQLiteStorageProvider implements StorageProvider {
325
334
  finding.message,
326
335
  finding.action,
327
336
  finding.evidence ?? null,
337
+ finding.scenario ?? null,
338
+ finding.impact ?? null,
339
+ finding.confidence ?? null,
340
+ finding.basis ?? null,
328
341
  finding.anchor ? JSON.stringify(finding.anchor) : null,
329
342
  finding.foundAtSha ?? null,
330
343
  finding.count,
@@ -28,6 +28,13 @@ export type ReviewRecord = {
28
28
  repoRoot: string;
29
29
  repoRemote?: string;
30
30
  branch: string;
31
+ /**
32
+ * Rótulo da variante A/B (ausente = run principal da branch).
33
+ *
34
+ * Faz parte da identidade junto com `(repoKey, branch)`: permite rodar a
35
+ * mesma PR com dois experimentos sem apagar a revisão anterior.
36
+ */
37
+ variant?: string;
31
38
  slug: string;
32
39
  base?: string;
33
40
  head?: string;
@@ -81,6 +88,8 @@ export type CreateReviewInput = {
81
88
  repo: RepoIdentity;
82
89
  branch: string;
83
90
  slug: string;
91
+ /** Variante A/B (ausente = run principal da branch). */
92
+ variant?: string;
84
93
  base?: string;
85
94
  head?: string;
86
95
  /** Worktree onde a revisão roda (ausente = clone principal). */
@@ -99,17 +108,20 @@ export type ArtifactKind =
99
108
  | "context"
100
109
  | "context-brief"
101
110
  | "context-summary"
111
+ | "business"
112
+ | "business-brief"
102
113
  | "finding-report"
103
- | "review"
104
114
  | "resume"
105
115
  | "technical"
106
- | "domain-review"
107
116
  | "verify"
108
117
  | "ast"
109
118
  | "ast-impact"
110
119
  | "ai"
111
120
  | "ai-usage"
112
121
  | "ai-brief"
122
+ | "hunt"
123
+ | "prepare"
124
+ | "compare"
113
125
  | "other";
114
126
 
115
127
  /** Artefato persistido de uma review. */
@@ -141,9 +153,10 @@ export type WriteArtifactInput = {
141
153
  export interface StorageProvider {
142
154
  readonly kind: "sqlite" | "file";
143
155
 
144
- /** Recupera ou cria a review de uma branch (identidade = repo + branch). */
156
+ /** Busca por identidade `(repo, branch, variante)`; variante ausente = principal. */
157
+ findReview(repoKey: string, branch: string, variant?: string): ReviewRecord | null;
158
+ /** Run da branch + variante (cria quando não existe). */
145
159
  getOrCreateReview(input: CreateReviewInput): ReviewRecord;
146
- findReview(repoKey: string, branch: string): ReviewRecord | null;
147
160
  getReview(id: string): ReviewRecord | null;
148
161
  /** Reviews do repositório (mais recentes primeiro); sem chave = todas. */
149
162
  listReviews(repoKey?: string): ReviewRecord[];
@@ -9,7 +9,10 @@
9
9
 
10
10
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
11
11
  import { join } from "node:path";
12
+ import { type BusinessSummary, parseBusinessSummary } from "../business-summary";
12
13
  import {
14
+ BUSINESS_BRIEF_FILE,
15
+ BUSINESS_SUMMARY_FILE,
13
16
  CONTEXT_BRIEF_FILE,
14
17
  CONTEXT_FILE,
15
18
  CONTEXT_SUMMARY_FILE,
@@ -97,7 +100,6 @@ export class ReviewWorkArea {
97
100
  brief: this.file(CONTEXT_BRIEF_FILE),
98
101
  };
99
102
  }
100
-
101
103
  /** Garante os diretórios; `clean` recria a área do zero. */
102
104
  ensure(clean = false): void {
103
105
  if (clean) this.clean();
@@ -143,6 +145,62 @@ export class ReviewWorkArea {
143
145
  writeFileSync(this.contextPaths.summary, `${JSON.stringify(summary, null, 2)}\n`);
144
146
  }
145
147
 
148
+ /** Caminhos do modelo de intenção (JSON + brief do passo). */
149
+ get businessPaths(): { summary: string; brief: string } {
150
+ return {
151
+ summary: this.file(BUSINESS_SUMMARY_FILE),
152
+ brief: this.file(BUSINESS_BRIEF_FILE),
153
+ };
154
+ }
155
+
156
+ /** Grava o modelo de intenção (determinístico, IA ou revisor). */
157
+ writeBusinessSummary(summary: unknown): void {
158
+ mkdirSync(this.root, { recursive: true });
159
+ writeFileSync(
160
+ this.businessPaths.summary,
161
+ `${JSON.stringify(summary, null, 2)}\n`,
162
+ );
163
+ }
164
+
165
+ /** Grava o brief do passo de intenção. */
166
+ writeBusinessBrief(content: string): void {
167
+ mkdirSync(this.root, { recursive: true });
168
+ writeFileSync(this.businessPaths.brief, `${content}\n`);
169
+ }
170
+
171
+ /** Modelo de intenção gravado (subagente/IA) — null se ausente/inválido. */
172
+ readBusinessSummary(): BusinessSummary | null {
173
+ const raw = this.readBusinessSummaryRaw();
174
+ if (raw === null) return null;
175
+ try {
176
+ return parseBusinessSummary(JSON.parse(raw), "reviewer");
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+
182
+ /** Conteúdo cru do modelo de intenção (para persistir como artefato). */
183
+ readBusinessSummaryRaw(): string | null {
184
+ const path = this.businessPaths.summary;
185
+ if (!existsSync(path)) return null;
186
+ try {
187
+ return readFileSync(path, "utf8");
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
192
+
193
+ /** Conteúdo cru do brief de intenção (para persistir como artefato). */
194
+ readBusinessBriefRaw(): string | null {
195
+ const path = this.businessPaths.brief;
196
+ if (!existsSync(path)) return null;
197
+ try {
198
+ return readFileSync(path, "utf8");
199
+ } catch {
200
+ return null;
201
+ }
202
+ }
203
+
146
204
  /** Finds brutos gravados pelos subagentes (um JSON por watchdog). */
147
205
  readFindings(activeIds: string[]): FindingsIngest {
148
206
  const reports: WatchdogReport[] = [];
package/src/core/types.ts CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  import type { BriefPlan } from "./ai/schemas";
10
10
  import type { AstFileBrief } from "./ast/types";
11
+ import type { BusinessSummary } from "./business-summary";
11
12
  import type { ChangedScope } from "./changed-scope";
12
13
  import type { JevRuleSpec } from "./jev/jev-types";
13
14
 
@@ -131,8 +132,38 @@ export type WatchdogRenderArgs = {
131
132
  ast?: readonly AstFileBrief[];
132
133
  /** Plano de revisão sugerido por IA (nunca substitui as regras). */
133
134
  aiPlan?: BriefPlan;
135
+ /** Modelo de intenção da PR (negócio) — insumo do revisor `general`. */
136
+ business?: BusinessSummary;
137
+ /** Caminho do `business-summary.json` no staging (versão canônica). */
138
+ businessSummaryPath?: string;
139
+ /** Trechos alterados (`diff` por arquivo) — só para o revisor de fluxo. */
140
+ snippets?: readonly { file: string; snippet: string }[];
141
+ /** `true` no revisor de fluxo: exige devolver/gravar o modelo de intenção. */
142
+ flowReview?: boolean;
134
143
  /** Escopo do diff por arquivo (símbolos alterados; hunks como fallback). */
135
144
  changedScope?: ChangedScope;
145
+ /** Micro-alterações iguais em muitos arquivos (propagação mecânica). */
146
+ propagation?: PropagationCluster | null;
147
+ /** Contratos alterados cujos importadores não mudaram (possível lacuna). */
148
+ propagationGaps?: readonly PropagationGap[];
149
+ };
150
+
151
+ /** Cluster de propagação mecânica (ex.: novo parâmetro em 100 call sites). */
152
+ export type PropagationCluster = {
153
+ /** Linhas adicionadas normalizadas (o padrão dominante). */
154
+ lines: string[];
155
+ /** Arquivos que seguem o padrão. */
156
+ files: string[];
157
+ /** Arquivos com micro-alteração que NÃO seguem o padrão. */
158
+ outliers: { file: string; lines: string[] }[];
159
+ };
160
+
161
+ /** Contrato que mudou e tem importadores fora do diff. */
162
+ export type PropagationGap = {
163
+ /** Arquivo do contrato (o que mudou). */
164
+ file: string;
165
+ /** Importadores que não aparecem no diff. */
166
+ importers: string[];
136
167
  };
137
168
 
138
169
  /** Definição completa de um watchdog — 1 por arquivo em `project/watchdogs/`. */
@@ -6,6 +6,7 @@
6
6
  * adaptadores para a API pública.
7
7
  */
8
8
 
9
+ import { builtinWatchdog, GENERAL_WATCHDOG_ID, mergeGeneralWatchdog } from "./builtins";
9
10
  import type { HoundsmanConfig } from "./config";
10
11
  import { Watchdog, type WatchdogContext } from "./domain/watchdog";
11
12
  import type { WatchdogDefinition } from "./types";
@@ -23,17 +24,34 @@ export class WatchdogRegistry<TId extends string = string> {
23
24
  return this.config.watchdogIds;
24
25
  }
25
26
 
26
- /** Definição de um watchdog (id string em runtime). */
27
+ /** Definição de um watchdog (config vence; senão, built-in). */
27
28
  get(watchdogId: string): WatchdogDefinition {
28
- return this.config.watchdogs[watchdogId as TId];
29
+ const definition = this.definitionOf(watchdogId);
30
+ if (!definition) {
31
+ throw new Error(`Watchdog '${watchdogId}' não encontrado.`);
32
+ }
33
+ return definition;
29
34
  }
30
35
 
31
36
  /** Busca null-safe por id. */
32
37
  find(watchdogId: string): Watchdog | null {
33
- const definition = this.config.watchdogs[watchdogId as TId];
38
+ const definition = this.definitionOf(watchdogId);
34
39
  return definition ? new Watchdog(definition) : null;
35
40
  }
36
41
 
42
+ /**
43
+ * Resolve a definição: config primeiro, built-in depois. O `general` é
44
+ * reservado e sempre inclui o piso de lógica do motor (mesmo se o consumidor
45
+ * declarar um watchdog com esse id).
46
+ */
47
+ private definitionOf(watchdogId: string): WatchdogDefinition | null {
48
+ const configured = this.config.watchdogs[watchdogId as TId];
49
+ if (watchdogId === GENERAL_WATCHDOG_ID) {
50
+ return mergeGeneralWatchdog(configured);
51
+ }
52
+ return configured ?? builtinWatchdog(watchdogId);
53
+ }
54
+
37
55
  /** Nome legível (default: o próprio id). */
38
56
  nameOf(watchdogId: string): string {
39
57
  return this.find(watchdogId)?.name ?? watchdogId;
package/src/index.ts CHANGED
@@ -50,6 +50,29 @@ export type {
50
50
  AstSymbolKind,
51
51
  } from "./core/ast/types";
52
52
  export { defaultRenderBrief, PROCEDURE, renderBrief } from "./core/brief";
53
+ export {
54
+ BUILTIN_WATCHDOGS,
55
+ builtinWatchdog,
56
+ GENERAL_WATCHDOG_ID,
57
+ generalWatchdog,
58
+ mergeGeneralWatchdog,
59
+ } from "./core/builtins";
60
+ export {
61
+ BUSINESS_SUMMARY_SCHEMA,
62
+ type BusinessBriefInput,
63
+ type BusinessContract,
64
+ type BusinessFlow,
65
+ type BusinessInput,
66
+ type BusinessState,
67
+ type BusinessSummary,
68
+ type BusinessSummarySource,
69
+ buildBusinessBrief,
70
+ buildBusinessPromptPayload,
71
+ buildBusinessSummary,
72
+ parseBusinessSummary,
73
+ renderBusinessLines,
74
+ summarizeBusiness,
75
+ } from "./core/business-summary";
53
76
  export type {
54
77
  BuildChangedScopeInput,
55
78
  ChangedScope,
@@ -67,7 +90,10 @@ export {
67
90
  mergeRanges,
68
91
  summarizeScope,
69
92
  } from "./core/changed-scope";
70
- export type { FindingsCommandResult } from "./core/commands/findings";
93
+ export type {
94
+ FindingsCommandResult,
95
+ FindingsContextOptions,
96
+ } from "./core/commands/findings";
71
97
  export { runFindingsCommand, runVerifyCommand } from "./core/commands/findings";
72
98
  export { ProjectScaffolder } from "./core/commands/scaffold";
73
99
  export type {
@@ -80,13 +106,19 @@ export type {
80
106
  HoundsmanConcurrency,
81
107
  HoundsmanConfig,
82
108
  HoundsmanConfigInput,
109
+ HoundsmanHunt,
110
+ HoundsmanPriority,
83
111
  HoundsmanStorage,
84
112
  } from "./core/config";
85
113
  export {
86
114
  DEFAULT_ALWAYS_INCLUDE_DOCS,
87
115
  DEFAULT_AST_MAX_FILE_BYTES,
88
116
  DEFAULT_AST_MAX_IMPACT_FILES,
117
+ DEFAULT_HUNT_CONCURRENCY,
118
+ DEFAULT_HUNT_RETRIES,
119
+ DEFAULT_HUNT_TIMEOUT_MS,
89
120
  DEFAULT_JEV_CONCURRENCY,
121
+ DEFAULT_MAKI_VERSION,
90
122
  DEFAULT_MAX_FILES_PER_BRIEF,
91
123
  defineHoundsmanConfig,
92
124
  resolveJevConcurrency,
@@ -181,6 +213,50 @@ export {
181
213
  syncState,
182
214
  } from "./core/findings-state";
183
215
  export type { GitResult, GitRunner } from "./core/git-runner";
216
+ export type {
217
+ HuntEvent,
218
+ HuntReport,
219
+ HuntRunnerOptions,
220
+ HuntTotals,
221
+ HuntWatchdogResult,
222
+ InstallMakiOptions,
223
+ InstallMakiResult,
224
+ MakiBinary,
225
+ MakiBinarySource,
226
+ MakiInstallManifest,
227
+ MakiModel,
228
+ MakiProcessRunner,
229
+ MakiRunMetrics,
230
+ MakiRunRequest,
231
+ MakiRunResult,
232
+ MakiUsage,
233
+ } from "./core/hunt";
234
+ export {
235
+ extractJsonObject,
236
+ HuntRunner,
237
+ houndsmanKeyEnvFor,
238
+ installMaki,
239
+ MAKI_API_KEY_ENV,
240
+ MAKI_DISALLOWED_TOOLS,
241
+ makiArgs,
242
+ makiCurrentFile,
243
+ makiInstallDir,
244
+ makiSourceLabel,
245
+ makiToolsRoot,
246
+ mapPool,
247
+ metricsFrom,
248
+ parseMakiOutput,
249
+ probeMakiVersion,
250
+ readMakiInstall,
251
+ renderHuntPrompt,
252
+ renderRetrySuffix,
253
+ requireMakiBinary,
254
+ requireMakiModel,
255
+ resolveMakiBinary,
256
+ resolveMakiModel,
257
+ SpawnMakiRunner,
258
+ uninstallMaki,
259
+ } from "./core/hunt";
184
260
  export {
185
261
  CORE_DIR,
186
262
  CORE_SKILL_DIR,
@@ -193,9 +269,7 @@ export { ProjectConfig } from "./core/project-config";
193
269
  export { checkRegistry } from "./core/registry-check";
194
270
  export { BriefRenderer } from "./core/renderers/brief-renderer";
195
271
  export { ManifestWriter } from "./core/renderers/manifest-writer";
196
- export { ReportRenderer } from "./core/renderers/report-renderer";
197
- export type { DomainReport } from "./core/report-builder";
198
- export { renderDomainReport } from "./core/report-builder";
272
+ export { RESUME_MAX_CHARS, renderReviewResume } from "./core/renderers/resume-renderer";
199
273
  export type { Clock } from "./core/services/clock";
200
274
  export { systemClock } from "./core/services/clock";
201
275
  export { FileClassifier } from "./core/services/file-classifier";
@@ -19,9 +19,11 @@ import {
19
19
  import { formatJson, highlightJson } from "./utils/json-highlight";
20
20
  import type { ArtifactMeta } from "./utils/types";
21
21
 
22
- /** Prefere o relatório consolidado; senão, o primeiro artefato disponível. */
22
+ /** Prefere o resumo final; senão, o primeiro artefato disponível. */
23
23
  function pickDefault(items: ArtifactMeta[]): string {
24
- return (items.find((item) => item.name === "review.md") ?? items[0])?.name ?? "";
24
+ return (
25
+ (items.find((item) => item.name === "review-resume.md") ?? items[0])?.name ?? ""
26
+ );
25
27
  }
26
28
 
27
29
  /** JSON com destaque de sintaxe (tokens em spans, sem lib externa). */