@payconductor/houndsman 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -2
- 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/doctor-command.ts +22 -0
- package/src/cli/commands/findings-command.ts +11 -1
- package/src/cli/commands/hunt-command.ts +479 -0
- package/src/cli/commands/review-command.ts +58 -2
- package/src/cli/commands/status-command.ts +3 -2
- package/src/cli/help.ts +20 -3
- package/src/cli/index.ts +20 -3
- package/src/cli/progress.ts +50 -0
- package/src/core/ai/ai-client.ts +38 -2
- package/src/core/ai/ai-sdk-model-provider.ts +39 -0
- 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 +14 -1
- 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/types.ts +2 -0
- 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 +38 -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 +57 -1
- package/src/core/defaults.ts +63 -2
- 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 +106 -26
- package/src/core/engine/review-session.ts +18 -16
- package/src/core/engine/review-workspace.ts +14 -0
- package/src/core/engine/run-artifacts.ts +196 -0
- package/src/core/engine/run-format.ts +4 -0
- package/src/core/env.ts +14 -0
- package/src/core/findings-state.ts +13 -1
- package/src/core/github-review.ts +5 -2
- package/src/core/hunt/hunt-runner.ts +418 -0
- package/src/core/hunt/index.ts +61 -0
- package/src/core/hunt/maki-install.ts +331 -0
- package/src/core/hunt/maki-intent-prompt.ts +50 -0
- package/src/core/hunt/maki-model.ts +101 -0
- package/src/core/hunt/maki-prompt.ts +83 -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 +8 -0
- package/src/core/logging/index.ts +7 -0
- package/src/core/logging/logger.ts +19 -0
- package/src/core/logging/progress.ts +366 -0
- package/src/core/orchestrator.ts +33 -1180
- 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 +46 -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 +17 -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 +78 -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 +30 -14
- package/src/skill/SETUP.md +22 -1
- package/src/skill/SKILL.md +17 -4
- package/src/skill/example-resume.md +43 -0
- package/src/skill/hunt.md +181 -58
- package/src/tests/__snapshots__/baseline.test.ts.snap +206 -178
- package/src/tests/ai-core.test.ts +84 -1
- 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/cli.test.ts +1 -0
- package/src/tests/compare.test.ts +233 -0
- package/src/tests/fixtures/fake-model-provider.ts +2 -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 +525 -0
- package/src/tests/markdown.test.ts +3 -3
- package/src/tests/progress.test.ts +145 -0
- package/src/tests/propagation.test.ts +119 -0
- package/src/tests/review-resume.test.ts +60 -1
- package/src/tests/review-scope.test.ts +90 -19
- 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,28 +52,47 @@ 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>;
|
|
58
63
|
};
|
|
59
64
|
|
|
65
|
+
/** Etapas da resolução de escopo reportadas ao CLI (sem log no core). */
|
|
66
|
+
export type ScopeStage = "diff" | "ast" | "classify";
|
|
67
|
+
|
|
68
|
+
/** Evento de progresso de uma etapa do escopo. */
|
|
69
|
+
export type ScopeProgressEvent =
|
|
70
|
+
| { type: "start"; stage: ScopeStage }
|
|
71
|
+
| { type: "done"; stage: ScopeStage; durationMs: number };
|
|
72
|
+
|
|
73
|
+
/** Callback de progresso (falhas do callback são ignoradas). */
|
|
74
|
+
export type ScopeProgress = (event: ScopeProgressEvent) => void;
|
|
75
|
+
|
|
60
76
|
/** Decide entre review completo e incremental. */
|
|
61
77
|
export class ReviewScopeResolver {
|
|
62
78
|
private readonly options: ScopeOptions;
|
|
63
79
|
private readonly classifier: FileClassifier;
|
|
64
80
|
private readonly ast?: AstService;
|
|
65
81
|
private readonly git: GitRepository;
|
|
82
|
+
private readonly onProgress?: ScopeProgress;
|
|
66
83
|
|
|
67
84
|
constructor(
|
|
68
85
|
options: ScopeOptions,
|
|
69
86
|
config: HoundsmanConfig,
|
|
70
87
|
ast?: AstService,
|
|
71
88
|
git: GitRepository = new GitRepository(),
|
|
89
|
+
onProgress?: ScopeProgress,
|
|
72
90
|
) {
|
|
73
91
|
this.options = options;
|
|
74
92
|
this.classifier = new FileClassifier(config);
|
|
75
93
|
this.ast = ast;
|
|
76
94
|
this.git = git;
|
|
95
|
+
this.onProgress = onProgress;
|
|
77
96
|
}
|
|
78
97
|
|
|
79
98
|
/**
|
|
@@ -100,27 +119,37 @@ export class ReviewScopeResolver {
|
|
|
100
119
|
!prevSha ||
|
|
101
120
|
!this.git.isAncestor(prevSha, headSha)
|
|
102
121
|
) {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
122
|
+
const diffData = this.step("diff", () => {
|
|
123
|
+
const diff = this.git.collectDiff(this.options.base, this.options.head);
|
|
124
|
+
const entries = parseEntries(
|
|
125
|
+
this.git.diffNameStatus(this.options.base, this.options.head),
|
|
126
|
+
);
|
|
127
|
+
const baseRef =
|
|
128
|
+
this.git.mergeBase(this.options.base, this.options.head) ??
|
|
129
|
+
this.options.base;
|
|
130
|
+
// Patch `-U0` sem truncar: mais preciso para delta AST e escopo.
|
|
131
|
+
const scopeDiff = this.zeroContext(
|
|
132
|
+
`${this.options.base}...${this.options.head}`,
|
|
133
|
+
diff,
|
|
134
|
+
);
|
|
135
|
+
return { diff, entries, baseRef, scopeDiff };
|
|
136
|
+
});
|
|
137
|
+
const { diff, entries, baseRef, scopeDiff } = diffData;
|
|
138
|
+
const astService = this.ast;
|
|
139
|
+
const ast = astService
|
|
140
|
+
? await this.stepAsync("ast", () =>
|
|
141
|
+
astService.analyze(entries, scopeDiff, baseRef),
|
|
142
|
+
)
|
|
117
143
|
: undefined;
|
|
118
|
-
const result = this.
|
|
119
|
-
.
|
|
120
|
-
|
|
144
|
+
const result = this.step("classify", () =>
|
|
145
|
+
this.classifier
|
|
146
|
+
.classify(entries, churnToRecord(diff.churn), factsOf(ast))
|
|
147
|
+
.toJSON(),
|
|
148
|
+
);
|
|
121
149
|
return {
|
|
122
150
|
kind: "review",
|
|
123
151
|
diff,
|
|
152
|
+
patch: scopeDiff.byFile,
|
|
124
153
|
ast,
|
|
125
154
|
classification: {
|
|
126
155
|
...result,
|
|
@@ -136,16 +165,25 @@ export class ReviewScopeResolver {
|
|
|
136
165
|
}
|
|
137
166
|
|
|
138
167
|
const range = `${prevSha}..${headSha}`;
|
|
139
|
-
const
|
|
140
|
-
|
|
168
|
+
const diffData = this.step("diff", () => {
|
|
169
|
+
const diff = this.git.collectDiffSince(prevSha, headSha);
|
|
170
|
+
const entries = parseEntries(this.git.run(["diff", range, "--name-status"]));
|
|
171
|
+
const scopeDiff = this.zeroContext(range, diff);
|
|
172
|
+
return { diff, entries, scopeDiff };
|
|
173
|
+
});
|
|
174
|
+
const { diff, entries, scopeDiff } = diffData;
|
|
141
175
|
const changedPaths = new Set(entries.map((entry) => entry.path));
|
|
142
|
-
const
|
|
143
|
-
const ast =
|
|
144
|
-
? await this.ast
|
|
176
|
+
const astService = this.ast;
|
|
177
|
+
const ast = astService
|
|
178
|
+
? await this.stepAsync("ast", () =>
|
|
179
|
+
astService.analyze(entries, scopeDiff, prevSha),
|
|
180
|
+
)
|
|
145
181
|
: undefined;
|
|
146
|
-
const result = this.
|
|
147
|
-
.
|
|
148
|
-
|
|
182
|
+
const result = this.step("classify", () =>
|
|
183
|
+
this.classifier
|
|
184
|
+
.classify(entries, churnToRecord(diff.churn), factsOf(ast))
|
|
185
|
+
.toJSON(),
|
|
186
|
+
);
|
|
149
187
|
const classification: RunClassification = {
|
|
150
188
|
...result,
|
|
151
189
|
base: prevSha,
|
|
@@ -160,7 +198,49 @@ export class ReviewScopeResolver {
|
|
|
160
198
|
ast,
|
|
161
199
|
}),
|
|
162
200
|
};
|
|
163
|
-
return {
|
|
201
|
+
return {
|
|
202
|
+
kind: "review",
|
|
203
|
+
classification,
|
|
204
|
+
diff,
|
|
205
|
+
patch: scopeDiff.byFile,
|
|
206
|
+
reviewedFrom: prevSha,
|
|
207
|
+
ast,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Executa uma etapa síncrona reportando início/fim. O callback de progresso
|
|
213
|
+
* é diagnóstico: nunca derruba a resolução.
|
|
214
|
+
*/
|
|
215
|
+
private step<T>(stage: ScopeStage, run: () => T): T {
|
|
216
|
+
if (!this.onProgress) return run();
|
|
217
|
+
const started = Date.now();
|
|
218
|
+
this.report({ type: "start", stage });
|
|
219
|
+
try {
|
|
220
|
+
return run();
|
|
221
|
+
} finally {
|
|
222
|
+
this.report({ type: "done", stage, durationMs: Date.now() - started });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Versão assíncrona de `step`. */
|
|
227
|
+
private async stepAsync<T>(stage: ScopeStage, run: () => Promise<T> | T): Promise<T> {
|
|
228
|
+
if (!this.onProgress) return run();
|
|
229
|
+
const started = Date.now();
|
|
230
|
+
this.report({ type: "start", stage });
|
|
231
|
+
try {
|
|
232
|
+
return await run();
|
|
233
|
+
} finally {
|
|
234
|
+
this.report({ type: "done", stage, durationMs: Date.now() - started });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private report(event: ScopeProgressEvent): void {
|
|
239
|
+
try {
|
|
240
|
+
this.onProgress?.(event);
|
|
241
|
+
} catch {
|
|
242
|
+
// Progresso nunca derruba a revisão.
|
|
243
|
+
}
|
|
164
244
|
}
|
|
165
245
|
|
|
166
246
|
/** Patch `-U0` do intervalo; falha aberta cai no diff já coletado. */
|
|
@@ -11,6 +11,7 @@ import { createGitRunner } from "../git-runner";
|
|
|
11
11
|
import {
|
|
12
12
|
type FinalizeResult,
|
|
13
13
|
finalize,
|
|
14
|
+
type PrepareProgress,
|
|
14
15
|
type PrepareResult,
|
|
15
16
|
prepare,
|
|
16
17
|
type ReviewContext,
|
|
@@ -19,7 +20,7 @@ import {
|
|
|
19
20
|
resolveBranch,
|
|
20
21
|
reviewContext,
|
|
21
22
|
} from "../orchestrator";
|
|
22
|
-
import {
|
|
23
|
+
import { reviewSlug } from "../report-builder";
|
|
23
24
|
import { GitRepository } from "../services/git-repository";
|
|
24
25
|
import type { RepoIdentity, StorageProvider } from "../storage/types";
|
|
25
26
|
|
|
@@ -47,12 +48,14 @@ export class ReviewSession {
|
|
|
47
48
|
this.branch = branch;
|
|
48
49
|
this.ast = ast;
|
|
49
50
|
this.ai = ai;
|
|
51
|
+
const variant = options.variant?.trim() || undefined;
|
|
50
52
|
const review = options.finalize
|
|
51
|
-
? storage.findReview(repo.key, branch)
|
|
53
|
+
? storage.findReview(repo.key, branch, variant)
|
|
52
54
|
: storage.getOrCreateReview({
|
|
53
55
|
repo,
|
|
54
56
|
branch,
|
|
55
|
-
|
|
57
|
+
variant,
|
|
58
|
+
slug: reviewSlug(branch, variant),
|
|
56
59
|
base: options.base,
|
|
57
60
|
head: options.head,
|
|
58
61
|
worktreePath: options.worktreePath,
|
|
@@ -60,7 +63,7 @@ export class ReviewSession {
|
|
|
60
63
|
});
|
|
61
64
|
if (!review) {
|
|
62
65
|
throw new Error(
|
|
63
|
-
`Nenhuma revisão para '${branch}'. Rode \`houndsman\` (preparar) primeiro.`,
|
|
66
|
+
`Nenhuma revisão para '${branch}'${variant ? ` (variante '${variant}')` : ""}. Rode \`houndsman\` (preparar) primeiro.`,
|
|
64
67
|
);
|
|
65
68
|
}
|
|
66
69
|
// Um prepare no clone principal invalida um worktree usado antes; o finalize
|
|
@@ -104,24 +107,23 @@ export class ReviewSession {
|
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
/** Passo 1 — preparar. */
|
|
107
|
-
prepare(): Promise<PrepareResult> {
|
|
108
|
-
return prepare(
|
|
109
|
-
this.
|
|
110
|
-
this.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
this.ast,
|
|
114
|
-
this.ai,
|
|
115
|
-
);
|
|
110
|
+
prepare(onProgress?: PrepareProgress): Promise<PrepareResult> {
|
|
111
|
+
return prepare(this.context, this.config, this.options, {
|
|
112
|
+
ast: this.ast,
|
|
113
|
+
ai: this.ai,
|
|
114
|
+
onProgress,
|
|
115
|
+
});
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/** Passo 3 — finalizar. */
|
|
119
119
|
finalize(): FinalizeResult {
|
|
120
|
-
return finalize(this.
|
|
120
|
+
return finalize(this.context, this.config, this.options);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/** Executa o passo escolhido em `--finalize`. */
|
|
124
|
-
run(): Promise<PrepareResult | FinalizeResult> {
|
|
125
|
-
return this.options.finalize
|
|
124
|
+
run(onProgress?: PrepareProgress): Promise<PrepareResult | FinalizeResult> {
|
|
125
|
+
return this.options.finalize
|
|
126
|
+
? Promise.resolve(this.finalize())
|
|
127
|
+
: this.prepare(onProgress);
|
|
126
128
|
}
|
|
127
129
|
}
|
|
@@ -35,6 +35,8 @@ export type WorkspaceDeps = {
|
|
|
35
35
|
worktreesLocation?: string;
|
|
36
36
|
/** Override explícito da raiz de worktrees (testes). */
|
|
37
37
|
worktreesRoot?: string;
|
|
38
|
+
/** Passos longos (gh/worktree) reportados ao CLI; nunca lança. */
|
|
39
|
+
onProgress?: (message: string) => void;
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
/** Workspace resolvido para a revisão. */
|
|
@@ -85,6 +87,14 @@ export function resolveReviewWorkspace(
|
|
|
85
87
|
deps: WorkspaceDeps = {},
|
|
86
88
|
): ResolvedWorkspace {
|
|
87
89
|
const messages: string[] = [];
|
|
90
|
+
/** Progresso é diagnóstico: nunca derruba a resolução. */
|
|
91
|
+
const report = (message: string): void => {
|
|
92
|
+
try {
|
|
93
|
+
deps.onProgress?.(message);
|
|
94
|
+
} catch {
|
|
95
|
+
// Progresso nunca derruba a revisão.
|
|
96
|
+
}
|
|
97
|
+
};
|
|
88
98
|
const cwdGit = repoAt(request.cwd);
|
|
89
99
|
const cwdRoot = resolve(cwdGit.rootPath() ?? request.cwd);
|
|
90
100
|
const mainRoot = resolve(mainRootOf(cwdGit, cwdRoot));
|
|
@@ -108,6 +118,7 @@ export function resolveReviewWorkspace(
|
|
|
108
118
|
// A camada gerada não vem no checkout; sem ela os comandos no worktree
|
|
109
119
|
// não carregam a config (`../core/types`).
|
|
110
120
|
const explicitMainRoot = mainRootOf(git, repo.root);
|
|
121
|
+
report("Sincronizando a camada gerada no worktree…");
|
|
111
122
|
syncProjectLayer(explicitMainRoot, root);
|
|
112
123
|
return {
|
|
113
124
|
root,
|
|
@@ -129,6 +140,7 @@ export function resolveReviewWorkspace(
|
|
|
129
140
|
deps.ghRunner ?? createGhRunner(mainRoot),
|
|
130
141
|
mainGit,
|
|
131
142
|
);
|
|
143
|
+
report(`Consultando a PR #${request.pr} via gh…`);
|
|
132
144
|
const resolved = resolver.resolve(request.pr);
|
|
133
145
|
if (!resolved.ok) {
|
|
134
146
|
throw new Error(
|
|
@@ -136,6 +148,7 @@ export function resolveReviewWorkspace(
|
|
|
136
148
|
);
|
|
137
149
|
}
|
|
138
150
|
const { pr } = resolved;
|
|
151
|
+
report("Buscando as refs da PR…");
|
|
139
152
|
const refs = resolver.prepareRefs(pr);
|
|
140
153
|
if (refs.warning) messages.push(`GitHub: ${refs.warning}`);
|
|
141
154
|
|
|
@@ -146,6 +159,7 @@ export function resolveReviewWorkspace(
|
|
|
146
159
|
deps.worktreesRoot ??
|
|
147
160
|
resolveWorktreesRoot(deps.worktreesLocation, mainRoot),
|
|
148
161
|
});
|
|
162
|
+
report("Criando/reutilizando o worktree da PR…");
|
|
149
163
|
const worktree = manager.ensure({ branch: pr.headRefName, ref: refs.head });
|
|
150
164
|
const worktreePath = resolve(worktree.path);
|
|
151
165
|
messages.push(
|
|
@@ -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
|
+
}
|
package/src/core/env.ts
CHANGED
|
@@ -19,6 +19,8 @@ export const ENV_NAMES = {
|
|
|
19
19
|
deepSeekApiKey: "HOUNDSMAN_DEEP_SEEK_API_KEY",
|
|
20
20
|
openAiApiKey: "HOUNDSMAN_OPENAI_API_KEY",
|
|
21
21
|
anthropicApiKey: "HOUNDSMAN_ANTHROPIC_API_KEY",
|
|
22
|
+
makiPath: "HOUNDSMAN_MAKI_PATH",
|
|
23
|
+
makiVersion: "HOUNDSMAN_MAKI_VERSION",
|
|
22
24
|
home: "HOUNDSMAN_HOME",
|
|
23
25
|
localAppData: "LOCALAPPDATA",
|
|
24
26
|
xdgDataHome: "XDG_DATA_HOME",
|
|
@@ -33,6 +35,10 @@ export type HoundsmanEnv = {
|
|
|
33
35
|
deepSeekApiKey?: string;
|
|
34
36
|
openAiApiKey?: string;
|
|
35
37
|
anthropicApiKey?: string;
|
|
38
|
+
/** Caminho explícito do binário do maki (harness local `hunt`). */
|
|
39
|
+
makiPath?: string;
|
|
40
|
+
/** Versão do maki a instalar (default: a fixada pelo pacote). */
|
|
41
|
+
makiVersion?: string;
|
|
36
42
|
home?: string;
|
|
37
43
|
localAppData?: string;
|
|
38
44
|
xdgDataHome?: string;
|
|
@@ -54,6 +60,8 @@ export function readEnv(): HoundsmanEnv {
|
|
|
54
60
|
deepSeekApiKey: read(ENV_NAMES.deepSeekApiKey),
|
|
55
61
|
openAiApiKey: read(ENV_NAMES.openAiApiKey),
|
|
56
62
|
anthropicApiKey: read(ENV_NAMES.anthropicApiKey),
|
|
63
|
+
makiPath: read(ENV_NAMES.makiPath),
|
|
64
|
+
makiVersion: read(ENV_NAMES.makiVersion),
|
|
57
65
|
home: read(ENV_NAMES.home),
|
|
58
66
|
localAppData: read(ENV_NAMES.localAppData),
|
|
59
67
|
xdgDataHome: read(ENV_NAMES.xdgDataHome),
|
|
@@ -80,6 +88,12 @@ export const env = {
|
|
|
80
88
|
get anthropicApiKey(): string | undefined {
|
|
81
89
|
return read(ENV_NAMES.anthropicApiKey);
|
|
82
90
|
},
|
|
91
|
+
get makiPath(): string | undefined {
|
|
92
|
+
return read(ENV_NAMES.makiPath);
|
|
93
|
+
},
|
|
94
|
+
get makiVersion(): string | undefined {
|
|
95
|
+
return read(ENV_NAMES.makiVersion);
|
|
96
|
+
},
|
|
83
97
|
get home(): string | undefined {
|
|
84
98
|
return read(ENV_NAMES.home);
|
|
85
99
|
},
|