@payconductor/houndsman 0.11.0 → 0.13.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 +14 -13
- package/package.json +1 -1
- package/src/cli/commands/doctor-command.ts +22 -0
- package/src/cli/commands/hunt-command.ts +440 -29
- package/src/cli/commands/review-command.ts +20 -2
- package/src/cli/help.ts +16 -8
- package/src/cli/index.ts +15 -2
- package/src/cli/progress.ts +45 -0
- package/src/core/ai/ai-client.ts +3 -7
- package/src/core/ai/ai-sdk-model-provider.ts +43 -24
- package/src/core/ai/features/brief-enricher.ts +0 -1
- package/src/core/ai/features/classifier.ts +0 -1
- package/src/core/ai/features/context-summarizer.ts +0 -1
- package/src/core/ai/features/pre-analyzer.ts +0 -1
- package/src/core/ai/features/technical-summarizer.ts +0 -1
- package/src/core/ai/features/verifier.ts +0 -1
- package/src/core/ai/index.ts +6 -4
- package/src/core/ai/prompts/pre-analysis.ts +3 -2
- package/src/core/ai/schemas.ts +1 -23
- package/src/core/ai/types.ts +4 -15
- package/src/core/ai/usage-budget.ts +1 -1
- package/src/core/ast/diff.ts +37 -2
- package/src/core/ast/service.ts +1 -1
- package/src/core/brief.ts +54 -5
- package/src/core/changed-scope.ts +207 -0
- package/src/core/commands/scaffold.ts +12 -18
- package/src/core/config.ts +30 -15
- package/src/core/defaults.ts +14 -5
- package/src/core/engine/review-scope.ts +131 -17
- package/src/core/engine/review-session.ts +7 -3
- package/src/core/engine/review-workspace.ts +14 -0
- package/src/core/env.ts +14 -0
- package/src/core/hunt/hunt-runner.ts +328 -0
- package/src/core/hunt/index.ts +59 -0
- package/src/core/hunt/maki-install.ts +331 -0
- package/src/core/hunt/maki-model.ts +101 -0
- package/src/core/hunt/maki-prompt.ts +71 -0
- package/src/core/hunt/maki-release.ts +104 -0
- package/src/core/hunt/maki-runner.ts +276 -0
- package/src/core/hunt/pool.ts +30 -0
- package/src/core/jev/jev-detector.ts +7 -2
- package/src/core/logging/index.ts +3 -1
- package/src/core/logging/logger.ts +22 -3
- package/src/core/logging/progress.ts +282 -0
- package/src/core/orchestrator.ts +173 -59
- package/src/core/project-config.ts +34 -24
- package/src/core/registry-check.ts +2 -12
- package/src/core/rule-text.ts +3 -55
- package/src/core/services/git-repository.ts +8 -0
- package/src/core/storage/types.ts +1 -1
- package/src/core/types.ts +3 -14
- package/src/index.ts +74 -3
- package/src/server/client/utils/constants.ts +3 -6
- package/src/server/client/utils/types.ts +0 -1
- package/src/skill/OUTPUT-FORMAT.md +5 -0
- package/src/skill/README.md +14 -1
- package/src/skill/SETUP.md +22 -2
- package/src/skill/SKILL.md +4 -0
- package/src/skill/fix.md +1 -1
- package/src/skill/hunt.md +38 -6
- package/src/tests/__snapshots__/baseline.test.ts.snap +69 -32
- package/src/tests/ai-core.test.ts +38 -1
- package/src/tests/changed-scope.test.ts +197 -0
- package/src/tests/cli.test.ts +1 -0
- package/src/tests/fallback.test.ts +1 -96
- package/src/tests/fixtures/fake-model-provider.ts +0 -1
- package/src/tests/hunt-cli.test.ts +185 -0
- package/src/tests/hunt-install.test.ts +153 -0
- package/src/tests/hunt-maki.test.ts +467 -0
- package/src/tests/progress.test.ts +117 -0
- package/src/tests/review-scope.test.ts +95 -0
- package/src/tests/worktree.test.ts +1 -3
- package/src/harness/README.md +0 -15
- package/src/harness/agent/agent-context.ts +0 -33
- package/src/harness/agent/agent-errors.ts +0 -30
- package/src/harness/agent/agent-factory.ts +0 -46
- package/src/harness/agent/agent-reporter.ts +0 -34
- package/src/harness/agent/agent-result.ts +0 -16
- package/src/harness/agent/agent-runner.ts +0 -32
- package/src/harness/agent/agent-scheduler.ts +0 -37
- package/src/harness/agent/review-agent.ts +0 -401
- package/src/harness/config/harness-config.ts +0 -89
- package/src/harness/hunt/hunt-harness.ts +0 -83
- package/src/harness/hunt/hunt-options.ts +0 -92
- package/src/harness/hunt/hunt-result.ts +0 -17
- package/src/harness/hunt/hunt-session.ts +0 -359
- package/src/harness/hunt/review-planner.ts +0 -98
- package/src/harness/index.ts +0 -36
- package/src/harness/investigation/agent-state.ts +0 -134
- package/src/harness/investigation/evidence-verifier.ts +0 -129
- package/src/harness/investigation/evidence.ts +0 -110
- package/src/harness/investigation/finding-critic.ts +0 -96
- package/src/harness/investigation/investigation-planner.ts +0 -118
- package/src/harness/investigation/investigation-task.ts +0 -10
- package/src/harness/investigation/review-journal.ts +0 -87
- package/src/harness/investigation/rule-analyzer.ts +0 -178
- package/src/harness/investigation/rule-signals.ts +0 -110
- package/src/harness/prompts/agent-schemas.ts +0 -57
- package/src/harness/prompts/reviewer-prompt-builder.ts +0 -101
- package/src/harness/prompts/states/conclude-prompt.ts +0 -16
- package/src/harness/prompts/states/investigate-prompt.ts +0 -37
- package/src/harness/prompts/states/orient-prompt.ts +0 -15
- package/src/harness/prompts/states/verify-prompt.ts +0 -22
- package/src/harness/tools/agent-tool.ts +0 -102
- package/src/harness/tools/builtin/changed-symbols-tool.ts +0 -48
- package/src/harness/tools/builtin/doc-context-tool.ts +0 -74
- package/src/harness/tools/builtin/file-outline-tool.ts +0 -66
- package/src/harness/tools/builtin/find-references-tool.ts +0 -66
- package/src/harness/tools/builtin/find-symbol-tool.ts +0 -91
- package/src/harness/tools/builtin/helpers.ts +0 -115
- package/src/harness/tools/builtin/impact-tool.ts +0 -48
- package/src/harness/tools/builtin/imports-exports-tool.ts +0 -53
- package/src/harness/tools/builtin/index.ts +0 -59
- package/src/harness/tools/builtin/list-files-tool.ts +0 -85
- package/src/harness/tools/builtin/pr-context-tool.ts +0 -41
- package/src/harness/tools/builtin/read-file-range-tool.ts +0 -73
- package/src/harness/tools/builtin/rule-signals-tool.ts +0 -36
- package/src/harness/tools/builtin/search-code-tool.ts +0 -65
- package/src/harness/tools/builtin/symbol-context-tool.ts +0 -106
- package/src/harness/tools/builtin/tests-for-symbol-tool.ts +0 -61
- package/src/harness/tools/path-guard.ts +0 -90
- package/src/harness/tools/result-limiter.ts +0 -49
- package/src/harness/tools/secret-redactor.ts +0 -54
- package/src/harness/tools/tool-cache.ts +0 -50
- package/src/harness/tools/tool-call-budget.ts +0 -60
- package/src/harness/tools/tool-context.ts +0 -5
- package/src/harness/tools/tool-middleware.ts +0 -105
- package/src/harness/tools/tool-policy.ts +0 -51
- package/src/harness/tools/tool-registry.ts +0 -112
- package/src/harness/tools/tool-result.ts +0 -15
- package/src/harness/tools/toolbox.ts +0 -36
- package/src/harness/validation/finding-validator.ts +0 -119
- package/src/tests/fixtures/fake-agent-tools.ts +0 -69
- package/src/tests/fixtures/fake-investigation.ts +0 -64
- package/src/tests/harness-agent.test.ts +0 -141
- package/src/tests/harness-investigation.test.ts +0 -206
- package/src/tests/harness-tools.test.ts +0 -148
- package/src/tests/hunt-command.test.ts +0 -212
- package/src/tests/rule-analyzer.test.ts +0 -61
- package/src/tests/tool-contracts.test.ts +0 -82
package/src/core/orchestrator.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* review.<watchdog>.md, context e context-summary.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { existsSync
|
|
19
|
+
import { existsSync } from "node:fs";
|
|
20
20
|
import { join } from "node:path";
|
|
21
21
|
import type { AiClient } from "./ai/ai-client";
|
|
22
22
|
import { AiBriefEnricher } from "./ai/features/brief-enricher";
|
|
@@ -28,6 +28,7 @@ import type { BriefPlan } from "./ai/schemas";
|
|
|
28
28
|
import { assetPath, OUTPUT_FORMAT_FILE } from "./assets";
|
|
29
29
|
import type { AstService } from "./ast/service";
|
|
30
30
|
import type { AstAnalysis, AstFileBrief, AstImpact } from "./ast/types";
|
|
31
|
+
import { summarizeScope } from "./changed-scope";
|
|
31
32
|
import {
|
|
32
33
|
type HoundsmanConfig,
|
|
33
34
|
resolveJevConcurrency,
|
|
@@ -38,7 +39,11 @@ import { discoverDocs } from "./doc-discoverer";
|
|
|
38
39
|
import { Classification } from "./domain/classification";
|
|
39
40
|
import { Watchdog } from "./domain/watchdog";
|
|
40
41
|
import { mergeCarryover } from "./engine/carryover";
|
|
41
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
ReviewScopeResolver,
|
|
44
|
+
type RunClassification,
|
|
45
|
+
type ScopeProgress,
|
|
46
|
+
} from "./engine/review-scope";
|
|
42
47
|
import { ENV_NAMES } from "./env";
|
|
43
48
|
import type { BudgetSelection, ClassificationResult } from "./file-classifier";
|
|
44
49
|
import { buildStateFromFindings, fileReader } from "./findings-state";
|
|
@@ -101,7 +106,7 @@ export type ReviewOptions = {
|
|
|
101
106
|
noAst: boolean;
|
|
102
107
|
/** Força review completo (ignora o modo incremental). */
|
|
103
108
|
full: boolean;
|
|
104
|
-
/** Desativa as features de IA do fluxo normal
|
|
109
|
+
/** Desativa as features de IA do fluxo normal. */
|
|
105
110
|
noAi: boolean;
|
|
106
111
|
/** PR de origem (resolvida via `--pr`). */
|
|
107
112
|
pr?: number;
|
|
@@ -231,7 +236,7 @@ function writeManifest(
|
|
|
231
236
|
|
|
232
237
|
function writeBriefs(
|
|
233
238
|
activeIds: string[],
|
|
234
|
-
classification:
|
|
239
|
+
classification: RunClassification,
|
|
235
240
|
ctx: ReviewContext,
|
|
236
241
|
branch: string,
|
|
237
242
|
context: ContextSummary | null,
|
|
@@ -276,6 +281,7 @@ function writeBriefs(
|
|
|
276
281
|
candidates: candidates.filter((c) => c.watchdogId === id),
|
|
277
282
|
ast: astFiles,
|
|
278
283
|
aiPlan: aiPlans?.get(id),
|
|
284
|
+
changedScope: classification.changedScope,
|
|
279
285
|
});
|
|
280
286
|
const content = `${brief}\n`;
|
|
281
287
|
work.writeBrief(id, content);
|
|
@@ -322,7 +328,9 @@ async function resolveCandidates(
|
|
|
322
328
|
const def = findWatchdog(config, id);
|
|
323
329
|
if (!def) continue;
|
|
324
330
|
const inputs = files.slice(0, 5).flatMap((file) => {
|
|
325
|
-
|
|
331
|
+
// Só o trecho alterado: candidato fora do diff vira ruído de código
|
|
332
|
+
// pré-existente no brief do watchdog.
|
|
333
|
+
const snippet = diff.byFile.get(file);
|
|
326
334
|
return snippet ? [{ file, snippet }] : [];
|
|
327
335
|
});
|
|
328
336
|
candidates.push(
|
|
@@ -406,6 +414,16 @@ function discoveryMessages(
|
|
|
406
414
|
return messages;
|
|
407
415
|
}
|
|
408
416
|
|
|
417
|
+
/** Resumo do escopo — lembra que só as faixas alteradas são revisáveis. */
|
|
418
|
+
function changedScopeMessage(classification: RunClassification): string[] {
|
|
419
|
+
const scope = classification.changedScope;
|
|
420
|
+
if (!scope) return [];
|
|
421
|
+
const totals = summarizeScope(scope);
|
|
422
|
+
return [
|
|
423
|
+
`Escopo: ${totals.files} arquivo(s), ${totals.symbols} símbolo(s) em ${totals.ranges} faixa(s) alterada(s).`,
|
|
424
|
+
];
|
|
425
|
+
}
|
|
426
|
+
|
|
409
427
|
/** Resumo do orçamento — só quando algum arquivo ficou de fora (PR grande). */
|
|
410
428
|
function budgetMessage(
|
|
411
429
|
classification: ClassificationResult,
|
|
@@ -422,18 +440,85 @@ function budgetMessage(
|
|
|
422
440
|
];
|
|
423
441
|
}
|
|
424
442
|
|
|
425
|
-
/**
|
|
426
|
-
|
|
443
|
+
/** Resultado do prepare — mensagens para o CLI imprimir. */
|
|
444
|
+
export type PrepareResult = {
|
|
445
|
+
messages: string[];
|
|
446
|
+
/** Watchdogs ativos neste ciclo (vazio em no-op/sem arquivos). */
|
|
447
|
+
activeIds: string[];
|
|
448
|
+
/** Diretório dos briefs do ciclo (ausente quando não há briefs). */
|
|
449
|
+
briefsDir?: string;
|
|
450
|
+
/** Review do ciclo (ausente quando nada foi preparado). */
|
|
451
|
+
reviewId?: string;
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
/** Etapas do prepare reportadas ao CLI (o core não escreve saída). */
|
|
455
|
+
export type PrepareStage =
|
|
456
|
+
| "diff"
|
|
457
|
+
| "ast"
|
|
458
|
+
| "classify"
|
|
459
|
+
| "impact"
|
|
460
|
+
| "jev"
|
|
461
|
+
| "context"
|
|
462
|
+
| "technical"
|
|
463
|
+
| "ai-plans"
|
|
464
|
+
| "briefs"
|
|
465
|
+
| "manifest";
|
|
466
|
+
|
|
467
|
+
/** Evento de progresso de uma etapa do prepare. */
|
|
468
|
+
export type PrepareProgressEvent =
|
|
469
|
+
| { type: "start"; stage: PrepareStage }
|
|
470
|
+
| { type: "done"; stage: PrepareStage; durationMs: number };
|
|
471
|
+
|
|
472
|
+
/** Callback de progresso do prepare (falhas do callback são ignoradas). */
|
|
473
|
+
export type PrepareProgress = (event: PrepareProgressEvent) => void;
|
|
474
|
+
|
|
475
|
+
/** Encaminha o progresso do escopo como etapa do prepare. */
|
|
476
|
+
function scopeProgress(onProgress?: PrepareProgress): ScopeProgress | undefined {
|
|
477
|
+
if (!onProgress) return undefined;
|
|
478
|
+
return (event) => emitProgress(onProgress, event);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Emite um evento de progresso sem deixar o callback derrubar a revisão. */
|
|
482
|
+
function emitProgress(
|
|
483
|
+
onProgress: PrepareProgress | undefined,
|
|
484
|
+
event: PrepareProgressEvent,
|
|
485
|
+
): void {
|
|
427
486
|
try {
|
|
428
|
-
|
|
429
|
-
return readFileSync(path, "utf8").split("\n").slice(0, 80).join("\n");
|
|
487
|
+
onProgress?.(event);
|
|
430
488
|
} catch {
|
|
431
|
-
|
|
489
|
+
// Progresso nunca derruba a revisão.
|
|
432
490
|
}
|
|
433
491
|
}
|
|
434
492
|
|
|
435
|
-
/**
|
|
436
|
-
|
|
493
|
+
/** Executa uma etapa reportando início/fim (sync ou async). */
|
|
494
|
+
async function runStage<T>(
|
|
495
|
+
onProgress: PrepareProgress | undefined,
|
|
496
|
+
stage: PrepareStage,
|
|
497
|
+
run: () => Promise<T> | T,
|
|
498
|
+
): Promise<T> {
|
|
499
|
+
if (!onProgress) return run();
|
|
500
|
+
const started = Date.now();
|
|
501
|
+
emitProgress(onProgress, { type: "start", stage });
|
|
502
|
+
try {
|
|
503
|
+
return await run();
|
|
504
|
+
} finally {
|
|
505
|
+
emitProgress(onProgress, {
|
|
506
|
+
type: "done",
|
|
507
|
+
stage,
|
|
508
|
+
durationMs: Date.now() - started,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** Como `runStage`, mas só reporta quando a etapa realmente vai rodar. */
|
|
514
|
+
async function runOptionalStage<T>(
|
|
515
|
+
onProgress: PrepareProgress | undefined,
|
|
516
|
+
enabled: boolean,
|
|
517
|
+
stage: PrepareStage,
|
|
518
|
+
run: () => Promise<T> | T,
|
|
519
|
+
): Promise<T> {
|
|
520
|
+
return enabled ? runStage(onProgress, stage, run) : run();
|
|
521
|
+
}
|
|
437
522
|
|
|
438
523
|
/**
|
|
439
524
|
* Classificação fallback por IA (opcional): atribui arquivos não classificados
|
|
@@ -591,6 +676,7 @@ export async function prepare(
|
|
|
591
676
|
ctx: ReviewContext,
|
|
592
677
|
ast?: AstService,
|
|
593
678
|
ai?: AiClient,
|
|
679
|
+
onProgress?: PrepareProgress,
|
|
594
680
|
): Promise<PrepareResult> {
|
|
595
681
|
const messages: string[] = [];
|
|
596
682
|
ctx.work.ensure();
|
|
@@ -605,6 +691,7 @@ export async function prepare(
|
|
|
605
691
|
config,
|
|
606
692
|
opts.noAst ? undefined : ast,
|
|
607
693
|
ctx.git,
|
|
694
|
+
scopeProgress(onProgress),
|
|
608
695
|
).resolve(previous, headSha);
|
|
609
696
|
|
|
610
697
|
// Mesmo commit já revisado: preserva artefatos e triagem, sem re-revisar.
|
|
@@ -622,7 +709,7 @@ export async function prepare(
|
|
|
622
709
|
? "Nada novo a preparar — o ciclo em andamento continua; rode `--finalize` quando os findings estiverem gravados."
|
|
623
710
|
: "Nada a revisar — o relatório anterior foi mantido. Use `--full` para re-revisar.",
|
|
624
711
|
);
|
|
625
|
-
return { messages };
|
|
712
|
+
return { messages, activeIds: [] };
|
|
626
713
|
}
|
|
627
714
|
|
|
628
715
|
const { classification, diff } = scope;
|
|
@@ -639,7 +726,7 @@ export async function prepare(
|
|
|
639
726
|
messages.push(
|
|
640
727
|
`Nenhuma alteração entre ${opts.base}...${opts.head} — nenhuma run criada.`,
|
|
641
728
|
);
|
|
642
|
-
return { messages };
|
|
729
|
+
return { messages, activeIds: [] };
|
|
643
730
|
}
|
|
644
731
|
}
|
|
645
732
|
|
|
@@ -657,7 +744,10 @@ export async function prepare(
|
|
|
657
744
|
});
|
|
658
745
|
|
|
659
746
|
const astAnalyses = scope.ast;
|
|
660
|
-
const astImpact =
|
|
747
|
+
const astImpact =
|
|
748
|
+
astAnalyses && ast
|
|
749
|
+
? await runStage(onProgress, "impact", () => ast.impact(astAnalyses))
|
|
750
|
+
: undefined;
|
|
661
751
|
if (astAnalyses && ast?.enabled) {
|
|
662
752
|
const summary = {
|
|
663
753
|
enabled: true,
|
|
@@ -686,7 +776,13 @@ export async function prepare(
|
|
|
686
776
|
}
|
|
687
777
|
}
|
|
688
778
|
|
|
689
|
-
const
|
|
779
|
+
const jevWillRun = Boolean(
|
|
780
|
+
(ai && !opts.noAi && new AiPreAnalyzer(ai).enabled) ||
|
|
781
|
+
(!opts.noJev && new JevClient().isConfigured()),
|
|
782
|
+
);
|
|
783
|
+
const jev = await runOptionalStage(onProgress, jevWillRun, "jev", () =>
|
|
784
|
+
resolveCandidates(opts, classification, ctx, config, diff, ai),
|
|
785
|
+
);
|
|
690
786
|
|
|
691
787
|
const activeIds = Object.keys(classification.watchdogs);
|
|
692
788
|
if (Object.keys(classification.files).length === 0) {
|
|
@@ -696,14 +792,14 @@ export async function prepare(
|
|
|
696
792
|
? `Nenhuma alteração desde a última revisão (${shortSha(classification.reviewedFrom ?? "")}).`
|
|
697
793
|
: `Nenhuma alteração entre ${opts.base}...${opts.head}.`,
|
|
698
794
|
);
|
|
699
|
-
return { messages };
|
|
795
|
+
return { messages, activeIds: [] };
|
|
700
796
|
}
|
|
701
797
|
if (activeIds.length === 0) {
|
|
702
798
|
writeRunStatus(ctx.storage, ctx.review.id, { status: "review_done" });
|
|
703
799
|
messages.push(
|
|
704
800
|
"Nenhum arquivo revisável (tudo excluído, deletado ou irrelevante).",
|
|
705
801
|
);
|
|
706
|
-
return { messages };
|
|
802
|
+
return { messages, activeIds: [] };
|
|
707
803
|
}
|
|
708
804
|
if (classification.mode === "incremental") {
|
|
709
805
|
messages.push(
|
|
@@ -712,15 +808,21 @@ export async function prepare(
|
|
|
712
808
|
}
|
|
713
809
|
|
|
714
810
|
persistContext(ctx);
|
|
715
|
-
const
|
|
716
|
-
await
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
811
|
+
const aiContext = ai && !opts.noAi ? new AiContextSummarizer(ai).enabled : false;
|
|
812
|
+
const context = await runOptionalStage(onProgress, aiContext, "context", () =>
|
|
813
|
+
resolveContextSummary(ctx, ai, opts),
|
|
814
|
+
);
|
|
815
|
+
const aiTechnical = ai && !opts.noAi ? new AiTechnicalSummarizer(ai).enabled : false;
|
|
816
|
+
await runOptionalStage(onProgress, aiTechnical, "technical", () =>
|
|
817
|
+
resolveTechnicalSummary(ctx, ai, opts, {
|
|
818
|
+
branch,
|
|
819
|
+
classification,
|
|
820
|
+
context,
|
|
821
|
+
ast: astAnalyses,
|
|
822
|
+
impact: astImpact,
|
|
823
|
+
domains: activeIds.map((id) => findWatchdog(config, id)?.name ?? id),
|
|
824
|
+
}),
|
|
825
|
+
);
|
|
724
826
|
|
|
725
827
|
const manifestAst: ManifestAst | undefined = astAnalyses
|
|
726
828
|
? {
|
|
@@ -741,15 +843,18 @@ export async function prepare(
|
|
|
741
843
|
}
|
|
742
844
|
: undefined;
|
|
743
845
|
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
846
|
+
const aiBriefs = ai && !opts.noAi ? new AiBriefEnricher(ai).enabled : false;
|
|
847
|
+
const aiPlans = await runOptionalStage(onProgress, aiBriefs, "ai-plans", () =>
|
|
848
|
+
buildAiPlans(
|
|
849
|
+
activeIds,
|
|
850
|
+
classification,
|
|
851
|
+
config,
|
|
852
|
+
context,
|
|
853
|
+
astAnalyses,
|
|
854
|
+
astImpact,
|
|
855
|
+
ai,
|
|
856
|
+
opts,
|
|
857
|
+
),
|
|
753
858
|
);
|
|
754
859
|
if (aiPlans.size > 0) {
|
|
755
860
|
ctx.storage.writeArtifact(ctx.review.id, {
|
|
@@ -779,33 +884,37 @@ export async function prepare(
|
|
|
779
884
|
}
|
|
780
885
|
: undefined;
|
|
781
886
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
887
|
+
await runStage(onProgress, "briefs", () =>
|
|
888
|
+
writeBriefs(
|
|
889
|
+
activeIds,
|
|
890
|
+
classification,
|
|
891
|
+
ctx,
|
|
892
|
+
branch,
|
|
893
|
+
context,
|
|
894
|
+
jev.candidates,
|
|
895
|
+
config,
|
|
896
|
+
astAnalyses,
|
|
897
|
+
astImpact,
|
|
898
|
+
aiPlans,
|
|
899
|
+
),
|
|
793
900
|
);
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
901
|
+
await runStage(onProgress, "manifest", () =>
|
|
902
|
+
writeManifest(
|
|
903
|
+
ctx,
|
|
904
|
+
config,
|
|
905
|
+
opts,
|
|
906
|
+
classification,
|
|
907
|
+
activeIds,
|
|
908
|
+
activeIds,
|
|
909
|
+
jev,
|
|
910
|
+
manifestAst,
|
|
911
|
+
manifestAi,
|
|
912
|
+
),
|
|
804
913
|
);
|
|
805
|
-
|
|
806
914
|
messages.push(
|
|
807
915
|
...discoveryMessages(activeIds, classification, ctx.work.briefsDir),
|
|
808
916
|
...budgetMessage(classification, config),
|
|
917
|
+
...changedScopeMessage(classification),
|
|
809
918
|
...jevSummary(jev),
|
|
810
919
|
);
|
|
811
920
|
if (astAnalyses) {
|
|
@@ -841,7 +950,12 @@ export async function prepare(
|
|
|
841
950
|
"Quando os findings estiverem gravados: bun run houndsman --finalize",
|
|
842
951
|
);
|
|
843
952
|
|
|
844
|
-
return {
|
|
953
|
+
return {
|
|
954
|
+
messages,
|
|
955
|
+
activeIds,
|
|
956
|
+
briefsDir: ctx.work.briefsDir,
|
|
957
|
+
reviewId: ctx.review.id,
|
|
958
|
+
};
|
|
845
959
|
}
|
|
846
960
|
/** Persiste o contexto da PR (bruto + resumo) como artefatos, quando existirem. */
|
|
847
961
|
function persistContext(ctx: ReviewContext): void {
|
|
@@ -13,20 +13,22 @@ import type {
|
|
|
13
13
|
HoundsmanAiFeatures,
|
|
14
14
|
HoundsmanAst,
|
|
15
15
|
HoundsmanConfig,
|
|
16
|
+
HoundsmanHunt,
|
|
16
17
|
HoundsmanStorage,
|
|
17
18
|
HoundsmanWorktrees,
|
|
18
19
|
} from "./config";
|
|
19
20
|
import {
|
|
20
21
|
DEFAULT_AI_CONCURRENCY,
|
|
21
22
|
DEFAULT_AI_MAX_CALLS_PER_REVIEW,
|
|
22
|
-
DEFAULT_AI_MAX_OUTPUT_TOKENS,
|
|
23
|
-
DEFAULT_AI_MAX_STEPS,
|
|
24
23
|
DEFAULT_AI_MODEL,
|
|
25
24
|
DEFAULT_AI_PROVIDER,
|
|
26
25
|
DEFAULT_AI_TIMEOUT_MS,
|
|
27
26
|
DEFAULT_ALWAYS_INCLUDE_DOCS,
|
|
28
27
|
DEFAULT_AST_MAX_FILE_BYTES,
|
|
29
28
|
DEFAULT_AST_MAX_IMPACT_FILES,
|
|
29
|
+
DEFAULT_HUNT_CONCURRENCY,
|
|
30
|
+
DEFAULT_HUNT_RETRIES,
|
|
31
|
+
DEFAULT_HUNT_TIMEOUT_MS,
|
|
30
32
|
DEFAULT_JEV_CONCURRENCY,
|
|
31
33
|
DEFAULT_MAX_FILES_PER_BRIEF,
|
|
32
34
|
DEFAULT_WORKTREES_LOCATION,
|
|
@@ -126,19 +128,6 @@ export class ProjectConfig<TId extends string = string> {
|
|
|
126
128
|
return positive(this.ai.concurrency, DEFAULT_AI_CONCURRENCY);
|
|
127
129
|
}
|
|
128
130
|
|
|
129
|
-
get aiMaxSteps(): number {
|
|
130
|
-
return positive(this.ai.maxSteps, DEFAULT_AI_MAX_STEPS);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
get aiMaxOutputTokens(): number {
|
|
134
|
-
return positive(this.ai.maxOutputTokens, DEFAULT_AI_MAX_OUTPUT_TOKENS);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
get aiTemperature(): number {
|
|
138
|
-
const value = this.ai.temperature;
|
|
139
|
-
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
131
|
/** Opções do AI SDK por provider (undefined quando ausentes/vazias). */
|
|
143
132
|
get aiProviderOptions(): AiProviderOptions | undefined {
|
|
144
133
|
const value = this.ai.providerOptions;
|
|
@@ -178,17 +167,38 @@ export class ProjectConfig<TId extends string = string> {
|
|
|
178
167
|
return override && override.length > 0 ? override : this.aiModel;
|
|
179
168
|
}
|
|
180
169
|
|
|
181
|
-
/**
|
|
182
|
-
|
|
183
|
-
return
|
|
170
|
+
/** Config do harness local (objeto vazio quando ausente). */
|
|
171
|
+
get hunt(): HoundsmanHunt {
|
|
172
|
+
return this.raw.hunt ?? {};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Harness local habilitado (default: ligado; sem binário é fail-closed no comando). */
|
|
176
|
+
get huntEnabled(): boolean {
|
|
177
|
+
return this.hunt.enabled !== false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Processos maki simultâneos. */
|
|
181
|
+
get huntConcurrency(): number {
|
|
182
|
+
return positive(this.hunt.concurrency, DEFAULT_HUNT_CONCURRENCY);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Timeout por watchdog (ms). */
|
|
186
|
+
get huntTimeoutMs(): number {
|
|
187
|
+
return positive(this.hunt.timeoutMs, DEFAULT_HUNT_TIMEOUT_MS);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Retentativas extras (0 desliga; valores negativos caem no default). */
|
|
191
|
+
get huntRetries(): number {
|
|
192
|
+
const value = this.hunt.retries;
|
|
193
|
+
return value !== undefined && Number.isInteger(value) && value >= 0
|
|
194
|
+
? value
|
|
195
|
+
: DEFAULT_HUNT_RETRIES;
|
|
184
196
|
}
|
|
185
197
|
|
|
186
|
-
/**
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
this.aiMaxOutputTokens,
|
|
191
|
-
);
|
|
198
|
+
/** Modelo base do harness local (sem prefixo de provider). */
|
|
199
|
+
get huntModel(): string {
|
|
200
|
+
const value = this.hunt.model?.trim();
|
|
201
|
+
return value && value.length > 0 ? value : this.aiModel;
|
|
192
202
|
}
|
|
193
203
|
}
|
|
194
204
|
|
|
@@ -282,12 +282,7 @@ function checkAiConfig(
|
|
|
282
282
|
if (ai.enabled !== false && ai.model !== undefined && !ai.model.trim()) {
|
|
283
283
|
warnings.push("ai.model vazio — usando o default.");
|
|
284
284
|
}
|
|
285
|
-
for (const key of [
|
|
286
|
-
"concurrency",
|
|
287
|
-
"maxSteps",
|
|
288
|
-
"maxOutputTokens",
|
|
289
|
-
"timeoutMs",
|
|
290
|
-
] as const) {
|
|
285
|
+
for (const key of ["concurrency", "timeoutMs"] as const) {
|
|
291
286
|
const value = ai[key];
|
|
292
287
|
if (value !== undefined && value <= 0) {
|
|
293
288
|
warnings.push(`ai.${key} = ${value} — não-positivo; usando o default.`);
|
|
@@ -303,15 +298,10 @@ function checkAiConfig(
|
|
|
303
298
|
`ai.maxTokensPerReview = ${ai.maxTokensPerReview} — negativo; sem teto.`,
|
|
304
299
|
);
|
|
305
300
|
}
|
|
306
|
-
for (const
|
|
301
|
+
for (const id of Object.keys(ai.watchdogs ?? {})) {
|
|
307
302
|
if (!config.watchdogIds.includes(id)) {
|
|
308
303
|
warnings.push(`ai.watchdogs['${id}'] não corresponde a nenhum watchdog.`);
|
|
309
304
|
}
|
|
310
|
-
if (override.maxSteps !== undefined && override.maxSteps <= 0) {
|
|
311
|
-
warnings.push(
|
|
312
|
-
`ai.watchdogs['${id}'].maxSteps não-positivo — usando o default.`,
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
305
|
}
|
|
316
306
|
|
|
317
307
|
const provider = ai.provider ?? "deepseek";
|
package/src/core/rule-text.ts
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Texto de regra (biblioteca) — fonte única de polaridade
|
|
2
|
+
* Texto de regra (biblioteca) — fonte única dos prefixos de polaridade.
|
|
3
3
|
*
|
|
4
|
-
* `
|
|
5
|
-
*
|
|
6
|
-
* Também define o vocabulário de findings emergentes (lógica/domínio).
|
|
4
|
+
* `registry-check` usa a mesma lista para validar que a regra declara
|
|
5
|
+
* “Promover/Evitar” explicitamente.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
|
-
/** Intenção de uma regra. */
|
|
10
|
-
export type RuleIntent = "promote" | "avoid";
|
|
11
|
-
|
|
12
8
|
/** Palavras que abrem uma regra de “deve acontecer”. */
|
|
13
9
|
export const PROMOTE_PREFIXES = [
|
|
14
10
|
"promover",
|
|
@@ -29,12 +25,6 @@ export const AVOID_PREFIXES = [
|
|
|
29
25
|
"jamais",
|
|
30
26
|
] as const;
|
|
31
27
|
|
|
32
|
-
/** Conjunto normalizado (usado para remover polaridade das keywords). */
|
|
33
|
-
export const POLARITY_WORDS: ReadonlySet<string> = new Set([
|
|
34
|
-
...PROMOTE_PREFIXES,
|
|
35
|
-
...AVOID_PREFIXES,
|
|
36
|
-
]);
|
|
37
|
-
|
|
38
28
|
/** Escapa um literal para uso em regex. */
|
|
39
29
|
function escapeWord(word: string): string {
|
|
40
30
|
return word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -48,50 +38,8 @@ function patternFor(words: readonly string[]): RegExp {
|
|
|
48
38
|
const PROMOTE_PATTERN = patternFor(PROMOTE_PREFIXES);
|
|
49
39
|
const AVOID_PATTERN = patternFor(AVOID_PREFIXES);
|
|
50
40
|
|
|
51
|
-
/** Polaridade explícita; sem prefixo conhecido, assume `avoid`. */
|
|
52
|
-
export function polarityOf(text: string): RuleIntent {
|
|
53
|
-
const normalized = text.trim().toLowerCase();
|
|
54
|
-
if (PROMOTE_PATTERN.test(normalized)) return "promote";
|
|
55
|
-
if (AVOID_PATTERN.test(normalized)) return "avoid";
|
|
56
|
-
return "avoid";
|
|
57
|
-
}
|
|
58
|
-
|
|
59
41
|
/** `true` quando a regra declara polaridade explícita. */
|
|
60
42
|
export function hasExplicitPolarity(text: string): boolean {
|
|
61
43
|
const normalized = text.trim().toLowerCase();
|
|
62
44
|
return PROMOTE_PATTERN.test(normalized) || AVOID_PATTERN.test(normalized);
|
|
63
45
|
}
|
|
64
|
-
|
|
65
|
-
/** Prefixos reservados para findings emergentes (fora das regras explícitas). */
|
|
66
|
-
export const EMERGENT_PREFIXES = {
|
|
67
|
-
logic: "Lógica:",
|
|
68
|
-
domain: "Domínio:",
|
|
69
|
-
} as const;
|
|
70
|
-
|
|
71
|
-
/** Espécie de finding emergente. */
|
|
72
|
-
export type EmergentKind = keyof typeof EMERGENT_PREFIXES;
|
|
73
|
-
|
|
74
|
-
/** Espécie de um finding emergente (`null` quando não é emergente). */
|
|
75
|
-
export function emergentKind(rule: string): EmergentKind | null {
|
|
76
|
-
const normalized = rule.trim();
|
|
77
|
-
if (normalized.startsWith(EMERGENT_PREFIXES.logic)) return "logic";
|
|
78
|
-
if (normalized.startsWith(EMERGENT_PREFIXES.domain)) return "domain";
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** `true` quando a regra usa um prefixo emergente. */
|
|
83
|
-
export function isEmergentRule(rule: string): boolean {
|
|
84
|
-
return emergentKind(rule) !== null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Texto que sobra após o prefixo (vazio é inválido). */
|
|
88
|
-
export function emergentText(rule: string): string {
|
|
89
|
-
const kind = emergentKind(rule);
|
|
90
|
-
if (!kind) return "";
|
|
91
|
-
return rule.trim().slice(EMERGENT_PREFIXES[kind].length).trim();
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Monta o texto de um finding emergente. */
|
|
95
|
-
export function emergentRule(kind: EmergentKind, text: string): string {
|
|
96
|
-
return `${EMERGENT_PREFIXES[kind]} ${text.trim()}`;
|
|
97
|
-
}
|
|
@@ -138,6 +138,14 @@ export class GitRepository {
|
|
|
138
138
|
return parseNumstat(this.run(["diff", "--numstat", "-z", range]));
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Patch com contexto zero (`git diff -U0 range`), sem truncar por arquivo.
|
|
143
|
+
* É compacto e exato nas linhas — base do escopo do diff e do delta AST.
|
|
144
|
+
*/
|
|
145
|
+
patchZeroContext(range: string): Map<string, string> {
|
|
146
|
+
return parseDiffIndex(this.run(["diff", "-U0", range]));
|
|
147
|
+
}
|
|
148
|
+
|
|
141
149
|
/** Patch por arquivo de um intervalo, opcionalmente restrito a `paths`. */
|
|
142
150
|
diffPatch(
|
|
143
151
|
range: string,
|
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 { ChangedScope } from "./changed-scope";
|
|
11
12
|
import type { JevRuleSpec } from "./jev/jev-types";
|
|
12
13
|
|
|
13
14
|
/** Severidade de um achado. `blocking` bloqueia o merge. */
|
|
@@ -29,19 +30,6 @@ export const SEVERITY_EMOJI: Record<Severity, string> = {
|
|
|
29
30
|
nit: "🔎",
|
|
30
31
|
};
|
|
31
32
|
|
|
32
|
-
/**
|
|
33
|
-
* Sinais opcionais de uma regra: ajudam o `RuleAnalyzer` a transformar texto
|
|
34
|
-
* em checagens concretas (símbolos, imports, decorators, paths e tools).
|
|
35
|
-
*/
|
|
36
|
-
export type RuleHints = {
|
|
37
|
-
symbols?: string[];
|
|
38
|
-
imports?: string[];
|
|
39
|
-
decorators?: string[];
|
|
40
|
-
paths?: string[];
|
|
41
|
-
/** Nomes de tools sugeridos (ex.: `symbol_context`). */
|
|
42
|
-
checks?: string[];
|
|
43
|
-
};
|
|
44
|
-
|
|
45
33
|
/**
|
|
46
34
|
* Regra do watchdog: severidade + texto. O texto declara a polaridade de forma
|
|
47
35
|
* explícita — `Promover:` (algo que DEVE acontecer) ou `Evitar:`/`Não`/`Nunca`
|
|
@@ -54,7 +42,6 @@ export type Rule = {
|
|
|
54
42
|
severity: Severity;
|
|
55
43
|
text: string;
|
|
56
44
|
jev?: JevRuleSpec;
|
|
57
|
-
hints?: RuleHints;
|
|
58
45
|
};
|
|
59
46
|
|
|
60
47
|
/**
|
|
@@ -144,6 +131,8 @@ export type WatchdogRenderArgs = {
|
|
|
144
131
|
ast?: readonly AstFileBrief[];
|
|
145
132
|
/** Plano de revisão sugerido por IA (nunca substitui as regras). */
|
|
146
133
|
aiPlan?: BriefPlan;
|
|
134
|
+
/** Escopo do diff por arquivo (símbolos alterados; hunks como fallback). */
|
|
135
|
+
changedScope?: ChangedScope;
|
|
147
136
|
};
|
|
148
137
|
|
|
149
138
|
/** Definição completa de um watchdog — 1 por arquivo em `project/watchdogs/`. */
|