@payconductor/houndsman 0.4.0 → 0.6.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/README.md +35 -41
- package/assets/grammars/tree-sitter-javascript.wasm +0 -0
- package/assets/grammars/tree-sitter-tsx.wasm +0 -0
- package/assets/grammars/tree-sitter-typescript.wasm +0 -0
- package/assets/grammars/web-tree-sitter.wasm +0 -0
- package/package.json +11 -3
- package/src/cli/commands/command.ts +5 -0
- package/src/cli/commands/doctor-command.ts +116 -24
- package/src/cli/commands/hunt-command.ts +25 -0
- package/src/cli/commands/status-command.ts +46 -0
- package/src/cli/help.ts +18 -1
- package/src/cli/index.ts +13 -6
- package/src/core/ai/ai-client.ts +221 -0
- package/src/core/ai/ai-sdk-model-provider.ts +101 -0
- package/src/core/ai/errors.ts +36 -0
- package/src/core/ai/features/brief-enricher.ts +62 -0
- package/src/core/ai/features/classifier.ts +50 -0
- package/src/core/ai/features/context-summarizer.ts +31 -0
- package/src/core/ai/features/pre-analyzer.ts +51 -0
- package/src/core/ai/features/verifier.ts +45 -0
- package/src/core/ai/index.ts +56 -0
- package/src/core/ai/model-factory.ts +137 -0
- package/src/core/ai/prompts/base.ts +36 -0
- package/src/core/ai/prompts/brief.ts +87 -0
- package/src/core/ai/prompts/classification.ts +36 -0
- package/src/core/ai/prompts/context.ts +29 -0
- package/src/core/ai/prompts/pre-analysis.ts +49 -0
- package/src/core/ai/prompts/verify.ts +40 -0
- package/src/core/ai/schemas.ts +110 -0
- package/src/core/ai/types.ts +76 -0
- package/src/core/ai/usage-budget.ts +75 -0
- package/src/core/ai/usage-recorder.ts +79 -0
- package/src/core/assets.ts +16 -0
- package/src/core/ast/diff.ts +220 -0
- package/src/core/ast/impact.ts +127 -0
- package/src/core/ast/parser.ts +167 -0
- package/src/core/ast/service.ts +233 -0
- package/src/core/ast/symbols.ts +294 -0
- package/src/core/ast/types.ts +125 -0
- package/src/core/brief.ts +138 -0
- package/src/core/commands/findings.ts +120 -6
- package/src/core/commands/review.ts +2 -0
- package/src/core/commands/scaffold.ts +159 -10
- package/src/core/commands/status.ts +63 -0
- package/src/core/commands/update.ts +5 -3
- package/src/core/config.ts +85 -0
- package/src/core/defaults.ts +27 -0
- package/src/core/domain/watchdog.ts +43 -5
- package/src/core/engine/houndsman-engine.ts +16 -1
- package/src/core/engine/review-scope.ts +77 -13
- package/src/core/engine/review-session.ts +27 -2
- package/src/core/env.ts +101 -0
- package/src/core/file-classifier.ts +25 -4
- package/src/core/findings-state.ts +21 -4
- package/src/core/github-review.ts +620 -57
- package/src/core/jev/jev-client.ts +8 -7
- package/src/core/jev/jev-verify.ts +2 -1
- package/src/core/logging/index.ts +17 -0
- package/src/core/logging/logger.ts +187 -0
- package/src/core/orchestrator.ts +454 -16
- package/src/core/paths.ts +3 -0
- package/src/core/project-config.ts +123 -1
- package/src/core/registry-check.ts +148 -6
- package/src/core/renderers/manifest-writer.ts +33 -0
- package/src/core/renderers/resume-renderer.ts +151 -0
- package/src/core/report-builder.ts +7 -4
- package/src/core/rule-text.ts +97 -0
- package/src/core/run-status.ts +39 -5
- package/src/core/services/file-classifier.ts +10 -3
- package/src/core/services/verifier.ts +14 -3
- package/src/core/storage/file/file-storage.ts +2 -0
- package/src/core/storage/paths.ts +6 -5
- package/src/core/storage/sqlite/mappers.ts +2 -0
- package/src/core/storage/sqlite/schema.ts +9 -1
- package/src/core/storage/sqlite/sqlite-storage.ts +5 -1
- package/src/core/storage/types.ts +15 -0
- package/src/core/storage/work-area.ts +12 -0
- package/src/core/types.ts +37 -1
- package/src/core/verify.ts +17 -5
- package/src/harness/README.md +15 -0
- package/src/harness/agent/agent-context.ts +33 -0
- package/src/harness/agent/agent-errors.ts +30 -0
- package/src/harness/agent/agent-factory.ts +45 -0
- package/src/harness/agent/agent-reporter.ts +34 -0
- package/src/harness/agent/agent-result.ts +16 -0
- package/src/harness/agent/agent-runner.ts +32 -0
- package/src/harness/agent/agent-scheduler.ts +37 -0
- package/src/harness/agent/review-agent.ts +391 -0
- package/src/harness/config/harness-config.ts +84 -0
- package/src/harness/hunt/hunt-harness.ts +83 -0
- package/src/harness/hunt/hunt-options.ts +75 -0
- package/src/harness/hunt/hunt-result.ts +17 -0
- package/src/harness/hunt/hunt-session.ts +372 -0
- package/src/harness/hunt/review-planner.ts +98 -0
- package/src/harness/index.ts +36 -0
- package/src/harness/investigation/agent-state.ts +134 -0
- package/src/harness/investigation/evidence-verifier.ts +129 -0
- package/src/harness/investigation/evidence.ts +110 -0
- package/src/harness/investigation/finding-critic.ts +96 -0
- package/src/harness/investigation/investigation-planner.ts +118 -0
- package/src/harness/investigation/investigation-task.ts +10 -0
- package/src/harness/investigation/review-journal.ts +87 -0
- package/src/harness/investigation/rule-analyzer.ts +178 -0
- package/src/harness/investigation/rule-signals.ts +110 -0
- package/src/harness/prompts/agent-schemas.ts +57 -0
- package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
- package/src/harness/prompts/states/conclude-prompt.ts +16 -0
- package/src/harness/prompts/states/investigate-prompt.ts +37 -0
- package/src/harness/prompts/states/orient-prompt.ts +15 -0
- package/src/harness/prompts/states/verify-prompt.ts +22 -0
- package/src/harness/tools/agent-tool.ts +102 -0
- package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
- package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
- package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
- package/src/harness/tools/builtin/find-references-tool.ts +66 -0
- package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
- package/src/harness/tools/builtin/helpers.ts +115 -0
- package/src/harness/tools/builtin/impact-tool.ts +48 -0
- package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
- package/src/harness/tools/builtin/index.ts +59 -0
- package/src/harness/tools/builtin/list-files-tool.ts +85 -0
- package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
- package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
- package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
- package/src/harness/tools/builtin/search-code-tool.ts +65 -0
- package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
- package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
- package/src/harness/tools/path-guard.ts +90 -0
- package/src/harness/tools/result-limiter.ts +49 -0
- package/src/harness/tools/secret-redactor.ts +54 -0
- package/src/harness/tools/tool-cache.ts +50 -0
- package/src/harness/tools/tool-call-budget.ts +60 -0
- package/src/harness/tools/tool-context.ts +5 -0
- package/src/harness/tools/tool-middleware.ts +105 -0
- package/src/harness/tools/tool-policy.ts +51 -0
- package/src/harness/tools/tool-registry.ts +112 -0
- package/src/harness/tools/tool-result.ts +15 -0
- package/src/harness/tools/toolbox.ts +36 -0
- package/src/harness/validation/finding-validator.ts +119 -0
- package/src/index.ts +46 -0
- package/src/server/client/App.tsx +26 -9
- package/src/server/client/ArtifactsPanel.tsx +12 -5
- package/src/server/client/AutoRefresh.tsx +1 -1
- package/src/server/client/Filters.tsx +7 -2
- package/src/server/client/FindingCard.tsx +14 -10
- package/src/server/client/FindingsList.tsx +2 -2
- package/src/server/client/MarkdownText.tsx +92 -12
- package/src/server/client/ResumePanel.tsx +368 -0
- package/src/server/client/RunList.tsx +3 -3
- package/src/server/client/SubmitModal.tsx +49 -6
- package/src/server/client/SummaryBar.tsx +1 -1
- package/src/server/client/icons.tsx +17 -0
- package/src/server/client/{constants.ts → utils/constants.ts} +61 -10
- package/src/server/client/utils/markdown.ts +319 -0
- package/src/server/client/{types.ts → utils/types.ts} +64 -0
- package/src/server/index.ts +66 -7
- package/src/server/run-repository.ts +9 -0
- package/src/skill/OUTPUT-FORMAT.md +12 -0
- package/src/skill/SETUP.md +6 -2
- package/src/skill/example-review.md +22 -14
- package/src/skill/hunt.md +45 -19
- package/src/skill/verify.md +3 -2
- package/src/tests/__snapshots__/baseline.test.ts.snap +126 -42
- package/src/tests/ai-core.test.ts +120 -0
- package/src/tests/ai-features.test.ts +183 -0
- package/src/tests/ast.test.ts +335 -0
- package/src/tests/baseline.test.ts +3 -0
- package/src/tests/code-highlight.test.ts +1 -1
- package/src/tests/env.test.ts +69 -0
- package/src/tests/fallback.test.ts +172 -0
- package/src/tests/findings-state.test.ts +50 -2
- package/src/tests/fixtures/fake-agent-tools.ts +69 -0
- package/src/tests/fixtures/fake-investigation.ts +64 -0
- package/src/tests/fixtures/fake-model-provider.ts +48 -0
- package/src/tests/github-review.test.ts +409 -60
- package/src/tests/harness-agent.test.ts +122 -0
- package/src/tests/harness-investigation.test.ts +206 -0
- package/src/tests/harness-tools.test.ts +148 -0
- package/src/tests/hunt-command.test.ts +212 -0
- package/src/tests/logger.test.ts +83 -0
- package/src/tests/markdown.test.ts +61 -1
- package/src/tests/review-resume.test.ts +101 -0
- package/src/tests/review-scope.test.ts +302 -0
- package/src/tests/review-scripts.test.ts +73 -0
- package/src/tests/rule-analyzer.test.ts +61 -0
- package/src/tests/run-status-view.test.ts +94 -0
- package/src/tests/run-status.test.ts +53 -0
- package/src/tests/server.test.ts +76 -21
- package/src/tests/storage.test.ts +63 -0
- package/src/tests/tool-contracts.test.ts +82 -0
- package/src/tests/verify.test.ts +1 -1
- package/src/server/client/markdown.ts +0 -153
- /package/src/server/client/{api.ts → utils/api.ts} +0 -0
- /package/src/server/client/{code-highlight.ts → utils/code-highlight.ts} +0 -0
- /package/src/server/client/{format.ts → utils/format.ts} +0 -0
- /package/src/server/client/{json-highlight.ts → utils/json-highlight.ts} +0 -0
package/src/core/orchestrator.ts
CHANGED
|
@@ -16,6 +16,15 @@
|
|
|
16
16
|
* review.<watchdog>.md, context e context-summary.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import type { AiClient } from "./ai/ai-client";
|
|
21
|
+
import { AiBriefEnricher } from "./ai/features/brief-enricher";
|
|
22
|
+
import { AiClassifier } from "./ai/features/classifier";
|
|
23
|
+
import { AiContextSummarizer } from "./ai/features/context-summarizer";
|
|
24
|
+
import { AiPreAnalyzer } from "./ai/features/pre-analyzer";
|
|
25
|
+
import type { BriefPlan } from "./ai/schemas";
|
|
26
|
+
import type { AstService } from "./ast/service";
|
|
27
|
+
import type { AstAnalysis, AstFileBrief, AstImpact } from "./ast/types";
|
|
19
28
|
import {
|
|
20
29
|
type HoundsmanConfig,
|
|
21
30
|
resolveJevConcurrency,
|
|
@@ -27,9 +36,11 @@ import { Classification } from "./domain/classification";
|
|
|
27
36
|
import { Watchdog } from "./domain/watchdog";
|
|
28
37
|
import { mergeCarryover } from "./engine/carryover";
|
|
29
38
|
import { ReviewScopeResolver, type RunClassification } from "./engine/review-scope";
|
|
39
|
+
import { ENV_NAMES } from "./env";
|
|
30
40
|
import type { BudgetSelection, ClassificationResult } from "./file-classifier";
|
|
31
41
|
import { buildStateFromFindings, fileReader } from "./findings-state";
|
|
32
42
|
import { resolveSha } from "./git-revision";
|
|
43
|
+
import { syncResolvedThreads } from "./github-review";
|
|
33
44
|
import {
|
|
34
45
|
type JevCandidate,
|
|
35
46
|
type JevCandidatesFile,
|
|
@@ -39,12 +50,22 @@ import {
|
|
|
39
50
|
parseCandidates,
|
|
40
51
|
serializeCandidates,
|
|
41
52
|
} from "./jev";
|
|
42
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
displayPath,
|
|
55
|
+
OUTPUT_FORMAT_PATH,
|
|
56
|
+
REVIEW_FILE,
|
|
57
|
+
REVIEW_RESUME_FILE,
|
|
58
|
+
} from "./paths";
|
|
43
59
|
import { currentBranch } from "./pr-context";
|
|
44
60
|
import { ProjectConfig } from "./project-config";
|
|
45
61
|
import { BriefRenderer } from "./renderers/brief-renderer";
|
|
46
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
type ManifestAi,
|
|
64
|
+
type ManifestAst,
|
|
65
|
+
ManifestWriter,
|
|
66
|
+
} from "./renderers/manifest-writer";
|
|
47
67
|
import { ReportRenderer } from "./renderers/report-renderer";
|
|
68
|
+
import { renderReviewResume } from "./renderers/resume-renderer";
|
|
48
69
|
import {
|
|
49
70
|
type ContextSummary,
|
|
50
71
|
type DomainReport,
|
|
@@ -68,8 +89,12 @@ export type ReviewOptions = {
|
|
|
68
89
|
failOnBlocking: boolean;
|
|
69
90
|
clean: boolean;
|
|
70
91
|
noJev: boolean;
|
|
92
|
+
/** Desativa a análise estrutural (AST) nesta run. */
|
|
93
|
+
noAst: boolean;
|
|
71
94
|
/** Força review completo (ignora o modo incremental). */
|
|
72
95
|
full: boolean;
|
|
96
|
+
/** Desativa as features de IA do fluxo normal (hunt não usa esta flag). */
|
|
97
|
+
noAi: boolean;
|
|
73
98
|
};
|
|
74
99
|
|
|
75
100
|
/** Valores default das opções (o CLI sobrepõe com as flags). */
|
|
@@ -81,7 +106,9 @@ export function defaultOptions(): ReviewOptions {
|
|
|
81
106
|
failOnBlocking: false,
|
|
82
107
|
clean: false,
|
|
83
108
|
noJev: false,
|
|
109
|
+
noAst: false,
|
|
84
110
|
full: false,
|
|
111
|
+
noAi: false,
|
|
85
112
|
};
|
|
86
113
|
}
|
|
87
114
|
|
|
@@ -142,6 +169,8 @@ function writeManifest(
|
|
|
142
169
|
activeIds: string[],
|
|
143
170
|
pending: string[],
|
|
144
171
|
jev: JevCandidatesFile,
|
|
172
|
+
ast?: ManifestAst,
|
|
173
|
+
ai?: ManifestAi,
|
|
145
174
|
): void {
|
|
146
175
|
new ManifestWriter(
|
|
147
176
|
ctx.storage,
|
|
@@ -149,7 +178,7 @@ function writeManifest(
|
|
|
149
178
|
ctx.work,
|
|
150
179
|
new ProjectConfig(config),
|
|
151
180
|
opts,
|
|
152
|
-
).write(classification, activeIds, pending, jev);
|
|
181
|
+
).write(classification, activeIds, pending, jev, ast, ai);
|
|
153
182
|
}
|
|
154
183
|
|
|
155
184
|
// ---------------------------------------------------------------------------
|
|
@@ -164,6 +193,9 @@ function writeBriefs(
|
|
|
164
193
|
context: ContextSummary | null,
|
|
165
194
|
candidates: JevCandidate[],
|
|
166
195
|
config: HoundsmanConfig,
|
|
196
|
+
astAnalyses?: ReadonlyMap<string, AstAnalysis>,
|
|
197
|
+
astImpact?: AstImpact,
|
|
198
|
+
aiPlans?: ReadonlyMap<string, BriefPlan>,
|
|
167
199
|
): void {
|
|
168
200
|
const work = ctx.work;
|
|
169
201
|
const docs = discoverDocs(Object.keys(classification.files), config);
|
|
@@ -173,6 +205,17 @@ function writeBriefs(
|
|
|
173
205
|
const def = findWatchdog(config, id);
|
|
174
206
|
if (!def) continue;
|
|
175
207
|
const { selected, overflow } = budgetFor(classification, id, maxFilesPerBrief);
|
|
208
|
+
const astFiles: AstFileBrief[] = selected.flatMap((file) => {
|
|
209
|
+
const analysis = astAnalyses?.get(file);
|
|
210
|
+
if (!analysis?.delta) return [];
|
|
211
|
+
return [
|
|
212
|
+
{
|
|
213
|
+
file,
|
|
214
|
+
delta: analysis.delta,
|
|
215
|
+
importers: astImpact?.importers[file],
|
|
216
|
+
},
|
|
217
|
+
];
|
|
218
|
+
});
|
|
176
219
|
const brief = renderer.render(new Watchdog(def), {
|
|
177
220
|
watchdogId: id,
|
|
178
221
|
files: selected,
|
|
@@ -186,6 +229,8 @@ function writeBriefs(
|
|
|
186
229
|
docs,
|
|
187
230
|
context,
|
|
188
231
|
candidates: candidates.filter((c) => c.watchdogId === id),
|
|
232
|
+
ast: astFiles,
|
|
233
|
+
aiPlan: aiPlans?.get(id),
|
|
189
234
|
});
|
|
190
235
|
const content = `${brief}\n`;
|
|
191
236
|
work.writeBrief(id, content);
|
|
@@ -212,6 +257,7 @@ async function resolveCandidates(
|
|
|
212
257
|
ctx: ReviewContext,
|
|
213
258
|
config: HoundsmanConfig,
|
|
214
259
|
diff: DiffSource,
|
|
260
|
+
ai?: AiClient,
|
|
215
261
|
): Promise<JevCandidatesFile> {
|
|
216
262
|
const save = (data: JevCandidatesFile): JevCandidatesFile => {
|
|
217
263
|
ctx.storage.writeArtifact(ctx.review.id, {
|
|
@@ -222,13 +268,46 @@ async function resolveCandidates(
|
|
|
222
268
|
return data;
|
|
223
269
|
};
|
|
224
270
|
|
|
271
|
+
// Pré-análise por IA (opcional) tem precedência sobre o Jev.
|
|
272
|
+
if (ai && !opts.noAi) {
|
|
273
|
+
const analyzer = new AiPreAnalyzer(ai);
|
|
274
|
+
if (analyzer.enabled) {
|
|
275
|
+
const candidates: JevCandidate[] = [];
|
|
276
|
+
for (const [id, files] of Object.entries(classification.watchdogs)) {
|
|
277
|
+
const def = findWatchdog(config, id);
|
|
278
|
+
if (!def) continue;
|
|
279
|
+
const inputs = files.slice(0, 5).flatMap((file) => {
|
|
280
|
+
const snippet = readSnippet(file);
|
|
281
|
+
return snippet ? [{ file, snippet }] : [];
|
|
282
|
+
});
|
|
283
|
+
candidates.push(
|
|
284
|
+
...(await analyzer.analyze({
|
|
285
|
+
watchdogId: id,
|
|
286
|
+
name: def.name,
|
|
287
|
+
rules: def.rules,
|
|
288
|
+
files: inputs,
|
|
289
|
+
})),
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
return save({
|
|
293
|
+
available: true,
|
|
294
|
+
model: ai.modelId,
|
|
295
|
+
threshold: JevCandidatesStore.THRESHOLD,
|
|
296
|
+
analyzedFiles: Object.values(classification.watchdogs).flat().length,
|
|
297
|
+
candidates,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
225
302
|
if (opts.noJev) {
|
|
226
303
|
return save(JevCandidatesStore.unavailable("desativado (--no-jev)"));
|
|
227
304
|
}
|
|
228
305
|
|
|
229
306
|
const client = new JevClient();
|
|
230
307
|
if (!client.isConfigured()) {
|
|
231
|
-
return save(
|
|
308
|
+
return save(
|
|
309
|
+
JevCandidatesStore.unavailable(`${ENV_NAMES.typeSafeApiKey} não configurada`),
|
|
310
|
+
);
|
|
232
311
|
}
|
|
233
312
|
|
|
234
313
|
try {
|
|
@@ -297,41 +376,208 @@ function budgetMessage(
|
|
|
297
376
|
];
|
|
298
377
|
}
|
|
299
378
|
|
|
379
|
+
/** Primeiras 80 linhas de um arquivo (pré-análise por IA; fail-open). */
|
|
380
|
+
function readSnippet(file: string): string | null {
|
|
381
|
+
try {
|
|
382
|
+
return readFileSync(file, "utf8").split("\n").slice(0, 80).join("\n");
|
|
383
|
+
} catch {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
300
388
|
/** Resultado do prepare — mensagens para o CLI imprimir. */
|
|
301
389
|
export type PrepareResult = { messages: string[] };
|
|
302
390
|
|
|
391
|
+
/**
|
|
392
|
+
* Classificação fallback por IA (opcional): atribui arquivos não classificados
|
|
393
|
+
* a watchdogs conhecidos. Fail-open — falha mantém `unclassified`.
|
|
394
|
+
*/
|
|
395
|
+
async function applyAiClassification(
|
|
396
|
+
classification: RunClassification,
|
|
397
|
+
config: HoundsmanConfig,
|
|
398
|
+
ai: AiClient | undefined,
|
|
399
|
+
opts: ReviewOptions,
|
|
400
|
+
): Promise<void> {
|
|
401
|
+
if (!ai || opts.noAi || classification.unclassified.length === 0) return;
|
|
402
|
+
const classifier = new AiClassifier(ai);
|
|
403
|
+
if (!classifier.enabled) return;
|
|
404
|
+
const watchdogs = config.watchdogIds.flatMap((id) => {
|
|
405
|
+
const def = config.watchdogs[id];
|
|
406
|
+
return def ? [{ id, name: def.name, scope: def.scope }] : [];
|
|
407
|
+
});
|
|
408
|
+
const assignments = await classifier.classify(classification.unclassified, watchdogs);
|
|
409
|
+
if (assignments.size === 0) return;
|
|
410
|
+
for (const [file, id] of assignments) {
|
|
411
|
+
const list = classification.watchdogs[id] ?? [];
|
|
412
|
+
if (!list.includes(file)) list.push(file);
|
|
413
|
+
classification.watchdogs[id] = list;
|
|
414
|
+
classification.stats[id] = list.length;
|
|
415
|
+
}
|
|
416
|
+
classification.unclassified = classification.unclassified.filter(
|
|
417
|
+
(file) => !assignments.has(file),
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Resumo do contexto da PR: usa o do subagente ou, na ausência, a IA
|
|
423
|
+
* (`AiContextSummarizer`). Grava o artefato e o staging.
|
|
424
|
+
*/
|
|
425
|
+
async function resolveContextSummary(
|
|
426
|
+
ctx: ReviewContext,
|
|
427
|
+
ai: AiClient | undefined,
|
|
428
|
+
opts: ReviewOptions,
|
|
429
|
+
): Promise<ContextSummary | null> {
|
|
430
|
+
const existing = ctx.work.readContextSummary() ?? readPersistedContextSummary(ctx);
|
|
431
|
+
if (existing) return existing;
|
|
432
|
+
if (!ai || opts.noAi) return null;
|
|
433
|
+
const raw = ctx.work.readContextRaw();
|
|
434
|
+
if (!raw) return null;
|
|
435
|
+
const summary = await new AiContextSummarizer(ai).summarize(raw);
|
|
436
|
+
if (!summary) return null;
|
|
437
|
+
ctx.work.writeContextSummary(summary);
|
|
438
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
439
|
+
name: "context-summary.json",
|
|
440
|
+
kind: "context-summary",
|
|
441
|
+
content: `${JSON.stringify(summary, null, 2)}\n`,
|
|
442
|
+
});
|
|
443
|
+
return summary;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Planos de revisão por watchdog (uma chamada por brief, concorrência
|
|
448
|
+
* limitada). Sem IA/chave/feature, devolve mapa vazio e o brief fica igual.
|
|
449
|
+
*/
|
|
450
|
+
async function buildAiPlans(
|
|
451
|
+
activeIds: string[],
|
|
452
|
+
classification: ClassificationResult,
|
|
453
|
+
config: HoundsmanConfig,
|
|
454
|
+
context: ContextSummary | null,
|
|
455
|
+
astAnalyses: ReadonlyMap<string, AstAnalysis> | undefined,
|
|
456
|
+
astImpact: AstImpact | undefined,
|
|
457
|
+
ai: AiClient | undefined,
|
|
458
|
+
opts: ReviewOptions,
|
|
459
|
+
): Promise<Map<string, BriefPlan>> {
|
|
460
|
+
const plans = new Map<string, BriefPlan>();
|
|
461
|
+
if (!ai || opts.noAi) return plans;
|
|
462
|
+
const enricher = new AiBriefEnricher(ai);
|
|
463
|
+
if (!enricher.enabled) return plans;
|
|
464
|
+
|
|
465
|
+
const docs = discoverDocs(Object.keys(classification.files), config);
|
|
466
|
+
const maxFilesPerBrief = resolveMaxFilesPerBrief(config);
|
|
467
|
+
const inputs = [];
|
|
468
|
+
for (const id of activeIds) {
|
|
469
|
+
const def = findWatchdog(config, id);
|
|
470
|
+
if (!def) continue;
|
|
471
|
+
const { selected } = budgetFor(classification, id, maxFilesPerBrief);
|
|
472
|
+
const astFiles: AstFileBrief[] = selected.flatMap((file) => {
|
|
473
|
+
const analysis = astAnalyses?.get(file);
|
|
474
|
+
if (!analysis?.delta) return [];
|
|
475
|
+
return [
|
|
476
|
+
{ file, delta: analysis.delta, importers: astImpact?.importers[file] },
|
|
477
|
+
];
|
|
478
|
+
});
|
|
479
|
+
inputs.push({
|
|
480
|
+
watchdogId: id,
|
|
481
|
+
name: def.name,
|
|
482
|
+
description: def.description,
|
|
483
|
+
scope: def.scope,
|
|
484
|
+
rules: def.rules,
|
|
485
|
+
files: selected,
|
|
486
|
+
churn: classification.churn,
|
|
487
|
+
context,
|
|
488
|
+
ast: astFiles,
|
|
489
|
+
docs,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
return enricher.enrichAll(inputs);
|
|
493
|
+
}
|
|
494
|
+
|
|
303
495
|
/** Passo 1 — descobre os arquivos e grava briefs + manifesto. */
|
|
304
496
|
export async function prepare(
|
|
305
497
|
opts: ReviewOptions,
|
|
306
498
|
branch: string,
|
|
307
499
|
config: HoundsmanConfig,
|
|
308
500
|
ctx: ReviewContext,
|
|
501
|
+
ast?: AstService,
|
|
502
|
+
ai?: AiClient,
|
|
309
503
|
): Promise<PrepareResult> {
|
|
310
504
|
const messages: string[] = [];
|
|
311
505
|
ctx.work.ensure();
|
|
312
506
|
if (opts.clean) ctx.work.cleanFindings();
|
|
313
507
|
ctx.storage.updateReview(ctx.review.id, { base: opts.base, head: opts.head });
|
|
314
508
|
|
|
509
|
+
// Marca o commit revisado e decide entre completo, incremental ou no-op.
|
|
510
|
+
const headSha = resolveSha(opts.head);
|
|
511
|
+
const previous = ctx.storage.readFindings(ctx.review.id);
|
|
512
|
+
const scope = await new ReviewScopeResolver(
|
|
513
|
+
opts,
|
|
514
|
+
config,
|
|
515
|
+
opts.noAst ? undefined : ast,
|
|
516
|
+
).resolve(previous, headSha);
|
|
517
|
+
|
|
518
|
+
// Mesmo commit já revisado: preserva artefatos e triagem, sem re-revisar.
|
|
519
|
+
if (scope.kind === "noop") {
|
|
520
|
+
const status = ctx.storage.getReview(ctx.review.id)?.status ?? null;
|
|
521
|
+
// Ciclo em andamento continua válido (os findings já despachados devem
|
|
522
|
+
// ser finalizados); fora isso, o relatório anterior é a resposta final.
|
|
523
|
+
const pendingCycle = status === "reviewing";
|
|
524
|
+
if (!pendingCycle) {
|
|
525
|
+
writeRunStatus(ctx.storage, ctx.review.id, { status: "review_done" });
|
|
526
|
+
}
|
|
527
|
+
messages.push(
|
|
528
|
+
`Nenhuma alteração desde a última revisão (${shortSha(scope.reviewedFrom)}).`,
|
|
529
|
+
pendingCycle
|
|
530
|
+
? "Nada novo a preparar — o ciclo em andamento continua; rode `--finalize` quando os findings estiverem gravados."
|
|
531
|
+
: "Nada a revisar — o relatório anterior foi mantido. Use `--full` para re-revisar.",
|
|
532
|
+
);
|
|
533
|
+
return { messages };
|
|
534
|
+
}
|
|
535
|
+
|
|
315
536
|
// Marca o run como "revisando" (reinicia o ciclo) — a UI mostra o status.
|
|
316
537
|
writeRunStatus(ctx.storage, ctx.review.id, {
|
|
317
538
|
status: "reviewing",
|
|
318
539
|
resetStartedAt: true,
|
|
319
540
|
});
|
|
320
541
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const previous = ctx.storage.readFindings(ctx.review.id);
|
|
324
|
-
const { classification, diff } = new ReviewScopeResolver(opts, config).resolve(
|
|
325
|
-
previous,
|
|
326
|
-
headSha,
|
|
327
|
-
);
|
|
542
|
+
const { classification, diff } = scope;
|
|
543
|
+
await applyAiClassification(classification, config, ai, opts);
|
|
328
544
|
ctx.storage.writeArtifact(ctx.review.id, {
|
|
329
545
|
name: "classification.json",
|
|
330
546
|
kind: "classification",
|
|
331
547
|
content: `${JSON.stringify(classification, null, 2)}\n`,
|
|
332
548
|
});
|
|
333
549
|
|
|
334
|
-
const
|
|
550
|
+
const astAnalyses = scope.ast;
|
|
551
|
+
const astImpact = astAnalyses && ast ? await ast.impact(astAnalyses) : undefined;
|
|
552
|
+
if (astAnalyses && ast?.enabled) {
|
|
553
|
+
const summary = {
|
|
554
|
+
enabled: true,
|
|
555
|
+
files: Object.fromEntries(
|
|
556
|
+
[...astAnalyses].map(([path, analysis]) => [
|
|
557
|
+
path,
|
|
558
|
+
{
|
|
559
|
+
facts: analysis.facts,
|
|
560
|
+
delta: analysis.delta,
|
|
561
|
+
deleted: analysis.deleted,
|
|
562
|
+
},
|
|
563
|
+
]),
|
|
564
|
+
),
|
|
565
|
+
};
|
|
566
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
567
|
+
name: "ast-summary.json",
|
|
568
|
+
kind: "ast",
|
|
569
|
+
content: `${JSON.stringify(summary, null, 2)}\n`,
|
|
570
|
+
});
|
|
571
|
+
if (astImpact) {
|
|
572
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
573
|
+
name: "ast-impact.json",
|
|
574
|
+
kind: "ast-impact",
|
|
575
|
+
content: `${JSON.stringify(astImpact, null, 2)}\n`,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const jev = await resolveCandidates(opts, classification, ctx, config, diff, ai);
|
|
335
581
|
|
|
336
582
|
const activeIds = Object.keys(classification.watchdogs);
|
|
337
583
|
if (Object.keys(classification.files).length === 0) {
|
|
@@ -356,17 +602,103 @@ export async function prepare(
|
|
|
356
602
|
);
|
|
357
603
|
}
|
|
358
604
|
|
|
359
|
-
const context = ctx.work.readContextSummary() ?? readPersistedContextSummary(ctx);
|
|
360
605
|
persistContext(ctx);
|
|
606
|
+
const context = await resolveContextSummary(ctx, ai, opts);
|
|
607
|
+
|
|
608
|
+
const manifestAst: ManifestAst | undefined = astAnalyses
|
|
609
|
+
? {
|
|
610
|
+
enabled: ast?.enabled ?? false,
|
|
611
|
+
languages: [
|
|
612
|
+
...new Set(
|
|
613
|
+
[...astAnalyses.values()].map(
|
|
614
|
+
(analysis) => analysis.facts.language,
|
|
615
|
+
),
|
|
616
|
+
),
|
|
617
|
+
].sort(),
|
|
618
|
+
files: astAnalyses.size,
|
|
619
|
+
impact: {
|
|
620
|
+
affectedFiles: astImpact?.affectedFiles.length ?? 0,
|
|
621
|
+
importers: Object.keys(astImpact?.importers ?? {}).length,
|
|
622
|
+
exportedChanges: astImpact?.exportedChanges.length ?? 0,
|
|
623
|
+
},
|
|
624
|
+
}
|
|
625
|
+
: undefined;
|
|
626
|
+
|
|
627
|
+
const aiPlans = await buildAiPlans(
|
|
628
|
+
activeIds,
|
|
629
|
+
classification,
|
|
630
|
+
config,
|
|
631
|
+
context,
|
|
632
|
+
astAnalyses,
|
|
633
|
+
astImpact,
|
|
634
|
+
ai,
|
|
635
|
+
opts,
|
|
636
|
+
);
|
|
637
|
+
if (aiPlans.size > 0) {
|
|
638
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
639
|
+
name: "ai-briefs.json",
|
|
640
|
+
kind: "ai-brief",
|
|
641
|
+
content: `${JSON.stringify(
|
|
642
|
+
{ plans: Object.fromEntries(aiPlans) },
|
|
643
|
+
null,
|
|
644
|
+
2,
|
|
645
|
+
)}\n`,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
const aiUsage = ai?.recorder.total;
|
|
649
|
+
const manifestAi: ManifestAi | undefined =
|
|
650
|
+
ai && !opts.noAi && (aiPlans.size > 0 || (aiUsage?.calls ?? 0) > 0)
|
|
651
|
+
? {
|
|
652
|
+
enabled: true,
|
|
653
|
+
provider: ai.projectConfig.aiProvider,
|
|
654
|
+
model: ai.modelId,
|
|
655
|
+
briefPlans: aiPlans.size,
|
|
656
|
+
usage: aiUsage ?? {
|
|
657
|
+
inputTokens: 0,
|
|
658
|
+
outputTokens: 0,
|
|
659
|
+
totalTokens: 0,
|
|
660
|
+
calls: 0,
|
|
661
|
+
},
|
|
662
|
+
}
|
|
663
|
+
: undefined;
|
|
361
664
|
|
|
362
|
-
writeBriefs(
|
|
363
|
-
|
|
665
|
+
writeBriefs(
|
|
666
|
+
activeIds,
|
|
667
|
+
classification,
|
|
668
|
+
ctx,
|
|
669
|
+
branch,
|
|
670
|
+
context,
|
|
671
|
+
jev.candidates,
|
|
672
|
+
config,
|
|
673
|
+
astAnalyses,
|
|
674
|
+
astImpact,
|
|
675
|
+
aiPlans,
|
|
676
|
+
);
|
|
677
|
+
writeManifest(
|
|
678
|
+
ctx,
|
|
679
|
+
config,
|
|
680
|
+
opts,
|
|
681
|
+
classification,
|
|
682
|
+
activeIds,
|
|
683
|
+
activeIds,
|
|
684
|
+
jev,
|
|
685
|
+
manifestAst,
|
|
686
|
+
manifestAi,
|
|
687
|
+
);
|
|
364
688
|
|
|
365
689
|
messages.push(
|
|
366
690
|
...discoveryMessages(activeIds, classification, ctx.work.briefsDir),
|
|
367
691
|
...budgetMessage(classification, config),
|
|
368
692
|
...jevSummary(jev),
|
|
369
693
|
);
|
|
694
|
+
if (astAnalyses) {
|
|
695
|
+
messages.push(
|
|
696
|
+
`AST: ${astAnalyses.size} arquivo(s) analisado(s), ` +
|
|
697
|
+
`${astImpact?.affectedFiles.length ?? 0} impactado(s).`,
|
|
698
|
+
);
|
|
699
|
+
} else {
|
|
700
|
+
messages.push("AST: desabilitado ou sem grammar compatível.");
|
|
701
|
+
}
|
|
370
702
|
|
|
371
703
|
if (context) {
|
|
372
704
|
messages.push("Contexto da PR resumido e injetado nos briefs.");
|
|
@@ -375,6 +707,17 @@ export async function prepare(
|
|
|
375
707
|
"Sem contexto da PR (rode `bun run houndsman context` antes, se houver PR).",
|
|
376
708
|
);
|
|
377
709
|
}
|
|
710
|
+
if (aiPlans.size > 0) {
|
|
711
|
+
messages.push(
|
|
712
|
+
`IA: plano de revisão em ${aiPlans.size} brief(s) (sugestão, não substitui regras).`,
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
if (aiUsage && aiUsage.calls > 0) {
|
|
716
|
+
messages.push(
|
|
717
|
+
`IA: ${aiUsage.calls} chamada(s), ${aiUsage.totalTokens} token(s).`,
|
|
718
|
+
);
|
|
719
|
+
ai?.recorder.write(ctx.storage, ctx.review.id);
|
|
720
|
+
}
|
|
378
721
|
|
|
379
722
|
messages.push(
|
|
380
723
|
`\nPróximo passo: despachar os sub-agentes (um por brief em ${displayPath(ctx.work.briefsDir)}/).`,
|
|
@@ -415,6 +758,24 @@ function persistContext(ctx: ReviewContext): void {
|
|
|
415
758
|
// Passo 2 — finalizar
|
|
416
759
|
// ---------------------------------------------------------------------------
|
|
417
760
|
|
|
761
|
+
/** Reaproveita `ast`/`ai` do manifesto anterior (o finalize não os recalcula). */
|
|
762
|
+
function readManifestExtras(ctx: ReviewContext): {
|
|
763
|
+
ast?: ManifestAst;
|
|
764
|
+
ai?: ManifestAi;
|
|
765
|
+
} {
|
|
766
|
+
const artifact = ctx.storage.readArtifact(ctx.review.id, "manifest.json");
|
|
767
|
+
if (!artifact) return {};
|
|
768
|
+
try {
|
|
769
|
+
const parsed = JSON.parse(artifact.content) as {
|
|
770
|
+
ast?: ManifestAst;
|
|
771
|
+
ai?: ManifestAi;
|
|
772
|
+
};
|
|
773
|
+
return { ast: parsed.ast, ai: parsed.ai };
|
|
774
|
+
} catch {
|
|
775
|
+
return {};
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
418
779
|
/** Lê o `classification.json` persistido pelo prepare (lança se ausente). */
|
|
419
780
|
function readClassification(ctx: ReviewContext): RunClassification {
|
|
420
781
|
const artifact = ctx.storage.readArtifact(ctx.review.id, "classification.json");
|
|
@@ -510,6 +871,27 @@ export function finalize(
|
|
|
510
871
|
config: HoundsmanConfig,
|
|
511
872
|
ctx: ReviewContext,
|
|
512
873
|
): FinalizeResult {
|
|
874
|
+
// `--finalize` só consolida um ciclo preparado (status `reviewing`). Sem esta
|
|
875
|
+
// guarda, um prepare no-op seguido de `--finalize` reescreveria o relatório
|
|
876
|
+
// anterior com a classificação antiga (perdendo os relatórios por domínio).
|
|
877
|
+
// O `ctx.review` é um snapshot: relê o status atual do storage.
|
|
878
|
+
const status = ctx.storage.getReview(ctx.review.id)?.status ?? null;
|
|
879
|
+
if (status !== "reviewing") {
|
|
880
|
+
const state = ctx.storage.readFindings(ctx.review.id);
|
|
881
|
+
const present = Object.values(state?.findings ?? {}).filter(
|
|
882
|
+
(finding) => finding.present,
|
|
883
|
+
);
|
|
884
|
+
return {
|
|
885
|
+
reportPath: ctx.storage.location(ctx.review.id),
|
|
886
|
+
blockers: present.filter((f) => f.severity === "blocking").length,
|
|
887
|
+
attention: present.filter((f) => f.severity === "attention").length,
|
|
888
|
+
messages: [
|
|
889
|
+
`Nada a finalizar: nenhum ciclo de revisão pendente (status: ${status ?? "sem status"}).`,
|
|
890
|
+
"Se nada mudou desde a última revisão, o relatório anterior foi mantido; use `--full` para re-revisar.",
|
|
891
|
+
],
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
513
895
|
const classification = readClassification(ctx);
|
|
514
896
|
const activeIds = Object.keys(classification.watchdogs);
|
|
515
897
|
|
|
@@ -517,7 +899,18 @@ export function finalize(
|
|
|
517
899
|
const { reports, pending, warnings } = ctx.work.readFindings(activeIds);
|
|
518
900
|
messages.push(...warnings);
|
|
519
901
|
const jev = readCandidates(ctx, config);
|
|
520
|
-
|
|
902
|
+
const extras = readManifestExtras(ctx);
|
|
903
|
+
writeManifest(
|
|
904
|
+
ctx,
|
|
905
|
+
config,
|
|
906
|
+
opts,
|
|
907
|
+
classification,
|
|
908
|
+
activeIds,
|
|
909
|
+
pending,
|
|
910
|
+
jev,
|
|
911
|
+
extras.ast,
|
|
912
|
+
extras.ai,
|
|
913
|
+
);
|
|
521
914
|
|
|
522
915
|
if (pending.length > 0) {
|
|
523
916
|
throw new Error(
|
|
@@ -546,12 +939,44 @@ export function finalize(
|
|
|
546
939
|
branch,
|
|
547
940
|
readFile: fileReader(process.cwd()),
|
|
548
941
|
previous: ctx.storage.readFindings(ctx.review.id),
|
|
942
|
+
reviewedPaths: new Set([
|
|
943
|
+
...reports.flatMap((report) => report.reviewedFiles),
|
|
944
|
+
// Arquivo deletado: o achado deixou de existir junto com ele.
|
|
945
|
+
...classification.deleted,
|
|
946
|
+
]),
|
|
549
947
|
now,
|
|
550
948
|
headSha: classification.headSha,
|
|
551
949
|
reviewed: classification.headSha
|
|
552
950
|
? { headSha: classification.headSha, at: now }
|
|
553
951
|
: undefined,
|
|
554
952
|
});
|
|
953
|
+
|
|
954
|
+
// Findings corrigidos entre revisões (ausentes + done) têm a conversa do PR
|
|
955
|
+
// resolvida automaticamente; as já resolvidas no GitHub são puladas. Só
|
|
956
|
+
// toca o `gh` quando há vínculo remoto pendente (fail-open).
|
|
957
|
+
const threadIds = Object.values(state.findings)
|
|
958
|
+
.filter(
|
|
959
|
+
(finding) =>
|
|
960
|
+
!finding.present &&
|
|
961
|
+
finding.execState === "done" &&
|
|
962
|
+
finding.github &&
|
|
963
|
+
finding.github.kind !== "comment" &&
|
|
964
|
+
!finding.github.resolvedAt,
|
|
965
|
+
)
|
|
966
|
+
.map((finding) => finding.id);
|
|
967
|
+
if (threadIds.length > 0) {
|
|
968
|
+
const sync = syncResolvedThreads({ state, ids: threadIds, now });
|
|
969
|
+
if (!sync.available) {
|
|
970
|
+
messages.push(`GitHub: threads não sincronizadas (${sync.reason}).`);
|
|
971
|
+
} else if (sync.checked > 0) {
|
|
972
|
+
const failures =
|
|
973
|
+
sync.failed.length > 0 ? `, ${sync.failed.length} falha(s)` : "";
|
|
974
|
+
messages.push(
|
|
975
|
+
`GitHub: ${sync.resolved.length} thread(s) resolvida(s), ${sync.skipped.length} já resolvida(s)/pulada(s)${failures}.`,
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
555
980
|
ctx.storage.writeFindings(ctx.review.id, state);
|
|
556
981
|
ctx.storage.updateReview(ctx.review.id, {
|
|
557
982
|
base: opts.base,
|
|
@@ -585,6 +1010,19 @@ export function finalize(
|
|
|
585
1010
|
kind: "review",
|
|
586
1011
|
content: `${incrementalNote(classification)}${markdown}\n`,
|
|
587
1012
|
});
|
|
1013
|
+
// Resumo final para leitura humana — não participa de nenhuma etapa seguinte.
|
|
1014
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
1015
|
+
name: REVIEW_RESUME_FILE,
|
|
1016
|
+
kind: "resume",
|
|
1017
|
+
content: renderReviewResume({
|
|
1018
|
+
branch,
|
|
1019
|
+
date: now.slice(0, 10),
|
|
1020
|
+
context,
|
|
1021
|
+
classification,
|
|
1022
|
+
findings: merged,
|
|
1023
|
+
nameOf,
|
|
1024
|
+
}),
|
|
1025
|
+
});
|
|
588
1026
|
|
|
589
1027
|
const location = ctx.storage.location(ctx.review.id);
|
|
590
1028
|
messages.push(
|
package/src/core/paths.ts
CHANGED
|
@@ -52,3 +52,6 @@ export const VERIFY_FILE = "verify.md";
|
|
|
52
52
|
|
|
53
53
|
/** Relatório consolidado da review (artefato `review.md`). */
|
|
54
54
|
export const REVIEW_FILE = "review.md";
|
|
55
|
+
|
|
56
|
+
/** Resumo final da review (artefato `review-resume.md`) — leitura humana apenas. */
|
|
57
|
+
export const REVIEW_RESUME_FILE = "review-resume.md";
|