@payconductor/houndsman 0.13.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.
- package/README.md +6 -1
- package/package.json +1 -1
- package/src/cli/commands/compare-command.ts +112 -0
- package/src/cli/commands/context-command.ts +1 -0
- package/src/cli/commands/findings-command.ts +11 -1
- package/src/cli/commands/hunt-command.ts +20 -3
- package/src/cli/commands/review-command.ts +38 -0
- package/src/cli/commands/status-command.ts +3 -2
- package/src/cli/help.ts +3 -1
- package/src/cli/index.ts +3 -1
- package/src/cli/progress.ts +7 -2
- package/src/core/ai/ai-client.ts +38 -2
- package/src/core/ai/features/brief-enricher.ts +1 -1
- package/src/core/ai/features/business-analyzer.ts +61 -0
- package/src/core/ai/features/business-summarizer.ts +44 -0
- package/src/core/ai/index.ts +8 -0
- package/src/core/ai/prompts/brief.ts +33 -7
- package/src/core/ai/prompts/business.ts +76 -0
- package/src/core/ai/provider-capabilities.ts +12 -0
- package/src/core/ai/schemas.ts +70 -2
- package/src/core/ai/usage-recorder.ts +53 -1
- package/src/core/brief.ts +246 -12
- package/src/core/builtins.ts +108 -0
- package/src/core/business-summary.ts +583 -0
- package/src/core/commands/compare.ts +233 -0
- package/src/core/commands/context.ts +6 -2
- package/src/core/commands/findings.ts +38 -30
- package/src/core/commands/review.ts +6 -0
- package/src/core/commands/scaffold.ts +27 -0
- package/src/core/commands/status.ts +9 -4
- package/src/core/compare.ts +243 -0
- package/src/core/concurrency.ts +30 -0
- package/src/core/config.ts +31 -3
- package/src/core/defaults.ts +50 -4
- package/src/core/domain/budget.ts +62 -4
- package/src/core/domain/classification.ts +12 -2
- package/src/core/engine/carryover.ts +4 -0
- package/src/core/engine/finalize-pipeline.ts +347 -0
- package/src/core/engine/prepare-pipeline.ts +1294 -0
- package/src/core/engine/review-context.ts +51 -0
- package/src/core/engine/review-options.ts +47 -0
- package/src/core/engine/review-scope.ts +14 -1
- package/src/core/engine/review-session.ts +11 -13
- package/src/core/engine/run-artifacts.ts +196 -0
- package/src/core/engine/run-format.ts +4 -0
- package/src/core/findings-state.ts +13 -1
- package/src/core/github-review.ts +5 -2
- package/src/core/hunt/hunt-runner.ts +90 -0
- package/src/core/hunt/index.ts +2 -0
- package/src/core/hunt/maki-intent-prompt.ts +50 -0
- package/src/core/hunt/maki-prompt.ts +13 -1
- package/src/core/hunt/pool.ts +4 -26
- package/src/core/logging/index.ts +6 -1
- package/src/core/logging/progress.ts +85 -1
- package/src/core/orchestrator.ts +33 -1289
- package/src/core/paths.ts +6 -3
- package/src/core/pr-context.ts +2 -4
- package/src/core/prepare-report.ts +61 -0
- package/src/core/project-config.ts +8 -1
- package/src/core/propagation.ts +127 -0
- package/src/core/registry-check.ts +60 -21
- package/src/core/renderers/manifest-writer.ts +13 -0
- package/src/core/renderers/resume-renderer.ts +151 -11
- package/src/core/report-builder.ts +108 -278
- package/src/core/storage/file/file-storage.ts +7 -3
- package/src/core/storage/sqlite/mappers.ts +10 -0
- package/src/core/storage/sqlite/schema.ts +104 -23
- package/src/core/storage/sqlite/sqlite-storage.ts +22 -9
- package/src/core/storage/types.ts +16 -4
- package/src/core/storage/work-area.ts +59 -1
- package/src/core/types.ts +31 -0
- package/src/core/watchdog-registry.ts +21 -3
- package/src/index.ts +29 -4
- package/src/server/client/ArtifactsPanel.tsx +4 -2
- package/src/server/client/FindingCard.tsx +21 -1
- package/src/server/client/RunsSection.tsx +5 -0
- package/src/server/client/utils/constants.ts +21 -11
- package/src/server/client/utils/markdown.ts +1 -1
- package/src/server/client/utils/types.ts +14 -2
- package/src/server/run-repository.ts +35 -14
- package/src/server/styles.css +2 -0
- package/src/skill/OUTPUT-FORMAT.md +36 -10
- package/src/skill/README.md +18 -14
- package/src/skill/SETUP.md +1 -0
- package/src/skill/SKILL.md +13 -4
- package/src/skill/example-resume.md +43 -0
- package/src/skill/hunt.md +145 -58
- package/src/tests/__snapshots__/baseline.test.ts.snap +187 -178
- package/src/tests/ai-core.test.ts +46 -0
- package/src/tests/ai-features.test.ts +95 -4
- package/src/tests/baseline.test.ts +29 -121
- package/src/tests/brief-scope.test.ts +250 -0
- package/src/tests/business-summary.test.ts +258 -0
- package/src/tests/compare.test.ts +233 -0
- package/src/tests/fixtures/fake-model-provider.ts +2 -1
- package/src/tests/hunt-maki.test.ts +58 -0
- package/src/tests/markdown.test.ts +3 -3
- package/src/tests/progress.test.ts +29 -1
- package/src/tests/propagation.test.ts +119 -0
- package/src/tests/review-resume.test.ts +60 -1
- package/src/tests/review-scope.test.ts +38 -29
- package/src/tests/review-scripts.test.ts +106 -218
- package/src/tests/server.test.ts +7 -4
- package/src/tests/storage.test.ts +194 -9
- package/src/core/renderers/report-renderer.ts +0 -66
- package/src/skill/example-review.md +0 -66
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contexto de uma run (engine) — storage + review + staging + raiz operacional.
|
|
3
|
+
*
|
|
4
|
+
* É criado uma vez por `ReviewSession` e injetado nas pipelines. Não existe
|
|
5
|
+
* contexto ambiente: duas runs concorrentes no mesmo processo mantêm contextos
|
|
6
|
+
* isolados (um singleton mutável trocaria root/git/storage entre `await`s).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { GitRepository } from "../services/git-repository";
|
|
10
|
+
import { ProjectFiles } from "../services/project-files";
|
|
11
|
+
import type { ReviewRecord, StorageProvider } from "../storage/types";
|
|
12
|
+
import { ReviewWorkArea } from "../storage/work-area";
|
|
13
|
+
|
|
14
|
+
/** Workspace onde os arquivos da review são lidos. */
|
|
15
|
+
export type ReviewWorkspaceContext = {
|
|
16
|
+
/** Raiz operacional (worktree ou clone principal). */
|
|
17
|
+
root?: string;
|
|
18
|
+
/** Raiz do clone principal (identidade/config). */
|
|
19
|
+
mainRoot?: string;
|
|
20
|
+
git?: GitRepository;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Contexto de uma run: storage + review + staging + raiz operacional. */
|
|
24
|
+
export type ReviewContext = {
|
|
25
|
+
storage: StorageProvider;
|
|
26
|
+
review: ReviewRecord;
|
|
27
|
+
work: ReviewWorkArea;
|
|
28
|
+
root: string;
|
|
29
|
+
mainRoot: string;
|
|
30
|
+
git: GitRepository;
|
|
31
|
+
/** Leitor relativo à raiz operacional (evita repetir `fileReader(root)`). */
|
|
32
|
+
files: ProjectFiles;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Cria o contexto de uma review existente. */
|
|
36
|
+
export function reviewContext(
|
|
37
|
+
storage: StorageProvider,
|
|
38
|
+
review: ReviewRecord,
|
|
39
|
+
workspace: ReviewWorkspaceContext = {},
|
|
40
|
+
): ReviewContext {
|
|
41
|
+
const root = workspace.root ?? process.cwd();
|
|
42
|
+
return {
|
|
43
|
+
storage,
|
|
44
|
+
review,
|
|
45
|
+
work: new ReviewWorkArea(review),
|
|
46
|
+
root,
|
|
47
|
+
mainRoot: workspace.mainRoot ?? root,
|
|
48
|
+
git: workspace.git ?? new GitRepository(),
|
|
49
|
+
files: new ProjectFiles(root),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opções de uma run (flags do CLI já resolvidas).
|
|
3
|
+
*
|
|
4
|
+
* Fica fora do `orchestrator` para as pipelines importarem o tipo sem ciclo.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type ReviewOptions = {
|
|
8
|
+
base: string;
|
|
9
|
+
head: string;
|
|
10
|
+
branch?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Rótulo da variante A/B. Cada variante vira uma run própria
|
|
13
|
+
* (`repoKey`, `branch`, `variant`) — permite comparar sem apagar a anterior.
|
|
14
|
+
*/
|
|
15
|
+
variant?: string;
|
|
16
|
+
finalize: boolean;
|
|
17
|
+
failOnBlocking: boolean;
|
|
18
|
+
clean: boolean;
|
|
19
|
+
noJev: boolean;
|
|
20
|
+
/** Desativa a análise estrutural (AST) nesta run. */
|
|
21
|
+
noAst: boolean;
|
|
22
|
+
/** Força review completo (ignora o modo incremental). */
|
|
23
|
+
full: boolean;
|
|
24
|
+
/** Desativa as features de IA do fluxo normal. */
|
|
25
|
+
noAi: boolean;
|
|
26
|
+
/** PR de origem (resolvida via `--pr`). */
|
|
27
|
+
pr?: number;
|
|
28
|
+
/** Worktree informado via flag (`--worktree`). */
|
|
29
|
+
worktree?: string;
|
|
30
|
+
/** Worktree resolvido (absoluto; ausente = clone principal). */
|
|
31
|
+
worktreePath?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Valores default das opções (o CLI sobrepõe com as flags). */
|
|
35
|
+
export function defaultOptions(): ReviewOptions {
|
|
36
|
+
return {
|
|
37
|
+
base: "main",
|
|
38
|
+
head: "HEAD",
|
|
39
|
+
finalize: false,
|
|
40
|
+
failOnBlocking: false,
|
|
41
|
+
clean: false,
|
|
42
|
+
noJev: false,
|
|
43
|
+
noAst: false,
|
|
44
|
+
full: false,
|
|
45
|
+
noAi: false,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -52,6 +52,11 @@ export type ReviewScope =
|
|
|
52
52
|
kind: "review";
|
|
53
53
|
classification: RunClassification;
|
|
54
54
|
diff: DiffSource;
|
|
55
|
+
/**
|
|
56
|
+
* Patch `-U0` sem truncar (path → hunks). É o insumo dos briefs: cada
|
|
57
|
+
* watchdog recebe os trechos alterados em vez de reler arquivos inteiros.
|
|
58
|
+
*/
|
|
59
|
+
patch: Map<string, string>;
|
|
55
60
|
reviewedFrom?: string;
|
|
56
61
|
/** Fatos/delta por arquivo alterado (quando o AST está habilitado). */
|
|
57
62
|
ast?: Map<string, AstAnalysis>;
|
|
@@ -144,6 +149,7 @@ export class ReviewScopeResolver {
|
|
|
144
149
|
return {
|
|
145
150
|
kind: "review",
|
|
146
151
|
diff,
|
|
152
|
+
patch: scopeDiff.byFile,
|
|
147
153
|
ast,
|
|
148
154
|
classification: {
|
|
149
155
|
...result,
|
|
@@ -192,7 +198,14 @@ export class ReviewScopeResolver {
|
|
|
192
198
|
ast,
|
|
193
199
|
}),
|
|
194
200
|
};
|
|
195
|
-
return {
|
|
201
|
+
return {
|
|
202
|
+
kind: "review",
|
|
203
|
+
classification,
|
|
204
|
+
diff,
|
|
205
|
+
patch: scopeDiff.byFile,
|
|
206
|
+
reviewedFrom: prevSha,
|
|
207
|
+
ast,
|
|
208
|
+
};
|
|
196
209
|
}
|
|
197
210
|
|
|
198
211
|
/**
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
resolveBranch,
|
|
21
21
|
reviewContext,
|
|
22
22
|
} from "../orchestrator";
|
|
23
|
-
import {
|
|
23
|
+
import { reviewSlug } from "../report-builder";
|
|
24
24
|
import { GitRepository } from "../services/git-repository";
|
|
25
25
|
import type { RepoIdentity, StorageProvider } from "../storage/types";
|
|
26
26
|
|
|
@@ -48,12 +48,14 @@ export class ReviewSession {
|
|
|
48
48
|
this.branch = branch;
|
|
49
49
|
this.ast = ast;
|
|
50
50
|
this.ai = ai;
|
|
51
|
+
const variant = options.variant?.trim() || undefined;
|
|
51
52
|
const review = options.finalize
|
|
52
|
-
? storage.findReview(repo.key, branch)
|
|
53
|
+
? storage.findReview(repo.key, branch, variant)
|
|
53
54
|
: storage.getOrCreateReview({
|
|
54
55
|
repo,
|
|
55
56
|
branch,
|
|
56
|
-
|
|
57
|
+
variant,
|
|
58
|
+
slug: reviewSlug(branch, variant),
|
|
57
59
|
base: options.base,
|
|
58
60
|
head: options.head,
|
|
59
61
|
worktreePath: options.worktreePath,
|
|
@@ -61,7 +63,7 @@ export class ReviewSession {
|
|
|
61
63
|
});
|
|
62
64
|
if (!review) {
|
|
63
65
|
throw new Error(
|
|
64
|
-
`Nenhuma revisão para '${branch}'. Rode \`houndsman\` (preparar) primeiro.`,
|
|
66
|
+
`Nenhuma revisão para '${branch}'${variant ? ` (variante '${variant}')` : ""}. Rode \`houndsman\` (preparar) primeiro.`,
|
|
65
67
|
);
|
|
66
68
|
}
|
|
67
69
|
// Um prepare no clone principal invalida um worktree usado antes; o finalize
|
|
@@ -106,20 +108,16 @@ export class ReviewSession {
|
|
|
106
108
|
|
|
107
109
|
/** Passo 1 — preparar. */
|
|
108
110
|
prepare(onProgress?: PrepareProgress): Promise<PrepareResult> {
|
|
109
|
-
return prepare(
|
|
110
|
-
this.
|
|
111
|
-
this.
|
|
112
|
-
this.config,
|
|
113
|
-
this.context,
|
|
114
|
-
this.ast,
|
|
115
|
-
this.ai,
|
|
111
|
+
return prepare(this.context, this.config, this.options, {
|
|
112
|
+
ast: this.ast,
|
|
113
|
+
ai: this.ai,
|
|
116
114
|
onProgress,
|
|
117
|
-
);
|
|
115
|
+
});
|
|
118
116
|
}
|
|
119
117
|
|
|
120
118
|
/** Passo 3 — finalizar. */
|
|
121
119
|
finalize(): FinalizeResult {
|
|
122
|
-
return finalize(this.
|
|
120
|
+
return finalize(this.context, this.config, this.options);
|
|
123
121
|
}
|
|
124
122
|
|
|
125
123
|
/** Executa o passo escolhido em `--finalize`. */
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Artefatos persistidos de uma run (engine) — leitura/escrita tipada.
|
|
3
|
+
*
|
|
4
|
+
* Concentra `classification`, `candidates`, `manifest`, resumos e contexto para
|
|
5
|
+
* que as pipelines não repitam `storage.readArtifact(...)`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AstAnalysis, AstImpact } from "../ast/types";
|
|
9
|
+
import { GENERAL_WATCHDOG_ID } from "../builtins";
|
|
10
|
+
import { type BusinessSummary, parseBusinessSummary } from "../business-summary";
|
|
11
|
+
import type { HoundsmanConfig } from "../config";
|
|
12
|
+
import { type JevCandidatesFile, JevCandidatesStore, parseCandidates } from "../jev";
|
|
13
|
+
import {
|
|
14
|
+
BUSINESS_BRIEF_FILE,
|
|
15
|
+
BUSINESS_SUMMARY_FILE,
|
|
16
|
+
TECHNICAL_SUMMARY_FILE,
|
|
17
|
+
} from "../paths";
|
|
18
|
+
import type {
|
|
19
|
+
ManifestAi,
|
|
20
|
+
ManifestAst,
|
|
21
|
+
ManifestBusiness,
|
|
22
|
+
} from "../renderers/manifest-writer";
|
|
23
|
+
import { type ContextSummary, parseContextSummary } from "../report-builder";
|
|
24
|
+
import type { ReviewRecord, StorageProvider } from "../storage/types";
|
|
25
|
+
import type { ReviewWorkArea } from "../storage/work-area";
|
|
26
|
+
import {
|
|
27
|
+
buildTechnicalSummary,
|
|
28
|
+
parseTechnicalSummary,
|
|
29
|
+
type TechnicalSummary,
|
|
30
|
+
} from "../technical-summary";
|
|
31
|
+
import type { RunClassification } from "./review-scope";
|
|
32
|
+
|
|
33
|
+
/** Acesso tipado aos artefatos de uma review. */
|
|
34
|
+
export class RunArtifacts {
|
|
35
|
+
constructor(
|
|
36
|
+
private readonly storage: StorageProvider,
|
|
37
|
+
private readonly review: ReviewRecord,
|
|
38
|
+
private readonly work: ReviewWorkArea,
|
|
39
|
+
) {}
|
|
40
|
+
|
|
41
|
+
/** JSON persistido, ou `null` quando ausente/inválido. */
|
|
42
|
+
json<T>(name: string): T | null {
|
|
43
|
+
const artifact = this.storage.readArtifact(this.review.id, name);
|
|
44
|
+
if (!artifact) return null;
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(artifact.content) as T;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Reaproveita `ast`/`ai`/`business` do manifesto anterior. */
|
|
53
|
+
manifestExtras(): {
|
|
54
|
+
ast?: ManifestAst;
|
|
55
|
+
ai?: ManifestAi;
|
|
56
|
+
business?: ManifestBusiness;
|
|
57
|
+
} {
|
|
58
|
+
const artifact = this.storage.readArtifact(this.review.id, "manifest.json");
|
|
59
|
+
if (!artifact) return {};
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(artifact.content) as {
|
|
62
|
+
ast?: ManifestAst;
|
|
63
|
+
ai?: ManifestAi;
|
|
64
|
+
business?: ManifestBusiness;
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
ast: parsed.ast,
|
|
68
|
+
ai: parsed.ai,
|
|
69
|
+
business: parsed.business ?? undefined,
|
|
70
|
+
};
|
|
71
|
+
} catch {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Lê o `classification.json` persistido pelo prepare (lança se ausente). */
|
|
77
|
+
classification(): RunClassification {
|
|
78
|
+
const artifact = this.storage.readArtifact(this.review.id, "classification.json");
|
|
79
|
+
if (!artifact) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Não encontrei a classificação da review. Rode \`bun run houndsman\` (preparar) primeiro.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return JSON.parse(artifact.content) as RunClassification;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Lê os candidatos Jev persistidos (unavailable se ausente/inválido). */
|
|
88
|
+
candidates(config: HoundsmanConfig): JevCandidatesFile {
|
|
89
|
+
const artifact = this.storage.readArtifact(this.review.id, "candidates.json");
|
|
90
|
+
if (!artifact) return JevCandidatesStore.unavailable("candidates.json ausente");
|
|
91
|
+
try {
|
|
92
|
+
return (
|
|
93
|
+
parseCandidates(JSON.parse(artifact.content), [
|
|
94
|
+
...config.watchdogIds,
|
|
95
|
+
GENERAL_WATCHDOG_ID,
|
|
96
|
+
]) ?? JevCandidatesStore.unavailable("candidates.json inválido")
|
|
97
|
+
);
|
|
98
|
+
} catch {
|
|
99
|
+
return JevCandidatesStore.unavailable("candidates.json inválido");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Resumo do contexto persistido (usado quando o staging já foi limpo). */
|
|
104
|
+
contextSummary(): ContextSummary | null {
|
|
105
|
+
const artifact = this.storage.readArtifact(
|
|
106
|
+
this.review.id,
|
|
107
|
+
"context-summary.json",
|
|
108
|
+
);
|
|
109
|
+
if (!artifact) return null;
|
|
110
|
+
try {
|
|
111
|
+
return parseContextSummary(JSON.parse(artifact.content));
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Modelo de intenção persistido pelo prepare/revisor; `null` quando ausente. */
|
|
118
|
+
businessSummary(): BusinessSummary | null {
|
|
119
|
+
const raw = this.json<unknown>(BUSINESS_SUMMARY_FILE);
|
|
120
|
+
return raw ? parseBusinessSummary(raw) : null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Pontos técnicos persistidos pelo `prepare`; `null` quando ausentes. */
|
|
124
|
+
technicalSummary(): TechnicalSummary | null {
|
|
125
|
+
const raw = this.json<unknown>(TECHNICAL_SUMMARY_FILE);
|
|
126
|
+
return raw ? parseTechnicalSummary(raw) : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Fallback para runs antigas: reconstrói os destaques dos artefatos de AST. */
|
|
130
|
+
technicalFallback(classification: RunClassification): TechnicalSummary {
|
|
131
|
+
const ast = this.json<{ files?: Record<string, AstAnalysis> }>(
|
|
132
|
+
"ast-summary.json",
|
|
133
|
+
);
|
|
134
|
+
return buildTechnicalSummary({
|
|
135
|
+
classification,
|
|
136
|
+
ast: ast?.files ? new Map(Object.entries(ast.files)) : undefined,
|
|
137
|
+
impact: this.json<AstImpact>("ast-impact.json") ?? undefined,
|
|
138
|
+
headSha: classification.headSha,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Persiste o contexto da PR (bruto + resumo) como artefatos, quando existirem. */
|
|
143
|
+
persistContext(): void {
|
|
144
|
+
const raw = this.work.readContextRaw();
|
|
145
|
+
if (raw !== null) {
|
|
146
|
+
this.storage.writeArtifact(this.review.id, {
|
|
147
|
+
name: "context.json",
|
|
148
|
+
kind: "context",
|
|
149
|
+
content: raw,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const summary = this.work.readContextSummaryRaw();
|
|
153
|
+
if (summary !== null) {
|
|
154
|
+
this.storage.writeArtifact(this.review.id, {
|
|
155
|
+
name: "context-summary.json",
|
|
156
|
+
kind: "context-summary",
|
|
157
|
+
content: summary,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
const brief = this.work.readContextBriefRaw();
|
|
161
|
+
if (brief !== null) {
|
|
162
|
+
this.storage.writeArtifact(this.review.id, {
|
|
163
|
+
name: "context-brief.md",
|
|
164
|
+
kind: "context-brief",
|
|
165
|
+
content: brief,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Persiste o `business-summary.json` final: o revisor `general` pode ter
|
|
172
|
+
* reescrito o modelo no staging durante a revisão.
|
|
173
|
+
*/
|
|
174
|
+
persistBusiness(): BusinessSummary | null {
|
|
175
|
+
const raw = this.work.readBusinessSummaryRaw();
|
|
176
|
+
if (raw === null) return this.businessSummary();
|
|
177
|
+
this.storage.writeArtifact(this.review.id, {
|
|
178
|
+
name: BUSINESS_SUMMARY_FILE,
|
|
179
|
+
kind: "business",
|
|
180
|
+
content: raw.endsWith("\n") ? raw : `${raw}\n`,
|
|
181
|
+
});
|
|
182
|
+
const brief = this.work.readBusinessBriefRaw();
|
|
183
|
+
if (brief !== null) {
|
|
184
|
+
this.storage.writeArtifact(this.review.id, {
|
|
185
|
+
name: BUSINESS_BRIEF_FILE,
|
|
186
|
+
kind: "business-brief",
|
|
187
|
+
content: brief.endsWith("\n") ? brief : `${brief}\n`,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
return parseBusinessSummary(JSON.parse(raw), "reviewer");
|
|
192
|
+
} catch {
|
|
193
|
+
return this.businessSummary();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
locateAnchor,
|
|
19
19
|
reanchor,
|
|
20
20
|
} from "./finding-anchor";
|
|
21
|
-
import { type MergedFinding, sanitizeBranch } from "./report-builder";
|
|
21
|
+
import { type FindingBasis, type MergedFinding, sanitizeBranch } from "./report-builder";
|
|
22
22
|
import { systemClock } from "./services/clock";
|
|
23
23
|
import { type FileReader, ProjectFiles } from "./services/project-files";
|
|
24
24
|
import { SEVERITY_ORDER, type Severity, severityRank } from "./types";
|
|
@@ -101,6 +101,14 @@ export type FindingState = {
|
|
|
101
101
|
message: string;
|
|
102
102
|
action: string;
|
|
103
103
|
evidence?: string;
|
|
104
|
+
/** Gatilho concreto do gap (fora de regra). */
|
|
105
|
+
scenario?: string;
|
|
106
|
+
/** Consequência observável do gap. */
|
|
107
|
+
impact?: string;
|
|
108
|
+
/** Confiança do revisor (0–1). */
|
|
109
|
+
confidence?: number;
|
|
110
|
+
/** Origem do achado: regra, lógica ou domínio. */
|
|
111
|
+
basis?: FindingBasis;
|
|
104
112
|
anchor: FindingAnchor | null;
|
|
105
113
|
/** Commit em que o achado foi observado (base do diff focado do verify). */
|
|
106
114
|
foundAtSha?: string;
|
|
@@ -370,6 +378,10 @@ export function buildStateFromFindings(
|
|
|
370
378
|
message: finding.message,
|
|
371
379
|
action: finding.action,
|
|
372
380
|
evidence: finding.evidence,
|
|
381
|
+
scenario: finding.scenario,
|
|
382
|
+
impact: finding.impact,
|
|
383
|
+
confidence: finding.confidence,
|
|
384
|
+
basis: finding.basis,
|
|
373
385
|
anchor,
|
|
374
386
|
// Carry-over preserva o `foundAtSha` original; novos/re-reportados usam o head atual.
|
|
375
387
|
foundAtSha: finding.foundAtSha ?? options.headSha ?? prev?.foundAtSha,
|
|
@@ -225,9 +225,10 @@ export function buildCommentBody(finding: FindingState): string {
|
|
|
225
225
|
`${SEVERITY_EMOJI[finding.severity]} **${finding.rule}** · _${finding.severity}_`,
|
|
226
226
|
"",
|
|
227
227
|
finding.message,
|
|
228
|
-
"",
|
|
229
|
-
`**Ação:** ${finding.action}`,
|
|
230
228
|
];
|
|
229
|
+
if (finding.scenario) lines.push("", `**Cenário:** ${finding.scenario}`);
|
|
230
|
+
if (finding.impact) lines.push("", `**Impacto:** ${finding.impact}`);
|
|
231
|
+
lines.push("", `**Ação:** ${finding.action}`);
|
|
231
232
|
if (finding.evidence) lines.push("", "```", finding.evidence, "```");
|
|
232
233
|
lines.push("", "_— houndsman_");
|
|
233
234
|
return lines.join("\n");
|
|
@@ -247,6 +248,8 @@ export function renderGeneralBody(findings: readonly FindingState[]): string {
|
|
|
247
248
|
`- ${SEVERITY_EMOJI[finding.severity]} **${finding.severity}** \`${location}\` — ${finding.rule}`,
|
|
248
249
|
` ${finding.message}`,
|
|
249
250
|
);
|
|
251
|
+
if (finding.scenario) lines.push(` **Cenário:** ${finding.scenario}`);
|
|
252
|
+
if (finding.impact) lines.push(` **Impacto:** ${finding.impact}`);
|
|
250
253
|
}
|
|
251
254
|
return lines.join("\n");
|
|
252
255
|
}
|
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
11
|
+
import { GENERAL_WATCHDOG_ID } from "../builtins";
|
|
12
|
+
import { type BusinessSummary, parseBusinessSummary } from "../business-summary";
|
|
11
13
|
import { parseWatchdogReport } from "../report-builder";
|
|
12
14
|
import { systemClock } from "../services/clock";
|
|
13
15
|
import type { ReviewWorkArea } from "../storage/work-area";
|
|
16
|
+
import { renderHuntIntentPrompt, renderIntentRetrySuffix } from "./maki-intent-prompt";
|
|
14
17
|
import { renderHuntPrompt, renderRetrySuffix } from "./maki-prompt";
|
|
15
18
|
import {
|
|
16
19
|
extractJsonObject,
|
|
@@ -22,6 +25,12 @@ import { mapPool } from "./pool";
|
|
|
22
25
|
|
|
23
26
|
/** Evento de progresso (CLI loga; core não escreve saída). */
|
|
24
27
|
export type HuntEvent =
|
|
28
|
+
| {
|
|
29
|
+
type: "intent";
|
|
30
|
+
status: "start" | "done" | "skipped" | "failed";
|
|
31
|
+
source?: BusinessSummary["source"];
|
|
32
|
+
error?: string;
|
|
33
|
+
}
|
|
25
34
|
| { type: "start"; watchdogId: string }
|
|
26
35
|
| {
|
|
27
36
|
type: "retry";
|
|
@@ -78,6 +87,8 @@ export type HuntReport = {
|
|
|
78
87
|
version: 1;
|
|
79
88
|
engine: "maki";
|
|
80
89
|
model: string;
|
|
90
|
+
/** Fonte do modelo de intenção usado na revisão. */
|
|
91
|
+
business?: BusinessSummary["source"];
|
|
81
92
|
startedAt: string;
|
|
82
93
|
finishedAt: string;
|
|
83
94
|
durationMs: number;
|
|
@@ -114,6 +125,9 @@ export class HuntRunner {
|
|
|
114
125
|
async run(activeIds: readonly string[]): Promise<HuntReport> {
|
|
115
126
|
const startedAt = systemClock();
|
|
116
127
|
const startedMs = Date.now();
|
|
128
|
+
// Intenção primeiro: o revisor `general` e os demais watchdogs leem o
|
|
129
|
+
// mesmo `business-summary.json` do staging.
|
|
130
|
+
const business = await this.runIntent();
|
|
117
131
|
const watchdogs = await mapPool(activeIds, this.options.concurrency, (id) =>
|
|
118
132
|
this.runOne(id),
|
|
119
133
|
);
|
|
@@ -122,6 +136,7 @@ export class HuntRunner {
|
|
|
122
136
|
version: 1,
|
|
123
137
|
engine: "maki",
|
|
124
138
|
model: this.options.model,
|
|
139
|
+
business,
|
|
125
140
|
startedAt,
|
|
126
141
|
finishedAt,
|
|
127
142
|
durationMs: Date.now() - startedMs,
|
|
@@ -130,6 +145,69 @@ export class HuntRunner {
|
|
|
130
145
|
};
|
|
131
146
|
}
|
|
132
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Pré-passe de intenção (IA embutida). Pula quando o prepare já gerou o
|
|
150
|
+
* modelo com IA; falha mantém o determinístico (fail-open).
|
|
151
|
+
*/
|
|
152
|
+
private async runIntent(): Promise<BusinessSummary["source"] | undefined> {
|
|
153
|
+
const existing = this.options.work.readBusinessSummary();
|
|
154
|
+
if (existing?.source === "ai") {
|
|
155
|
+
this.emit({ type: "intent", status: "skipped", source: "ai" });
|
|
156
|
+
return "ai";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let brief: string;
|
|
160
|
+
try {
|
|
161
|
+
brief = readFileSync(this.options.work.businessPaths.brief, "utf8");
|
|
162
|
+
} catch {
|
|
163
|
+
return existing?.source;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
this.emit({ type: "intent", status: "start" });
|
|
167
|
+
const basePrompt = renderHuntIntentPrompt({
|
|
168
|
+
projectName: this.options.projectName,
|
|
169
|
+
brief,
|
|
170
|
+
});
|
|
171
|
+
let lastError = "";
|
|
172
|
+
for (let attempt = 0; attempt <= this.options.retries; attempt++) {
|
|
173
|
+
const prompt =
|
|
174
|
+
attempt === 0
|
|
175
|
+
? basePrompt
|
|
176
|
+
: `${basePrompt}\n${renderIntentRetrySuffix(lastError)}`;
|
|
177
|
+
const result = await this.options.runner.run({
|
|
178
|
+
prompt,
|
|
179
|
+
cwd: this.options.root,
|
|
180
|
+
model: this.options.model,
|
|
181
|
+
apiKeyEnv: this.options.apiKeyEnv,
|
|
182
|
+
apiKey: this.options.apiKey,
|
|
183
|
+
timeoutMs: this.options.timeoutMs,
|
|
184
|
+
});
|
|
185
|
+
if (!result.ok) {
|
|
186
|
+
lastError = result.error;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const raw = extractJsonObject(result.text);
|
|
190
|
+
if (raw === null) {
|
|
191
|
+
lastError = "a resposta final não contém um objeto JSON.";
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const summary = parseBusinessSummary(
|
|
195
|
+
{ ...raw, source: "reviewer" },
|
|
196
|
+
"reviewer",
|
|
197
|
+
);
|
|
198
|
+
if (!summary) {
|
|
199
|
+
lastError = "JSON fora do schema do modelo de intenção.";
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
this.options.work.writeBusinessSummary(summary);
|
|
203
|
+
this.emit({ type: "intent", status: "done", source: "reviewer" });
|
|
204
|
+
return "reviewer";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this.emit({ type: "intent", status: "failed", error: lastError });
|
|
208
|
+
return existing?.source ?? "deterministic";
|
|
209
|
+
}
|
|
210
|
+
|
|
133
211
|
private async runOne(watchdogId: string): Promise<HuntWatchdogResult> {
|
|
134
212
|
const briefPath = this.options.work.briefFile(watchdogId);
|
|
135
213
|
let brief: string;
|
|
@@ -202,6 +280,18 @@ export class HuntRunner {
|
|
|
202
280
|
continue;
|
|
203
281
|
}
|
|
204
282
|
|
|
283
|
+
// O revisor de fluxo devolve o modelo de intenção no JSON (maki não escreve).
|
|
284
|
+
if (watchdogId === GENERAL_WATCHDOG_ID) {
|
|
285
|
+
const summary = (raw as Record<string, unknown>).businessSummary;
|
|
286
|
+
if (typeof summary === "object" && summary !== null) {
|
|
287
|
+
const parsed = parseBusinessSummary(
|
|
288
|
+
{ ...(summary as Record<string, unknown>), source: "reviewer" },
|
|
289
|
+
"reviewer",
|
|
290
|
+
);
|
|
291
|
+
if (parsed) this.options.work.writeBusinessSummary(parsed);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
205
295
|
this.options.work.writeFinding(
|
|
206
296
|
watchdogId,
|
|
207
297
|
`${JSON.stringify(report, null, 2)}\n`,
|
package/src/core/hunt/index.ts
CHANGED
|
@@ -32,6 +32,8 @@ export {
|
|
|
32
32
|
resolveMakiBinary,
|
|
33
33
|
uninstallMaki,
|
|
34
34
|
} from "./maki-install";
|
|
35
|
+
export type { HuntIntentPromptInput } from "./maki-intent-prompt";
|
|
36
|
+
export { renderHuntIntentPrompt, renderIntentRetrySuffix } from "./maki-intent-prompt";
|
|
35
37
|
export type { MakiModel } from "./maki-model";
|
|
36
38
|
export {
|
|
37
39
|
houndsmanKeyEnvFor,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt do pré-passe de intenção no harness local (`houndsman hunt`).
|
|
3
|
+
*
|
|
4
|
+
* Executado antes dos watchdogs, com a IA embutida (maki). O output é o mesmo
|
|
5
|
+
* `business-summary.json` que a skill grava via subagente.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { BUSINESS_SUMMARY_SCHEMA } from "../business-summary";
|
|
9
|
+
|
|
10
|
+
/** Dados do prompt de intenção. */
|
|
11
|
+
export type HuntIntentPromptInput = {
|
|
12
|
+
projectName: string;
|
|
13
|
+
/** Conteúdo de `business-brief.md` (já traz o modelo determinístico). */
|
|
14
|
+
brief: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Monta o prompt do passo de intenção. */
|
|
18
|
+
export function renderHuntIntentPrompt(input: HuntIntentPromptInput): string {
|
|
19
|
+
return [
|
|
20
|
+
`Você é o passo de intenção da PR do projeto ${input.projectName}.`,
|
|
21
|
+
"Leia o brief abaixo e devolva **apenas** o JSON do modelo de intenção.",
|
|
22
|
+
"",
|
|
23
|
+
"## Contrato do harness local",
|
|
24
|
+
"",
|
|
25
|
+
"- Use apenas leitura e busca; **não escreva arquivos** e não rode comandos.",
|
|
26
|
+
"- Ignore a instrução do brief sobre gravar `outputPath`: o houndsman grava por você.",
|
|
27
|
+
"- Responda **somente** com um objeto JSON válido (sem markdown, sem texto antes/depois).",
|
|
28
|
+
'- Inclua `"source": "reviewer"`.',
|
|
29
|
+
"- Não produza achados, severidade nem recomendações.",
|
|
30
|
+
"",
|
|
31
|
+
"### Schema",
|
|
32
|
+
"",
|
|
33
|
+
BUSINESS_SUMMARY_SCHEMA,
|
|
34
|
+
"",
|
|
35
|
+
"## Brief",
|
|
36
|
+
"",
|
|
37
|
+
input.brief.trim(),
|
|
38
|
+
].join("\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Sufixo corretivo das retentativas do passo de intenção. */
|
|
42
|
+
export function renderIntentRetrySuffix(error: string): string {
|
|
43
|
+
return [
|
|
44
|
+
"",
|
|
45
|
+
"## Correção obrigatória",
|
|
46
|
+
"",
|
|
47
|
+
`A resposta anterior foi rejeitada: ${error}`,
|
|
48
|
+
"Responda APENAS com o objeto JSON do modelo de intenção, sem markdown.",
|
|
49
|
+
].join("\n");
|
|
50
|
+
}
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* não altera o brief usado pela skill `/houndsman hunt`.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { GENERAL_WATCHDOG_ID } from "../builtins";
|
|
11
|
+
|
|
10
12
|
/** Dados do prompt de um watchdog. */
|
|
11
13
|
export type HuntPromptInput = {
|
|
12
14
|
projectName: string;
|
|
@@ -29,7 +31,11 @@ function outputSchema(watchdogId: string): string {
|
|
|
29
31
|
"line": 123,
|
|
30
32
|
"message": "o que está errado",
|
|
31
33
|
"action": "o que fazer",
|
|
32
|
-
"evidence": "trecho que comprova"
|
|
34
|
+
"evidence": "trecho que comprova",
|
|
35
|
+
"scenario": "gatilho concreto que expõe o gap (fora de regra)",
|
|
36
|
+
"impact": "consequência observável",
|
|
37
|
+
"confidence": 0.8,
|
|
38
|
+
"basis": "rule | logic | domain"
|
|
33
39
|
}
|
|
34
40
|
]
|
|
35
41
|
}`;
|
|
@@ -48,6 +54,12 @@ export function renderHuntPrompt(input: HuntPromptInput): string {
|
|
|
48
54
|
"- Responda **somente** com um objeto JSON válido (sem markdown, sem texto antes/depois).",
|
|
49
55
|
"- `watchdogId` deve ser exatamente o id acima; use `findings: []` quando não houver achado.",
|
|
50
56
|
"- `line` deve estar dentro do escopo do diff descrito no brief.",
|
|
57
|
+
'- Fora de regra, use `basis: "logic" | "domain"`, com `scenario` + `evidence`; sem os dois a severidade máxima é `attention`.',
|
|
58
|
+
...(input.watchdogId === GENERAL_WATCHDOG_ID
|
|
59
|
+
? [
|
|
60
|
+
'- Este é o revisor de fluxo: inclua `businessSummary` (modelo de intenção reconstruído, `source: "reviewer"`) no JSON.',
|
|
61
|
+
]
|
|
62
|
+
: []),
|
|
51
63
|
"",
|
|
52
64
|
"### Schema",
|
|
53
65
|
"",
|