@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
|
@@ -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
|
}
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orquestração do harness local (`houndsman hunt`).
|
|
3
|
+
*
|
|
4
|
+
* Recebe os briefs já preparados e despacha um processo maki por watchdog. O
|
|
5
|
+
* watchdog devolve o JSON na resposta final; validamos com o mesmo parser do
|
|
6
|
+
* `--finalize` e gravamos em `findings/<id>.json`. Falhas são reportadas por
|
|
7
|
+
* watchdog — a política de finalizar ou não fica com o chamador (CLI).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { GENERAL_WATCHDOG_ID } from "../builtins";
|
|
12
|
+
import { type BusinessSummary, parseBusinessSummary } from "../business-summary";
|
|
13
|
+
import { parseWatchdogReport } from "../report-builder";
|
|
14
|
+
import { systemClock } from "../services/clock";
|
|
15
|
+
import type { ReviewWorkArea } from "../storage/work-area";
|
|
16
|
+
import { renderHuntIntentPrompt, renderIntentRetrySuffix } from "./maki-intent-prompt";
|
|
17
|
+
import { renderHuntPrompt, renderRetrySuffix } from "./maki-prompt";
|
|
18
|
+
import {
|
|
19
|
+
extractJsonObject,
|
|
20
|
+
type MakiProcessRunner,
|
|
21
|
+
type MakiRunMetrics,
|
|
22
|
+
type MakiUsage,
|
|
23
|
+
} from "./maki-runner";
|
|
24
|
+
import { mapPool } from "./pool";
|
|
25
|
+
|
|
26
|
+
/** Evento de progresso (CLI loga; core não escreve saída). */
|
|
27
|
+
export type HuntEvent =
|
|
28
|
+
| {
|
|
29
|
+
type: "intent";
|
|
30
|
+
status: "start" | "done" | "skipped" | "failed";
|
|
31
|
+
source?: BusinessSummary["source"];
|
|
32
|
+
error?: string;
|
|
33
|
+
}
|
|
34
|
+
| { type: "start"; watchdogId: string }
|
|
35
|
+
| {
|
|
36
|
+
type: "retry";
|
|
37
|
+
watchdogId: string;
|
|
38
|
+
attempt: number;
|
|
39
|
+
maxAttempts: number;
|
|
40
|
+
error: string;
|
|
41
|
+
}
|
|
42
|
+
| {
|
|
43
|
+
type: "done";
|
|
44
|
+
watchdogId: string;
|
|
45
|
+
findings: number;
|
|
46
|
+
reviewedFiles: number;
|
|
47
|
+
attempts: number;
|
|
48
|
+
metrics: MakiRunMetrics;
|
|
49
|
+
}
|
|
50
|
+
| {
|
|
51
|
+
type: "failed";
|
|
52
|
+
watchdogId: string;
|
|
53
|
+
error: string;
|
|
54
|
+
attempts: number;
|
|
55
|
+
metrics: MakiRunMetrics;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Resultado de um watchdog. */
|
|
59
|
+
export type HuntWatchdogResult = {
|
|
60
|
+
watchdogId: string;
|
|
61
|
+
status: "done" | "failed";
|
|
62
|
+
findings: number;
|
|
63
|
+
reviewedFiles: number;
|
|
64
|
+
attempts: number;
|
|
65
|
+
durationMs: number;
|
|
66
|
+
sessionId?: string;
|
|
67
|
+
numTurns?: number;
|
|
68
|
+
costUsd?: number;
|
|
69
|
+
usage?: MakiUsage;
|
|
70
|
+
error?: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Agregados do relatório. */
|
|
74
|
+
export type HuntTotals = {
|
|
75
|
+
watchdogs: number;
|
|
76
|
+
done: number;
|
|
77
|
+
failed: number;
|
|
78
|
+
findings: number;
|
|
79
|
+
costUsd: number;
|
|
80
|
+
inputTokens: number;
|
|
81
|
+
outputTokens: number;
|
|
82
|
+
totalTokens: number;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Relatório persistido como `hunt-report.json`. */
|
|
86
|
+
export type HuntReport = {
|
|
87
|
+
version: 1;
|
|
88
|
+
engine: "maki";
|
|
89
|
+
model: string;
|
|
90
|
+
/** Fonte do modelo de intenção usado na revisão. */
|
|
91
|
+
business?: BusinessSummary["source"];
|
|
92
|
+
startedAt: string;
|
|
93
|
+
finishedAt: string;
|
|
94
|
+
durationMs: number;
|
|
95
|
+
watchdogs: HuntWatchdogResult[];
|
|
96
|
+
totals: HuntTotals;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** Dependências do runner. */
|
|
100
|
+
export type HuntRunnerOptions = {
|
|
101
|
+
/** Staging da review (briefs/findings). */
|
|
102
|
+
work: ReviewWorkArea;
|
|
103
|
+
/** Raiz de leitura dos arquivos da review. */
|
|
104
|
+
root: string;
|
|
105
|
+
projectName: string;
|
|
106
|
+
/** Spec `provider/model-id`. */
|
|
107
|
+
model: string;
|
|
108
|
+
apiKeyEnv: string;
|
|
109
|
+
apiKey: string;
|
|
110
|
+
concurrency: number;
|
|
111
|
+
timeoutMs: number;
|
|
112
|
+
retries: number;
|
|
113
|
+
runner: MakiProcessRunner;
|
|
114
|
+
onEvent?: (event: HuntEvent) => void;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** Despacha os briefs via maki e devolve o relatório consolidado. */
|
|
118
|
+
export class HuntRunner {
|
|
119
|
+
private readonly options: HuntRunnerOptions;
|
|
120
|
+
|
|
121
|
+
constructor(options: HuntRunnerOptions) {
|
|
122
|
+
this.options = options;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async run(activeIds: readonly string[]): Promise<HuntReport> {
|
|
126
|
+
const startedAt = systemClock();
|
|
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();
|
|
131
|
+
const watchdogs = await mapPool(activeIds, this.options.concurrency, (id) =>
|
|
132
|
+
this.runOne(id),
|
|
133
|
+
);
|
|
134
|
+
const finishedAt = systemClock();
|
|
135
|
+
return {
|
|
136
|
+
version: 1,
|
|
137
|
+
engine: "maki",
|
|
138
|
+
model: this.options.model,
|
|
139
|
+
business,
|
|
140
|
+
startedAt,
|
|
141
|
+
finishedAt,
|
|
142
|
+
durationMs: Date.now() - startedMs,
|
|
143
|
+
watchdogs,
|
|
144
|
+
totals: totalsOf(watchdogs),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
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
|
+
|
|
211
|
+
private async runOne(watchdogId: string): Promise<HuntWatchdogResult> {
|
|
212
|
+
const briefPath = this.options.work.briefFile(watchdogId);
|
|
213
|
+
let brief: string;
|
|
214
|
+
try {
|
|
215
|
+
brief = readFileSync(briefPath, "utf8");
|
|
216
|
+
} catch {
|
|
217
|
+
const error = `Brief ausente: ${briefPath}`;
|
|
218
|
+
this.emit({
|
|
219
|
+
type: "failed",
|
|
220
|
+
watchdogId,
|
|
221
|
+
error,
|
|
222
|
+
attempts: 0,
|
|
223
|
+
metrics: { durationMs: 0 },
|
|
224
|
+
});
|
|
225
|
+
return failedResult(watchdogId, error, 0, { durationMs: 0 });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const basePrompt = renderHuntPrompt({
|
|
229
|
+
projectName: this.options.projectName,
|
|
230
|
+
watchdogId,
|
|
231
|
+
brief,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
this.emit({ type: "start", watchdogId });
|
|
235
|
+
let attempts = 0;
|
|
236
|
+
let lastError = "";
|
|
237
|
+
const accumulated: MakiRunMetrics = { durationMs: 0 };
|
|
238
|
+
|
|
239
|
+
while (attempts <= this.options.retries) {
|
|
240
|
+
attempts++;
|
|
241
|
+
const prompt =
|
|
242
|
+
attempts === 1
|
|
243
|
+
? basePrompt
|
|
244
|
+
: `${basePrompt}\n${renderRetrySuffix(lastError)}`;
|
|
245
|
+
const result = await this.options.runner.run({
|
|
246
|
+
prompt,
|
|
247
|
+
cwd: this.options.root,
|
|
248
|
+
model: this.options.model,
|
|
249
|
+
apiKeyEnv: this.options.apiKeyEnv,
|
|
250
|
+
apiKey: this.options.apiKey,
|
|
251
|
+
timeoutMs: this.options.timeoutMs,
|
|
252
|
+
});
|
|
253
|
+
mergeMetrics(accumulated, result.metrics);
|
|
254
|
+
|
|
255
|
+
if (!result.ok) {
|
|
256
|
+
lastError = result.error;
|
|
257
|
+
this.emitRetry(watchdogId, attempts, lastError);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const raw = extractJsonObject(result.text);
|
|
262
|
+
if (raw === null) {
|
|
263
|
+
lastError = "a resposta final não contém um objeto JSON.";
|
|
264
|
+
this.emitRetry(watchdogId, attempts, lastError);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const warnings: string[] = [];
|
|
269
|
+
const report = parseWatchdogReport(raw, `maki:${watchdogId}`, (message) =>
|
|
270
|
+
warnings.push(message),
|
|
271
|
+
);
|
|
272
|
+
if (!report) {
|
|
273
|
+
lastError = warnings[0] ?? "JSON fora do schema do watchdog.";
|
|
274
|
+
this.emitRetry(watchdogId, attempts, lastError);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (report.watchdogId !== watchdogId) {
|
|
278
|
+
lastError = `watchdogId '${report.watchdogId}' não corresponde a '${watchdogId}'.`;
|
|
279
|
+
this.emitRetry(watchdogId, attempts, lastError);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
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
|
+
|
|
295
|
+
this.options.work.writeFinding(
|
|
296
|
+
watchdogId,
|
|
297
|
+
`${JSON.stringify(report, null, 2)}\n`,
|
|
298
|
+
);
|
|
299
|
+
const done: HuntWatchdogResult = {
|
|
300
|
+
watchdogId,
|
|
301
|
+
status: "done",
|
|
302
|
+
findings: report.findings.length,
|
|
303
|
+
reviewedFiles: report.reviewedFiles.length,
|
|
304
|
+
attempts,
|
|
305
|
+
durationMs: accumulated.durationMs,
|
|
306
|
+
sessionId: accumulated.sessionId,
|
|
307
|
+
numTurns: accumulated.numTurns,
|
|
308
|
+
costUsd: accumulated.costUsd,
|
|
309
|
+
usage: accumulated.usage,
|
|
310
|
+
};
|
|
311
|
+
this.emit({
|
|
312
|
+
type: "done",
|
|
313
|
+
watchdogId,
|
|
314
|
+
findings: done.findings,
|
|
315
|
+
reviewedFiles: done.reviewedFiles,
|
|
316
|
+
attempts,
|
|
317
|
+
metrics: accumulated,
|
|
318
|
+
});
|
|
319
|
+
return done;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const error = lastError || "falha desconhecida.";
|
|
323
|
+
this.emit({ type: "failed", watchdogId, error, attempts, metrics: accumulated });
|
|
324
|
+
return failedResult(watchdogId, error, attempts, accumulated);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private emit(event: HuntEvent): void {
|
|
328
|
+
try {
|
|
329
|
+
this.options.onEvent?.(event);
|
|
330
|
+
} catch {
|
|
331
|
+
// Progresso nunca derruba a revisão.
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Avisa a próxima tentativa (só quando ainda há retry). */
|
|
336
|
+
private emitRetry(watchdogId: string, attempt: number, error: string): void {
|
|
337
|
+
if (attempt > this.options.retries) return;
|
|
338
|
+
this.emit({
|
|
339
|
+
type: "retry",
|
|
340
|
+
watchdogId,
|
|
341
|
+
attempt,
|
|
342
|
+
maxAttempts: this.options.retries + 1,
|
|
343
|
+
error,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Soma métricas de várias tentativas no acumulado. */
|
|
349
|
+
function mergeMetrics(target: MakiRunMetrics, source: MakiRunMetrics): void {
|
|
350
|
+
target.durationMs += source.durationMs;
|
|
351
|
+
target.sessionId = source.sessionId ?? target.sessionId;
|
|
352
|
+
if (source.numTurns !== undefined) {
|
|
353
|
+
target.numTurns = (target.numTurns ?? 0) + source.numTurns;
|
|
354
|
+
}
|
|
355
|
+
if (source.costUsd !== undefined) {
|
|
356
|
+
target.costUsd = (target.costUsd ?? 0) + source.costUsd;
|
|
357
|
+
}
|
|
358
|
+
if (source.usage) {
|
|
359
|
+
target.usage = {
|
|
360
|
+
inputTokens:
|
|
361
|
+
(target.usage?.inputTokens ?? 0) + (source.usage.inputTokens ?? 0),
|
|
362
|
+
outputTokens:
|
|
363
|
+
(target.usage?.outputTokens ?? 0) + (source.usage.outputTokens ?? 0),
|
|
364
|
+
totalTokens:
|
|
365
|
+
(target.usage?.totalTokens ?? 0) + (source.usage.totalTokens ?? 0),
|
|
366
|
+
cacheReadInputTokens:
|
|
367
|
+
(target.usage?.cacheReadInputTokens ?? 0) +
|
|
368
|
+
(source.usage.cacheReadInputTokens ?? 0),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Resultado de falha de um watchdog. */
|
|
374
|
+
function failedResult(
|
|
375
|
+
watchdogId: string,
|
|
376
|
+
error: string,
|
|
377
|
+
attempts: number,
|
|
378
|
+
metrics: MakiRunMetrics,
|
|
379
|
+
): HuntWatchdogResult {
|
|
380
|
+
return {
|
|
381
|
+
watchdogId,
|
|
382
|
+
status: "failed",
|
|
383
|
+
findings: 0,
|
|
384
|
+
reviewedFiles: 0,
|
|
385
|
+
attempts,
|
|
386
|
+
durationMs: metrics.durationMs,
|
|
387
|
+
sessionId: metrics.sessionId,
|
|
388
|
+
numTurns: metrics.numTurns,
|
|
389
|
+
costUsd: metrics.costUsd,
|
|
390
|
+
usage: metrics.usage,
|
|
391
|
+
error,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Agrega os resultados por watchdog. */
|
|
396
|
+
function totalsOf(watchdogs: readonly HuntWatchdogResult[]): HuntTotals {
|
|
397
|
+
const totals: HuntTotals = {
|
|
398
|
+
watchdogs: watchdogs.length,
|
|
399
|
+
done: 0,
|
|
400
|
+
failed: 0,
|
|
401
|
+
findings: 0,
|
|
402
|
+
costUsd: 0,
|
|
403
|
+
inputTokens: 0,
|
|
404
|
+
outputTokens: 0,
|
|
405
|
+
totalTokens: 0,
|
|
406
|
+
};
|
|
407
|
+
for (const watchdog of watchdogs) {
|
|
408
|
+
if (watchdog.status === "done") totals.done++;
|
|
409
|
+
else totals.failed++;
|
|
410
|
+
totals.findings += watchdog.findings;
|
|
411
|
+
totals.costUsd += watchdog.costUsd ?? 0;
|
|
412
|
+
totals.inputTokens += watchdog.usage?.inputTokens ?? 0;
|
|
413
|
+
totals.outputTokens += watchdog.usage?.outputTokens ?? 0;
|
|
414
|
+
totals.totalTokens += watchdog.usage?.totalTokens ?? 0;
|
|
415
|
+
}
|
|
416
|
+
totals.costUsd = Number(totals.costUsd.toFixed(6));
|
|
417
|
+
return totals;
|
|
418
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fachada do harness local (`core/hunt`).
|
|
3
|
+
*
|
|
4
|
+
* A skill `/houndsman hunt` não importa nada daqui: este módulo é exclusivo do
|
|
5
|
+
* caminho CLI (`houndsman hunt`), que usa o maki para executar os briefs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type {
|
|
9
|
+
HuntEvent,
|
|
10
|
+
HuntReport,
|
|
11
|
+
HuntRunnerOptions,
|
|
12
|
+
HuntTotals,
|
|
13
|
+
HuntWatchdogResult,
|
|
14
|
+
} from "./hunt-runner";
|
|
15
|
+
export { HuntRunner } from "./hunt-runner";
|
|
16
|
+
export type {
|
|
17
|
+
InstallMakiOptions,
|
|
18
|
+
InstallMakiResult,
|
|
19
|
+
MakiBinary,
|
|
20
|
+
MakiBinarySource,
|
|
21
|
+
MakiInstallManifest,
|
|
22
|
+
} from "./maki-install";
|
|
23
|
+
export {
|
|
24
|
+
installMaki,
|
|
25
|
+
makiCurrentFile,
|
|
26
|
+
makiInstallDir,
|
|
27
|
+
makiSourceLabel,
|
|
28
|
+
makiToolsRoot,
|
|
29
|
+
probeMakiVersion,
|
|
30
|
+
readMakiInstall,
|
|
31
|
+
requireMakiBinary,
|
|
32
|
+
resolveMakiBinary,
|
|
33
|
+
uninstallMaki,
|
|
34
|
+
} from "./maki-install";
|
|
35
|
+
export type { HuntIntentPromptInput } from "./maki-intent-prompt";
|
|
36
|
+
export { renderHuntIntentPrompt, renderIntentRetrySuffix } from "./maki-intent-prompt";
|
|
37
|
+
export type { MakiModel } from "./maki-model";
|
|
38
|
+
export {
|
|
39
|
+
houndsmanKeyEnvFor,
|
|
40
|
+
MAKI_API_KEY_ENV,
|
|
41
|
+
requireMakiModel,
|
|
42
|
+
resolveMakiModel,
|
|
43
|
+
} from "./maki-model";
|
|
44
|
+
export { renderHuntPrompt, renderRetrySuffix } from "./maki-prompt";
|
|
45
|
+
export type {
|
|
46
|
+
MakiJsonOutput,
|
|
47
|
+
MakiProcessRunner,
|
|
48
|
+
MakiRunMetrics,
|
|
49
|
+
MakiRunRequest,
|
|
50
|
+
MakiRunResult,
|
|
51
|
+
MakiUsage,
|
|
52
|
+
} from "./maki-runner";
|
|
53
|
+
export {
|
|
54
|
+
extractJsonObject,
|
|
55
|
+
MAKI_DISALLOWED_TOOLS,
|
|
56
|
+
makiArgs,
|
|
57
|
+
metricsFrom,
|
|
58
|
+
parseMakiOutput,
|
|
59
|
+
SpawnMakiRunner,
|
|
60
|
+
} from "./maki-runner";
|
|
61
|
+
export { mapPool } from "./pool";
|