@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
|
@@ -0,0 +1,59 @@
|
|
|
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 { MakiModel } from "./maki-model";
|
|
36
|
+
export {
|
|
37
|
+
houndsmanKeyEnvFor,
|
|
38
|
+
MAKI_API_KEY_ENV,
|
|
39
|
+
requireMakiModel,
|
|
40
|
+
resolveMakiModel,
|
|
41
|
+
} from "./maki-model";
|
|
42
|
+
export { renderHuntPrompt, renderRetrySuffix } from "./maki-prompt";
|
|
43
|
+
export type {
|
|
44
|
+
MakiJsonOutput,
|
|
45
|
+
MakiProcessRunner,
|
|
46
|
+
MakiRunMetrics,
|
|
47
|
+
MakiRunRequest,
|
|
48
|
+
MakiRunResult,
|
|
49
|
+
MakiUsage,
|
|
50
|
+
} from "./maki-runner";
|
|
51
|
+
export {
|
|
52
|
+
extractJsonObject,
|
|
53
|
+
MAKI_DISALLOWED_TOOLS,
|
|
54
|
+
makiArgs,
|
|
55
|
+
metricsFrom,
|
|
56
|
+
parseMakiOutput,
|
|
57
|
+
SpawnMakiRunner,
|
|
58
|
+
} from "./maki-runner";
|
|
59
|
+
export { mapPool } from "./pool";
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instalação e descoberta do binário do maki (harness local).
|
|
3
|
+
*
|
|
4
|
+
* O binário é opcional e fica fora do projeto: `<HOUNDSMAN_HOME>/tools/maki`.
|
|
5
|
+
* `houndsman hunt install` baixa o release fixado e verifica o sha256; o
|
|
6
|
+
* `houndsman update`/`setup` nunca toca nisso. A skill `/houndsman hunt`
|
|
7
|
+
* (harness externo) não usa este módulo.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import {
|
|
12
|
+
chmodSync,
|
|
13
|
+
copyFileSync,
|
|
14
|
+
existsSync,
|
|
15
|
+
mkdirSync,
|
|
16
|
+
mkdtempSync,
|
|
17
|
+
readdirSync,
|
|
18
|
+
readFileSync,
|
|
19
|
+
rmSync,
|
|
20
|
+
writeFileSync,
|
|
21
|
+
} from "node:fs";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
23
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
24
|
+
import { env } from "../env";
|
|
25
|
+
import { displayPath } from "../paths";
|
|
26
|
+
import { systemClock } from "../services/clock";
|
|
27
|
+
import { houndsmanHome } from "../storage/paths";
|
|
28
|
+
import {
|
|
29
|
+
checksumFor,
|
|
30
|
+
effectiveMakiVersion,
|
|
31
|
+
makiAssetFor,
|
|
32
|
+
makiBinaryName,
|
|
33
|
+
makiChecksumsUrl,
|
|
34
|
+
makiDownloadUrl,
|
|
35
|
+
normalizeMakiVersion,
|
|
36
|
+
} from "./maki-release";
|
|
37
|
+
|
|
38
|
+
/** Origem do binário resolvido. */
|
|
39
|
+
export type MakiBinarySource = "env" | "config" | "managed" | "path";
|
|
40
|
+
|
|
41
|
+
/** Binário do maki disponível para execução. */
|
|
42
|
+
export type MakiBinary = {
|
|
43
|
+
path: string;
|
|
44
|
+
source: MakiBinarySource;
|
|
45
|
+
/** Versão conhecida (instalação gerenciada). */
|
|
46
|
+
version?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Manifesto da instalação gerenciada (`current.json`). */
|
|
50
|
+
export type MakiInstallManifest = {
|
|
51
|
+
version: string;
|
|
52
|
+
path: string;
|
|
53
|
+
asset?: string;
|
|
54
|
+
sha256?: string;
|
|
55
|
+
installedAt: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Raiz das versões gerenciadas do maki. */
|
|
59
|
+
export function makiToolsRoot(): string {
|
|
60
|
+
return join(houndsmanHome(), "tools", "maki");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Diretório de uma versão instalada. */
|
|
64
|
+
export function makiInstallDir(version: string): string {
|
|
65
|
+
return join(makiToolsRoot(), normalizeMakiVersion(version));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Caminho do ponteiro para a instalação ativa. */
|
|
69
|
+
export function makiCurrentFile(): string {
|
|
70
|
+
return join(makiToolsRoot(), "current.json");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Lê o manifesto da instalação gerenciada (null se ausente/inválido). */
|
|
74
|
+
export function readMakiInstall(): MakiInstallManifest | null {
|
|
75
|
+
const file = makiCurrentFile();
|
|
76
|
+
if (!existsSync(file)) return null;
|
|
77
|
+
try {
|
|
78
|
+
const parsed = JSON.parse(
|
|
79
|
+
readFileSync(file, "utf8"),
|
|
80
|
+
) as Partial<MakiInstallManifest>;
|
|
81
|
+
if (typeof parsed.version !== "string" || typeof parsed.path !== "string") {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
version: parsed.version,
|
|
86
|
+
path: parsed.path,
|
|
87
|
+
asset: parsed.asset,
|
|
88
|
+
sha256: parsed.sha256,
|
|
89
|
+
installedAt: parsed.installedAt ?? systemClock(),
|
|
90
|
+
};
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Grava o ponteiro da instalação ativa. */
|
|
97
|
+
function writeInstallManifest(manifest: MakiInstallManifest): void {
|
|
98
|
+
const file = makiCurrentFile();
|
|
99
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
100
|
+
writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolve o binário: `HOUNDSMAN_MAKI_PATH` > `hunt.binaryPath` > instalação
|
|
105
|
+
* gerenciada > `maki` no PATH. Não valida existência (use `requireMakiBinary`).
|
|
106
|
+
*/
|
|
107
|
+
export function resolveMakiBinary(configPath?: string): MakiBinary | null {
|
|
108
|
+
const explicit = env.makiPath ?? configPath?.trim();
|
|
109
|
+
if (explicit && explicit.length > 0) {
|
|
110
|
+
return { path: resolve(explicit), source: env.makiPath ? "env" : "config" };
|
|
111
|
+
}
|
|
112
|
+
const managed = readMakiInstall();
|
|
113
|
+
if (managed)
|
|
114
|
+
return { path: managed.path, source: "managed", version: managed.version };
|
|
115
|
+
const which = Bun.which("maki");
|
|
116
|
+
if (which) return { path: which, source: "path" };
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Rótulo legível da origem (diagnóstico). */
|
|
121
|
+
export function makiSourceLabel(source: MakiBinarySource): string {
|
|
122
|
+
switch (source) {
|
|
123
|
+
case "env":
|
|
124
|
+
return "HOUNDSMAN_MAKI_PATH";
|
|
125
|
+
case "config":
|
|
126
|
+
return "hunt.binaryPath";
|
|
127
|
+
case "managed":
|
|
128
|
+
return "instalação gerenciada";
|
|
129
|
+
default:
|
|
130
|
+
return "PATH";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Resolve e exige um binário existente; lança erro acionável quando ausente. */
|
|
135
|
+
export function requireMakiBinary(configPath?: string): MakiBinary {
|
|
136
|
+
const binary = resolveMakiBinary(configPath);
|
|
137
|
+
if (!binary) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
"Capability `hunt` não instalada: rode `houndsman hunt install` (ou defina HOUNDSMAN_MAKI_PATH).",
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (!existsSync(binary.path)) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Binário do maki não encontrado em ${displayPath(binary.path)} (${makiSourceLabel(binary.source)}). ` +
|
|
145
|
+
"Rode `houndsman hunt install --force` ou ajuste HOUNDSMAN_MAKI_PATH.",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return binary;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Lê a versão do binário (`maki --version`); `undefined` se indisponível. */
|
|
152
|
+
export function probeMakiVersion(binaryPath: string): string | undefined {
|
|
153
|
+
try {
|
|
154
|
+
const result = Bun.spawnSync([binaryPath, "--version"], {
|
|
155
|
+
stdout: "pipe",
|
|
156
|
+
stderr: "pipe",
|
|
157
|
+
});
|
|
158
|
+
if (result.exitCode !== 0) return undefined;
|
|
159
|
+
const output = result.stdout.toString().trim();
|
|
160
|
+
return output.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/)?.[0];
|
|
161
|
+
} catch {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Opções de instalação. */
|
|
167
|
+
export type InstallMakiOptions = {
|
|
168
|
+
/** Versão alvo (`vX.Y.Z` ou `X.Y.Z`). */
|
|
169
|
+
version?: string;
|
|
170
|
+
/** Reinstala mesmo se o binário já existir. */
|
|
171
|
+
force?: boolean;
|
|
172
|
+
/** Arquivo local (`.tar.gz`/`.zip`) para ambientes offline. */
|
|
173
|
+
from?: string;
|
|
174
|
+
/** Progresso textual (CLI). */
|
|
175
|
+
log?: (message: string) => void;
|
|
176
|
+
/** `fetch` injetável (testes). */
|
|
177
|
+
fetchImpl?: typeof fetch;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/** Resultado da instalação. */
|
|
181
|
+
export type InstallMakiResult = {
|
|
182
|
+
version: string;
|
|
183
|
+
path: string;
|
|
184
|
+
skipped: boolean;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/** Baixa (ou extrai) o maki para `<HOUNDSMAN_HOME>/tools/maki/<versão>`. */
|
|
188
|
+
export async function installMaki(
|
|
189
|
+
options: InstallMakiOptions = {},
|
|
190
|
+
): Promise<InstallMakiResult> {
|
|
191
|
+
const version = effectiveMakiVersion(options.version);
|
|
192
|
+
const binaryName = makiBinaryName();
|
|
193
|
+
const target = join(makiInstallDir(version), binaryName);
|
|
194
|
+
const log = options.log ?? (() => {});
|
|
195
|
+
|
|
196
|
+
if (existsSync(target) && !options.force) {
|
|
197
|
+
writeInstallManifest({ version, path: target, installedAt: systemClock() });
|
|
198
|
+
return { version, path: target, skipped: true };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const asset = makiAssetFor(process.platform, process.arch, version);
|
|
202
|
+
if (!asset) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Sem release do maki para ${process.platform}/${process.arch}. ` +
|
|
205
|
+
"Instale manualmente (https://github.com/tontinton/maki/releases) e defina HOUNDSMAN_MAKI_PATH.",
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const temp = mkdtempSync(join(tmpdir(), "houndsman-maki-"));
|
|
210
|
+
try {
|
|
211
|
+
let archivePath: string;
|
|
212
|
+
let checksum: string | undefined;
|
|
213
|
+
|
|
214
|
+
if (options.from) {
|
|
215
|
+
archivePath = resolve(options.from);
|
|
216
|
+
if (!existsSync(archivePath)) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Arquivo do maki não encontrado: ${displayPath(archivePath)}.`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
log(`Extraindo ${displayPath(archivePath)}...`);
|
|
222
|
+
} else {
|
|
223
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
224
|
+
const url = makiDownloadUrl(version, asset.name);
|
|
225
|
+
log(`Baixando maki v${version} (${asset.name})...`);
|
|
226
|
+
const response = await fetchImpl(url);
|
|
227
|
+
if (!response.ok) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`Falha ao baixar o maki (HTTP ${response.status}): ${url}`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
233
|
+
|
|
234
|
+
const checksumsResponse = await fetchImpl(makiChecksumsUrl(version));
|
|
235
|
+
if (checksumsResponse.ok) {
|
|
236
|
+
const expected = checksumFor(await checksumsResponse.text(), asset.name);
|
|
237
|
+
if (expected) {
|
|
238
|
+
const actual = createHash("sha256").update(bytes).digest("hex");
|
|
239
|
+
if (actual !== expected) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`Checksum inválido para ${asset.name} (esperado ${expected.slice(0, 12)}…, obtido ${actual.slice(0, 12)}…).`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
checksum = expected;
|
|
245
|
+
log("Checksum sha256 verificado.");
|
|
246
|
+
} else {
|
|
247
|
+
log(
|
|
248
|
+
"Aviso: release sem checksum para o asset; seguindo sem verificação.",
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
log(
|
|
253
|
+
`Aviso: não foi possível obter sha256sums.txt (HTTP ${checksumsResponse.status}).`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
archivePath = join(temp, asset.name);
|
|
258
|
+
writeFileSync(archivePath, bytes);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const extractDir = join(temp, "extract");
|
|
262
|
+
mkdirSync(extractDir, { recursive: true });
|
|
263
|
+
extractArchive(archivePath, extractDir);
|
|
264
|
+
const found = findFile(extractDir, binaryName);
|
|
265
|
+
if (!found) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`O pacote do maki não contém '${binaryName}'. Conteúdo inesperado em ${basename(archivePath)}.`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const installDir = makiInstallDir(version);
|
|
272
|
+
mkdirSync(installDir, { recursive: true });
|
|
273
|
+
copyFileSync(found, target);
|
|
274
|
+
chmodSync(target, 0o755);
|
|
275
|
+
writeInstallManifest({
|
|
276
|
+
version,
|
|
277
|
+
path: target,
|
|
278
|
+
asset: asset.name,
|
|
279
|
+
sha256: checksum,
|
|
280
|
+
installedAt: systemClock(),
|
|
281
|
+
});
|
|
282
|
+
return { version, path: target, skipped: false };
|
|
283
|
+
} finally {
|
|
284
|
+
rmSync(temp, { recursive: true, force: true });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Remove todas as versões gerenciadas; devolve o caminho removido (ou null). */
|
|
289
|
+
export function uninstallMaki(): string | null {
|
|
290
|
+
const root = makiToolsRoot();
|
|
291
|
+
if (!existsSync(root)) return null;
|
|
292
|
+
rmSync(root, { recursive: true, force: true });
|
|
293
|
+
return root;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Extrai `.tar.gz`/`.zip` (Windows usa o `tar.exe`/bsdtar do sistema). */
|
|
297
|
+
function extractArchive(archive: string, directory: string): void {
|
|
298
|
+
try {
|
|
299
|
+
const result = Bun.spawnSync(["tar", "-xf", archive, "-C", directory], {
|
|
300
|
+
stdout: "pipe",
|
|
301
|
+
stderr: "pipe",
|
|
302
|
+
});
|
|
303
|
+
if (result.exitCode !== 0) {
|
|
304
|
+
const detail = result.stderr.toString().trim();
|
|
305
|
+
throw new Error(
|
|
306
|
+
`Falha ao extrair ${basename(archive)}${detail ? `: ${detail}` : "."}`,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (error instanceof Error && error.message.startsWith("Falha ao extrair")) {
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
throw new Error(
|
|
314
|
+
`Falha ao extrair ${basename(archive)}: 'tar' indisponível no sistema.`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Procura um arquivo por nome, com profundidade limitada. */
|
|
320
|
+
function findFile(root: string, name: string, depth = 0): string | null {
|
|
321
|
+
if (depth > 4) return null;
|
|
322
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
323
|
+
const full = join(root, entry.name);
|
|
324
|
+
if (entry.isFile() && entry.name === name) return full;
|
|
325
|
+
if (entry.isDirectory()) {
|
|
326
|
+
const found = findFile(full, name, depth + 1);
|
|
327
|
+
if (found) return found;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolução do modelo/credencial do maki (harness local).
|
|
3
|
+
*
|
|
4
|
+
* O houndsman reaproveita a config de IA (`ai.provider`/`ai.model`) e a env já
|
|
5
|
+
* preenchida (`HOUNDSMAN_DEEP_SEEK_API_KEY` etc.). O maki lê o nome nativo do
|
|
6
|
+
* provider (`DEEPSEEK_API_KEY`); a chave é injetada só no processo filho.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AiProviderKind } from "../config";
|
|
10
|
+
import { ENV_NAMES, env } from "../env";
|
|
11
|
+
import type { ProjectConfig } from "../project-config";
|
|
12
|
+
|
|
13
|
+
/** Modelo e credencial no formato esperado pelo maki. */
|
|
14
|
+
export type MakiModel = {
|
|
15
|
+
/** Spec `provider/model-id` passada em `--model`. */
|
|
16
|
+
spec: string;
|
|
17
|
+
provider: AiProviderKind;
|
|
18
|
+
/** Variável nativa que o maki lê (ex.: `DEEPSEEK_API_KEY`). */
|
|
19
|
+
apiKeyEnv: string;
|
|
20
|
+
/** Chave do houndsman (pode faltar; `requireMakiModel` exige). */
|
|
21
|
+
apiKey?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Env nativa do maki por provider. */
|
|
25
|
+
export const MAKI_API_KEY_ENV: Record<AiProviderKind, string> = {
|
|
26
|
+
deepseek: "DEEPSEEK_API_KEY",
|
|
27
|
+
openai: "OPENAI_API_KEY",
|
|
28
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Chave do houndsman por provider (para mensagens acionáveis). */
|
|
32
|
+
const HOUNDSMAN_KEY_ENV: Record<AiProviderKind, string> = {
|
|
33
|
+
deepseek: ENV_NAMES.deepSeekApiKey,
|
|
34
|
+
openai: ENV_NAMES.openAiApiKey,
|
|
35
|
+
anthropic: ENV_NAMES.anthropicApiKey,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Nome da env do houndsman que guarda a chave do provider. */
|
|
39
|
+
export function houndsmanKeyEnvFor(provider: AiProviderKind): string {
|
|
40
|
+
return HOUNDSMAN_KEY_ENV[provider];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `true` para um slug de provider suportado. */
|
|
44
|
+
function isAiProvider(value: string): value is AiProviderKind {
|
|
45
|
+
return value === "deepseek" || value === "openai" || value === "anthropic";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Chave do houndsman para o provider. */
|
|
49
|
+
function providerKey(provider: AiProviderKind): string | undefined {
|
|
50
|
+
switch (provider) {
|
|
51
|
+
case "deepseek":
|
|
52
|
+
return env.deepSeekApiKey;
|
|
53
|
+
case "openai":
|
|
54
|
+
return env.openAiApiKey;
|
|
55
|
+
case "anthropic":
|
|
56
|
+
return env.anthropicApiKey;
|
|
57
|
+
default:
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve o modelo: override → `hunt.model` → `ai.model`.
|
|
64
|
+
*
|
|
65
|
+
* Um prefixo reconhecido (`openai/...`, `anthropic/...`) troca o provider do
|
|
66
|
+
* harness e, junto, a variável de credencial usada.
|
|
67
|
+
*/
|
|
68
|
+
export function resolveMakiModel(project: ProjectConfig, override?: string): MakiModel {
|
|
69
|
+
const configured = override?.trim() || project.huntModel;
|
|
70
|
+
let provider = project.aiProvider;
|
|
71
|
+
let modelId = configured;
|
|
72
|
+
|
|
73
|
+
const [prefix, ...tail] = configured.split("/");
|
|
74
|
+
if (tail.length > 0 && isAiProvider(prefix)) {
|
|
75
|
+
provider = prefix;
|
|
76
|
+
modelId = tail.join("/");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const spec = `${provider}/${modelId}`;
|
|
80
|
+
return {
|
|
81
|
+
spec,
|
|
82
|
+
provider,
|
|
83
|
+
apiKeyEnv: MAKI_API_KEY_ENV[provider],
|
|
84
|
+
apiKey: providerKey(provider),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Como `resolveMakiModel`, mas lança quando a credencial falta. */
|
|
89
|
+
export function requireMakiModel(
|
|
90
|
+
project: ProjectConfig,
|
|
91
|
+
override?: string,
|
|
92
|
+
): MakiModel & { apiKey: string } {
|
|
93
|
+
const model = resolveMakiModel(project, override);
|
|
94
|
+
if (!model.apiKey) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Harness local sem credencial: defina ${HOUNDSMAN_KEY_ENV[model.provider]} ` +
|
|
97
|
+
`(o maki usa ${model.apiKeyEnv}).`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return { ...model, apiKey: model.apiKey };
|
|
101
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt do watchdog no harness local.
|
|
3
|
+
*
|
|
4
|
+
* O brief original manda gravar o JSON em `findings/<id>.json`. No maki o
|
|
5
|
+
* watchdog **não** recebe permissão de escrita: ele devolve o JSON na resposta
|
|
6
|
+
* final e o houndsman valida e grava. Este módulo só acrescenta o contrato;
|
|
7
|
+
* não altera o brief usado pela skill `/houndsman hunt`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Dados do prompt de um watchdog. */
|
|
11
|
+
export type HuntPromptInput = {
|
|
12
|
+
projectName: string;
|
|
13
|
+
watchdogId: string;
|
|
14
|
+
/** Conteúdo do brief renderizado pelo prepare. */
|
|
15
|
+
brief: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Schema compacto repetido no prompt (evita depender de arquivo externo). */
|
|
19
|
+
function outputSchema(watchdogId: string): string {
|
|
20
|
+
return `{
|
|
21
|
+
"watchdogId": "${watchdogId}",
|
|
22
|
+
"reviewedFiles": ["caminho/relativo.ts"],
|
|
23
|
+
"skipped": ["arquivo não lido + motivo"],
|
|
24
|
+
"findings": [
|
|
25
|
+
{
|
|
26
|
+
"rule": "regra do brief",
|
|
27
|
+
"severity": "blocking | attention | suggestion | nit",
|
|
28
|
+
"file": "caminho/relativo.ts",
|
|
29
|
+
"line": 123,
|
|
30
|
+
"message": "o que está errado",
|
|
31
|
+
"action": "o que fazer",
|
|
32
|
+
"evidence": "trecho que comprova"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Monta o prompt enviado ao maki para um brief. */
|
|
39
|
+
export function renderHuntPrompt(input: HuntPromptInput): string {
|
|
40
|
+
return [
|
|
41
|
+
`Você é o watchdog \`${input.watchdogId}\` do projeto ${input.projectName}.`,
|
|
42
|
+
"Execute o brief abaixo e devolva os achados como JSON.",
|
|
43
|
+
"",
|
|
44
|
+
"## Contrato do harness local",
|
|
45
|
+
"",
|
|
46
|
+
"- Use apenas leitura e busca; **não escreva arquivos** e não rode comandos.",
|
|
47
|
+
"- Ignore a instrução do brief sobre gravar `outputPath`: o houndsman grava por você.",
|
|
48
|
+
"- Responda **somente** com um objeto JSON válido (sem markdown, sem texto antes/depois).",
|
|
49
|
+
"- `watchdogId` deve ser exatamente o id acima; use `findings: []` quando não houver achado.",
|
|
50
|
+
"- `line` deve estar dentro do escopo do diff descrito no brief.",
|
|
51
|
+
"",
|
|
52
|
+
"### Schema",
|
|
53
|
+
"",
|
|
54
|
+
outputSchema(input.watchdogId),
|
|
55
|
+
"",
|
|
56
|
+
"## Brief",
|
|
57
|
+
"",
|
|
58
|
+
input.brief.trim(),
|
|
59
|
+
].join("\n");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Sufixo corretivo das retentativas (saída inválida). */
|
|
63
|
+
export function renderRetrySuffix(error: string): string {
|
|
64
|
+
return [
|
|
65
|
+
"",
|
|
66
|
+
"## Correção obrigatória",
|
|
67
|
+
"",
|
|
68
|
+
`A resposta anterior foi rejeitada: ${error}`,
|
|
69
|
+
"Responda APENAS com o objeto JSON completo, no schema acima, sem markdown.",
|
|
70
|
+
].join("\n");
|
|
71
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Releases do maki (harness local) — resolução pura de versão, asset e checksum.
|
|
3
|
+
*
|
|
4
|
+
* O houndsman fixa uma versão testada (`DEFAULT_MAKI_VERSION`) e baixa o binário
|
|
5
|
+
* do GitHub Releases. Nada aqui acessa rede: são funções puras para teste.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { DEFAULT_MAKI_VERSION } from "../defaults";
|
|
9
|
+
import { env } from "../env";
|
|
10
|
+
|
|
11
|
+
/** Repositório oficial do maki (binários por plataforma). */
|
|
12
|
+
export const MAKI_REPO = "tontinton/maki";
|
|
13
|
+
|
|
14
|
+
/** Base dos downloads de release. */
|
|
15
|
+
export const MAKI_RELEASE_BASE = `https://github.com/${MAKI_REPO}/releases/download`;
|
|
16
|
+
|
|
17
|
+
/** Asset de release para uma combinação plataforma/arquitetura. */
|
|
18
|
+
export type MakiAsset = {
|
|
19
|
+
/** Nome do arquivo no release. */
|
|
20
|
+
name: string;
|
|
21
|
+
/** Formato de compressão (só informativo; a extração usa `tar`). */
|
|
22
|
+
format: "tar.gz" | "zip";
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Assets publicados por plataforma/arquitetura. */
|
|
26
|
+
const MAKI_ASSETS: Record<string, MakiAsset> = {
|
|
27
|
+
"win32-x64": { name: "maki-v{version}-x86_64-pc-windows-msvc.zip", format: "zip" },
|
|
28
|
+
"darwin-x64": {
|
|
29
|
+
name: "maki-v{version}-x86_64-apple-darwin.tar.gz",
|
|
30
|
+
format: "tar.gz",
|
|
31
|
+
},
|
|
32
|
+
"darwin-arm64": {
|
|
33
|
+
name: "maki-v{version}-aarch64-apple-darwin.tar.gz",
|
|
34
|
+
format: "tar.gz",
|
|
35
|
+
},
|
|
36
|
+
"linux-x64": {
|
|
37
|
+
name: "maki-v{version}-x86_64-unknown-linux-musl.tar.gz",
|
|
38
|
+
format: "tar.gz",
|
|
39
|
+
},
|
|
40
|
+
"linux-arm64": {
|
|
41
|
+
name: "maki-v{version}-aarch64-unknown-linux-musl.tar.gz",
|
|
42
|
+
format: "tar.gz",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Nome do executável do maki na plataforma atual. */
|
|
47
|
+
export function makiBinaryName(platform: string = process.platform): string {
|
|
48
|
+
return platform === "win32" ? "maki.exe" : "maki";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Normaliza a versão (`v0.6.5` → `0.6.5`) e valida os caracteres. */
|
|
52
|
+
export function normalizeMakiVersion(value: string): string {
|
|
53
|
+
const normalized = value.trim().replace(/^v/i, "");
|
|
54
|
+
if (!/^[0-9A-Za-z][0-9A-Za-z._-]*$/.test(normalized)) {
|
|
55
|
+
throw new Error(`Versão do maki inválida: '${value}'.`);
|
|
56
|
+
}
|
|
57
|
+
return normalized;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Versão efetiva: flag/env (`HOUNDSMAN_MAKI_VERSION`) ou a fixada no pacote. */
|
|
61
|
+
export function effectiveMakiVersion(override?: string): string {
|
|
62
|
+
const value = override?.trim() || env.makiVersion || DEFAULT_MAKI_VERSION;
|
|
63
|
+
return normalizeMakiVersion(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Asset da plataforma/arquitetura informada, ou `null` se não houver release. */
|
|
67
|
+
export function makiAssetFor(
|
|
68
|
+
platform: string,
|
|
69
|
+
arch: string,
|
|
70
|
+
version: string,
|
|
71
|
+
): MakiAsset | null {
|
|
72
|
+
const asset = MAKI_ASSETS[`${platform}-${arch}`];
|
|
73
|
+
if (!asset) return null;
|
|
74
|
+
return {
|
|
75
|
+
name: asset.name.replace("{version}", normalizeMakiVersion(version)),
|
|
76
|
+
format: asset.format,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** URL do asset no GitHub Releases. */
|
|
81
|
+
export function makiDownloadUrl(version: string, asset: string): string {
|
|
82
|
+
return `${MAKI_RELEASE_BASE}/v${normalizeMakiVersion(version)}/${asset}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** URL do arquivo de checksums do release. */
|
|
86
|
+
export function makiChecksumsUrl(version: string): string {
|
|
87
|
+
return `${MAKI_RELEASE_BASE}/v${normalizeMakiVersion(version)}/sha256sums.txt`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Converte o `sha256sums.txt` (`<hash> <arquivo>`) em mapa. */
|
|
91
|
+
export function parseChecksums(text: string): Map<string, string> {
|
|
92
|
+
const checksums = new Map<string, string>();
|
|
93
|
+
for (const line of text.split(/\r?\n/)) {
|
|
94
|
+
const match = line.trim().match(/^([0-9a-f]{64})\s+\*?(.+)$/i);
|
|
95
|
+
if (!match) continue;
|
|
96
|
+
checksums.set(match[2].trim(), match[1].toLowerCase());
|
|
97
|
+
}
|
|
98
|
+
return checksums;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Checksum esperado de um asset (ou `undefined` se o release não listar). */
|
|
102
|
+
export function checksumFor(checksumsText: string, asset: string): string | undefined {
|
|
103
|
+
return parseChecksums(checksumsText).get(asset);
|
|
104
|
+
}
|