@payconductor/houndsman 0.10.0 → 0.12.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 +4 -19
- package/package.json +1 -1
- package/src/cli/help.ts +0 -7
- package/src/cli/index.ts +9 -3
- package/src/core/ai/ai-client.ts +3 -7
- package/src/core/ai/ai-sdk-model-provider.ts +4 -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 +0 -3
- package/src/core/ai/prompts/base.ts +7 -0
- package/src/core/ai/prompts/context.ts +2 -1
- package/src/core/ai/prompts/pre-analysis.ts +3 -2
- package/src/core/ai/prompts/technical.ts +2 -1
- package/src/core/ai/schemas.ts +1 -23
- package/src/core/ai/types.ts +2 -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 +1 -18
- package/src/core/config-loader.ts +18 -4
- package/src/core/config.ts +2 -15
- package/src/core/defaults.ts +0 -6
- package/src/core/engine/review-scope.ts +55 -8
- package/src/core/engine/review-workspace.ts +6 -2
- package/src/core/jev/jev-detector.ts +7 -2
- package/src/core/logging/index.ts +1 -1
- package/src/core/logging/logger.ts +3 -3
- package/src/core/orchestrator.ts +19 -14
- package/src/core/project-config.ts +0 -28
- 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/services/worktree-manager.ts +61 -2
- package/src/core/storage/types.ts +0 -1
- package/src/core/types.ts +3 -14
- package/src/index.ts +25 -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 +2 -1
- package/src/skill/SETUP.md +1 -1
- package/src/skill/fix.md +1 -1
- package/src/skill/hunt.md +2 -6
- package/src/tests/__snapshots__/baseline.test.ts.snap +50 -32
- package/src/tests/ai-features.test.ts +11 -0
- package/src/tests/changed-scope.test.ts +197 -0
- package/src/tests/fallback.test.ts +1 -96
- package/src/tests/fixtures/fake-model-provider.ts +0 -1
- package/src/tests/review-scope.test.ts +33 -0
- package/src/tests/worktree.test.ts +72 -4
- package/src/cli/commands/hunt-command.ts +0 -51
- 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
|
@@ -282,12 +282,7 @@ function checkAiConfig(
|
|
|
282
282
|
if (ai.enabled !== false && ai.model !== undefined && !ai.model.trim()) {
|
|
283
283
|
warnings.push("ai.model vazio — usando o default.");
|
|
284
284
|
}
|
|
285
|
-
for (const key of [
|
|
286
|
-
"concurrency",
|
|
287
|
-
"maxSteps",
|
|
288
|
-
"maxOutputTokens",
|
|
289
|
-
"timeoutMs",
|
|
290
|
-
] as const) {
|
|
285
|
+
for (const key of ["concurrency", "timeoutMs"] as const) {
|
|
291
286
|
const value = ai[key];
|
|
292
287
|
if (value !== undefined && value <= 0) {
|
|
293
288
|
warnings.push(`ai.${key} = ${value} — não-positivo; usando o default.`);
|
|
@@ -303,15 +298,10 @@ function checkAiConfig(
|
|
|
303
298
|
`ai.maxTokensPerReview = ${ai.maxTokensPerReview} — negativo; sem teto.`,
|
|
304
299
|
);
|
|
305
300
|
}
|
|
306
|
-
for (const
|
|
301
|
+
for (const id of Object.keys(ai.watchdogs ?? {})) {
|
|
307
302
|
if (!config.watchdogIds.includes(id)) {
|
|
308
303
|
warnings.push(`ai.watchdogs['${id}'] não corresponde a nenhum watchdog.`);
|
|
309
304
|
}
|
|
310
|
-
if (override.maxSteps !== undefined && override.maxSteps <= 0) {
|
|
311
|
-
warnings.push(
|
|
312
|
-
`ai.watchdogs['${id}'].maxSteps não-positivo — usando o default.`,
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
305
|
}
|
|
316
306
|
|
|
317
307
|
const provider = ai.provider ?? "deepseek";
|
package/src/core/rule-text.ts
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Texto de regra (biblioteca) — fonte única de polaridade
|
|
2
|
+
* Texto de regra (biblioteca) — fonte única dos prefixos de polaridade.
|
|
3
3
|
*
|
|
4
|
-
* `
|
|
5
|
-
*
|
|
6
|
-
* Também define o vocabulário de findings emergentes (lógica/domínio).
|
|
4
|
+
* `registry-check` usa a mesma lista para validar que a regra declara
|
|
5
|
+
* “Promover/Evitar” explicitamente.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
|
-
/** Intenção de uma regra. */
|
|
10
|
-
export type RuleIntent = "promote" | "avoid";
|
|
11
|
-
|
|
12
8
|
/** Palavras que abrem uma regra de “deve acontecer”. */
|
|
13
9
|
export const PROMOTE_PREFIXES = [
|
|
14
10
|
"promover",
|
|
@@ -29,12 +25,6 @@ export const AVOID_PREFIXES = [
|
|
|
29
25
|
"jamais",
|
|
30
26
|
] as const;
|
|
31
27
|
|
|
32
|
-
/** Conjunto normalizado (usado para remover polaridade das keywords). */
|
|
33
|
-
export const POLARITY_WORDS: ReadonlySet<string> = new Set([
|
|
34
|
-
...PROMOTE_PREFIXES,
|
|
35
|
-
...AVOID_PREFIXES,
|
|
36
|
-
]);
|
|
37
|
-
|
|
38
28
|
/** Escapa um literal para uso em regex. */
|
|
39
29
|
function escapeWord(word: string): string {
|
|
40
30
|
return word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -48,50 +38,8 @@ function patternFor(words: readonly string[]): RegExp {
|
|
|
48
38
|
const PROMOTE_PATTERN = patternFor(PROMOTE_PREFIXES);
|
|
49
39
|
const AVOID_PATTERN = patternFor(AVOID_PREFIXES);
|
|
50
40
|
|
|
51
|
-
/** Polaridade explícita; sem prefixo conhecido, assume `avoid`. */
|
|
52
|
-
export function polarityOf(text: string): RuleIntent {
|
|
53
|
-
const normalized = text.trim().toLowerCase();
|
|
54
|
-
if (PROMOTE_PATTERN.test(normalized)) return "promote";
|
|
55
|
-
if (AVOID_PATTERN.test(normalized)) return "avoid";
|
|
56
|
-
return "avoid";
|
|
57
|
-
}
|
|
58
|
-
|
|
59
41
|
/** `true` quando a regra declara polaridade explícita. */
|
|
60
42
|
export function hasExplicitPolarity(text: string): boolean {
|
|
61
43
|
const normalized = text.trim().toLowerCase();
|
|
62
44
|
return PROMOTE_PATTERN.test(normalized) || AVOID_PATTERN.test(normalized);
|
|
63
45
|
}
|
|
64
|
-
|
|
65
|
-
/** Prefixos reservados para findings emergentes (fora das regras explícitas). */
|
|
66
|
-
export const EMERGENT_PREFIXES = {
|
|
67
|
-
logic: "Lógica:",
|
|
68
|
-
domain: "Domínio:",
|
|
69
|
-
} as const;
|
|
70
|
-
|
|
71
|
-
/** Espécie de finding emergente. */
|
|
72
|
-
export type EmergentKind = keyof typeof EMERGENT_PREFIXES;
|
|
73
|
-
|
|
74
|
-
/** Espécie de um finding emergente (`null` quando não é emergente). */
|
|
75
|
-
export function emergentKind(rule: string): EmergentKind | null {
|
|
76
|
-
const normalized = rule.trim();
|
|
77
|
-
if (normalized.startsWith(EMERGENT_PREFIXES.logic)) return "logic";
|
|
78
|
-
if (normalized.startsWith(EMERGENT_PREFIXES.domain)) return "domain";
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** `true` quando a regra usa um prefixo emergente. */
|
|
83
|
-
export function isEmergentRule(rule: string): boolean {
|
|
84
|
-
return emergentKind(rule) !== null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Texto que sobra após o prefixo (vazio é inválido). */
|
|
88
|
-
export function emergentText(rule: string): string {
|
|
89
|
-
const kind = emergentKind(rule);
|
|
90
|
-
if (!kind) return "";
|
|
91
|
-
return rule.trim().slice(EMERGENT_PREFIXES[kind].length).trim();
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Monta o texto de um finding emergente. */
|
|
95
|
-
export function emergentRule(kind: EmergentKind, text: string): string {
|
|
96
|
-
return `${EMERGENT_PREFIXES[kind]} ${text.trim()}`;
|
|
97
|
-
}
|
|
@@ -138,6 +138,14 @@ export class GitRepository {
|
|
|
138
138
|
return parseNumstat(this.run(["diff", "--numstat", "-z", range]));
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Patch com contexto zero (`git diff -U0 range`), sem truncar por arquivo.
|
|
143
|
+
* É compacto e exato nas linhas — base do escopo do diff e do delta AST.
|
|
144
|
+
*/
|
|
145
|
+
patchZeroContext(range: string): Map<string, string> {
|
|
146
|
+
return parseDiffIndex(this.run(["diff", "-U0", range]));
|
|
147
|
+
}
|
|
148
|
+
|
|
141
149
|
/** Patch por arquivo de um intervalo, opcionalmente restrito a `paths`. */
|
|
142
150
|
diffPatch(
|
|
143
151
|
range: string,
|
|
@@ -10,12 +10,17 @@
|
|
|
10
10
|
* truncado porque o checkout de uma PR herda a profundidade dos arquivos do
|
|
11
11
|
* projeto, e o Windows (MAX_PATH = 260) estoura "Filename too long" com
|
|
12
12
|
* repo+branch longos. O hash mantém o nome determinístico e único.
|
|
13
|
+
*
|
|
14
|
+
* Cada worktree recebe o espelho da camada gerada (`.houndsman/core`) do clone
|
|
15
|
+
* principal — o checkout não traz arquivos ignorados e, sem ela, a config do
|
|
16
|
+
* projeto não importa (`../core/types`).
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
19
|
import { createHash } from "node:crypto";
|
|
16
|
-
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
17
21
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
18
22
|
import { createGitRunner } from "../git-runner";
|
|
23
|
+
import { CORE_DIR } from "../paths";
|
|
19
24
|
import { sanitizeBranch } from "../report-builder";
|
|
20
25
|
import { worktreesRoot } from "../storage/paths";
|
|
21
26
|
import { GitRepository } from "./git-repository";
|
|
@@ -59,7 +64,7 @@ export type RemoveWorktreeResult = {
|
|
|
59
64
|
/** Opções do gerenciador. */
|
|
60
65
|
export type WorktreeManagerOptions = {
|
|
61
66
|
repoKey: string;
|
|
62
|
-
/** Raiz do clone principal. */
|
|
67
|
+
/** Raiz do clone principal (fonte da camada gerada espelhada). */
|
|
63
68
|
repoRoot: string;
|
|
64
69
|
/**
|
|
65
70
|
* Raiz onde os diretórios por repositório são criados.
|
|
@@ -112,6 +117,54 @@ function isInside(root: string, path: string): boolean {
|
|
|
112
117
|
return rel === "" || (!rel.startsWith("..") && !rel.split(sep).includes(".."));
|
|
113
118
|
}
|
|
114
119
|
|
|
120
|
+
/** Raiz do clone principal a partir do diretório git comum (ou `null`). */
|
|
121
|
+
function mainRootOf(git: GitRepository): string | null {
|
|
122
|
+
const common = git.gitCommonDir();
|
|
123
|
+
if (!common) return null;
|
|
124
|
+
const gitDir = resolve(common);
|
|
125
|
+
return basename(gitDir).toLowerCase() === ".git" ? dirname(gitDir) : null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Espelha a camada gerada (`.houndsman/core`) do clone principal no worktree.
|
|
130
|
+
*
|
|
131
|
+
* O checkout de uma PR só traz arquivos versionados e `core/` é gerado (e
|
|
132
|
+
* ignorado pelo git). Sem ele, qualquer comando dentro do worktree falha ao
|
|
133
|
+
* importar `../core/types` a partir dos watchdogs. Fail-open: nunca bloqueia
|
|
134
|
+
* a revisão.
|
|
135
|
+
*/
|
|
136
|
+
export function syncProjectLayer(mainRoot: string, worktreeRoot: string): boolean {
|
|
137
|
+
const source = join(mainRoot, CORE_DIR);
|
|
138
|
+
const dest = join(worktreeRoot, CORE_DIR);
|
|
139
|
+
if (normalizePath(source) === normalizePath(dest)) return false;
|
|
140
|
+
if (!existsSync(source)) return false;
|
|
141
|
+
try {
|
|
142
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
143
|
+
cpSync(source, dest, { recursive: true, force: true });
|
|
144
|
+
return true;
|
|
145
|
+
} catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Auto-reparo para comandos executados **dentro** de um worktree: repõe a
|
|
152
|
+
* camada gerada a partir do clone principal quando ela não existe ali. No
|
|
153
|
+
* clone principal (camada presente) é no-op. Fail-open.
|
|
154
|
+
*/
|
|
155
|
+
export function syncProjectLayerForRoot(root: string): boolean {
|
|
156
|
+
try {
|
|
157
|
+
const git = new GitRepository(createGitRunner(root));
|
|
158
|
+
const gitRoot = git.rootPath();
|
|
159
|
+
if (!gitRoot || existsSync(join(gitRoot, CORE_DIR))) return false;
|
|
160
|
+
const mainRoot = mainRootOf(git);
|
|
161
|
+
if (!mainRoot) return false;
|
|
162
|
+
return syncProjectLayer(mainRoot, gitRoot);
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
115
168
|
/** Fatia a saída de `git worktree list --porcelain` em entradas. */
|
|
116
169
|
export function parseWorktreeList(raw: string): WorktreeEntry[] {
|
|
117
170
|
const entries: WorktreeEntry[] = [];
|
|
@@ -150,11 +203,13 @@ export function parseWorktreeList(raw: string): WorktreeEntry[] {
|
|
|
150
203
|
export class WorktreeManager {
|
|
151
204
|
private readonly git: GitRepository;
|
|
152
205
|
private readonly repoKey: string;
|
|
206
|
+
private readonly repoRoot: string;
|
|
153
207
|
private readonly rootDir: string;
|
|
154
208
|
|
|
155
209
|
constructor(git: GitRepository, options: WorktreeManagerOptions) {
|
|
156
210
|
this.git = git;
|
|
157
211
|
this.repoKey = options.repoKey;
|
|
212
|
+
this.repoRoot = options.repoRoot;
|
|
158
213
|
this.rootDir = join(
|
|
159
214
|
options.baseDir ?? worktreesRoot(),
|
|
160
215
|
repoSlug(options.repoKey),
|
|
@@ -214,6 +269,7 @@ export class WorktreeManager {
|
|
|
214
269
|
|
|
215
270
|
if (existing) {
|
|
216
271
|
if (existing.head === target) {
|
|
272
|
+
syncProjectLayer(this.repoRoot, path);
|
|
217
273
|
return { path, head: target, created: false, reused: true };
|
|
218
274
|
}
|
|
219
275
|
const checkout = new GitRepository(createGitRunner(path)).tryRun([
|
|
@@ -222,6 +278,7 @@ export class WorktreeManager {
|
|
|
222
278
|
target,
|
|
223
279
|
]);
|
|
224
280
|
if (checkout.ok) {
|
|
281
|
+
syncProjectLayer(this.repoRoot, path);
|
|
225
282
|
return { path, head: target, created: false, reused: true };
|
|
226
283
|
}
|
|
227
284
|
// Worktree quebrado/inacessível: remove e recria do zero.
|
|
@@ -251,6 +308,8 @@ export class WorktreeManager {
|
|
|
251
308
|
);
|
|
252
309
|
}
|
|
253
310
|
}
|
|
311
|
+
// A camada gerada é ignorada pelo git e não vem no checkout da PR.
|
|
312
|
+
syncProjectLayer(this.repoRoot, path);
|
|
254
313
|
return { path, head: target, created: true, reused: false };
|
|
255
314
|
}
|
|
256
315
|
|
package/src/core/types.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type { BriefPlan } from "./ai/schemas";
|
|
10
10
|
import type { AstFileBrief } from "./ast/types";
|
|
11
|
+
import type { ChangedScope } from "./changed-scope";
|
|
11
12
|
import type { JevRuleSpec } from "./jev/jev-types";
|
|
12
13
|
|
|
13
14
|
/** Severidade de um achado. `blocking` bloqueia o merge. */
|
|
@@ -29,19 +30,6 @@ export const SEVERITY_EMOJI: Record<Severity, string> = {
|
|
|
29
30
|
nit: "🔎",
|
|
30
31
|
};
|
|
31
32
|
|
|
32
|
-
/**
|
|
33
|
-
* Sinais opcionais de uma regra: ajudam o `RuleAnalyzer` a transformar texto
|
|
34
|
-
* em checagens concretas (símbolos, imports, decorators, paths e tools).
|
|
35
|
-
*/
|
|
36
|
-
export type RuleHints = {
|
|
37
|
-
symbols?: string[];
|
|
38
|
-
imports?: string[];
|
|
39
|
-
decorators?: string[];
|
|
40
|
-
paths?: string[];
|
|
41
|
-
/** Nomes de tools sugeridos (ex.: `symbol_context`). */
|
|
42
|
-
checks?: string[];
|
|
43
|
-
};
|
|
44
|
-
|
|
45
33
|
/**
|
|
46
34
|
* Regra do watchdog: severidade + texto. O texto declara a polaridade de forma
|
|
47
35
|
* explícita — `Promover:` (algo que DEVE acontecer) ou `Evitar:`/`Não`/`Nunca`
|
|
@@ -54,7 +42,6 @@ export type Rule = {
|
|
|
54
42
|
severity: Severity;
|
|
55
43
|
text: string;
|
|
56
44
|
jev?: JevRuleSpec;
|
|
57
|
-
hints?: RuleHints;
|
|
58
45
|
};
|
|
59
46
|
|
|
60
47
|
/**
|
|
@@ -144,6 +131,8 @@ export type WatchdogRenderArgs = {
|
|
|
144
131
|
ast?: readonly AstFileBrief[];
|
|
145
132
|
/** Plano de revisão sugerido por IA (nunca substitui as regras). */
|
|
146
133
|
aiPlan?: BriefPlan;
|
|
134
|
+
/** Escopo do diff por arquivo (símbolos alterados; hunks como fallback). */
|
|
135
|
+
changedScope?: ChangedScope;
|
|
147
136
|
};
|
|
148
137
|
|
|
149
138
|
/** Definição completa de um watchdog — 1 por arquivo em `project/watchdogs/`. */
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
|
|
8
8
|
export type {
|
|
9
9
|
AiResult,
|
|
10
|
-
AiTool,
|
|
11
10
|
AiUsage,
|
|
12
11
|
ModelProvider,
|
|
13
12
|
ModelRequest,
|
|
@@ -26,7 +25,14 @@ export {
|
|
|
26
25
|
} from "./core/ai";
|
|
27
26
|
export { ArgParser } from "./core/arg-parser";
|
|
28
27
|
export { assetPath, OUTPUT_FORMAT_FILE } from "./core/assets";
|
|
29
|
-
export {
|
|
28
|
+
export {
|
|
29
|
+
changedAnchorLines,
|
|
30
|
+
changedAnchors,
|
|
31
|
+
changedNewLines,
|
|
32
|
+
computeDelta,
|
|
33
|
+
emptyDelta,
|
|
34
|
+
parseHunks,
|
|
35
|
+
} from "./core/ast/diff";
|
|
30
36
|
export { buildImpact, resolveImport } from "./core/ast/impact";
|
|
31
37
|
export { AstParser, languageForPath } from "./core/ast/parser";
|
|
32
38
|
export { AstService } from "./core/ast/service";
|
|
@@ -44,6 +50,23 @@ export type {
|
|
|
44
50
|
AstSymbolKind,
|
|
45
51
|
} from "./core/ast/types";
|
|
46
52
|
export { defaultRenderBrief, PROCEDURE, renderBrief } from "./core/brief";
|
|
53
|
+
export type {
|
|
54
|
+
BuildChangedScopeInput,
|
|
55
|
+
ChangedScope,
|
|
56
|
+
ChangedScopeSource,
|
|
57
|
+
ChangedSymbol,
|
|
58
|
+
FileChangedScope,
|
|
59
|
+
LineRange,
|
|
60
|
+
ScopeEntry,
|
|
61
|
+
} from "./core/changed-scope";
|
|
62
|
+
export {
|
|
63
|
+
buildChangedScope,
|
|
64
|
+
formatRanges,
|
|
65
|
+
formatSymbols,
|
|
66
|
+
isLineInScope,
|
|
67
|
+
mergeRanges,
|
|
68
|
+
summarizeScope,
|
|
69
|
+
} from "./core/changed-scope";
|
|
47
70
|
export type { FindingsCommandResult } from "./core/commands/findings";
|
|
48
71
|
export { runFindingsCommand, runVerifyCommand } from "./core/commands/findings";
|
|
49
72
|
export { ProjectScaffolder } from "./core/commands/scaffold";
|
|
@@ -231,7 +254,6 @@ export type {
|
|
|
231
254
|
FileStatus,
|
|
232
255
|
FileStatusMap,
|
|
233
256
|
Rule,
|
|
234
|
-
RuleHints,
|
|
235
257
|
Severity,
|
|
236
258
|
WatchdogDefinition,
|
|
237
259
|
WatchdogMeta,
|
|
@@ -97,7 +97,6 @@ const KIND_ORDER: ArtifactKind[] = [
|
|
|
97
97
|
"ai",
|
|
98
98
|
"ai-usage",
|
|
99
99
|
"ai-brief",
|
|
100
|
-
"harness-transcript",
|
|
101
100
|
"context",
|
|
102
101
|
"context-brief",
|
|
103
102
|
"context-summary",
|
|
@@ -119,10 +118,9 @@ const GROUP_BY_KIND: Record<ArtifactKind, { key: string; label: string; order: n
|
|
|
119
118
|
candidates: { key: "core", label: "Núcleo da revisão", order: 3 },
|
|
120
119
|
ast: { key: "core", label: "Núcleo da revisão", order: 3 },
|
|
121
120
|
"ast-impact": { key: "core", label: "Núcleo da revisão", order: 3 },
|
|
122
|
-
ai: { key: "ai", label: "IA
|
|
123
|
-
"ai-usage": { key: "ai", label: "IA
|
|
124
|
-
"ai-brief": { key: "ai", label: "IA
|
|
125
|
-
"harness-transcript": { key: "ai", label: "IA e harness", order: 4 },
|
|
121
|
+
ai: { key: "ai", label: "IA", order: 4 },
|
|
122
|
+
"ai-usage": { key: "ai", label: "IA", order: 4 },
|
|
123
|
+
"ai-brief": { key: "ai", label: "IA", order: 4 },
|
|
126
124
|
context: { key: "context", label: "Contexto da PR", order: 5 },
|
|
127
125
|
"context-brief": { key: "context", label: "Contexto da PR", order: 5 },
|
|
128
126
|
"context-summary": { key: "context", label: "Contexto da PR", order: 5 },
|
|
@@ -146,7 +144,6 @@ export const ARTIFACT_KIND_LABELS: Record<ArtifactKind, string> = {
|
|
|
146
144
|
ai: "ia",
|
|
147
145
|
"ai-usage": "uso ia",
|
|
148
146
|
"ai-brief": "plano ia",
|
|
149
|
-
"harness-transcript": "transcript",
|
|
150
147
|
context: "contexto",
|
|
151
148
|
"context-brief": "brief ctx",
|
|
152
149
|
"context-summary": "resumo ctx",
|
|
@@ -69,6 +69,11 @@ staging global da review:
|
|
|
69
69
|
- Sem achados → `"findings": []` (arquivo **obrigatório**).
|
|
70
70
|
- Um achado por regra violada; não agrupar regras diferentes.
|
|
71
71
|
- `file:line` sempre que possível; `evidence` quando o achado não é óbvio.
|
|
72
|
+
- **Escopo do diff:** a `line` deve cair dentro das faixas listadas em
|
|
73
|
+
*Escopo do diff (símbolos alterados)* no brief. Código fora delas é contexto — não é
|
|
74
|
+
achado desta revisão. Se a alteração quebra código não alterado, ancore o finding na
|
|
75
|
+
linha alterada que causou o problema.
|
|
76
|
+
- Achado **sem linha** só quando for inerente à alteração (vira comentário de arquivo).
|
|
72
77
|
- Não inventar achados — se não houver violação, o array fica vazio.
|
|
73
78
|
|
|
74
79
|
## Findings emergentes (opt-in)
|
package/src/skill/README.md
CHANGED
|
@@ -18,7 +18,8 @@ Um único skill (`/houndsman`), quatro capabilities (detalhadas em `hunt.md`, `t
|
|
|
18
18
|
|
|
19
19
|
- **houndsman** — o orquestrador/treinador (CLI + motor).
|
|
20
20
|
- **watchdog** — cada agente especialista por domínio que audita o diff.
|
|
21
|
-
- **brief** — instruções prontas para um watchdog (o prompt do subagente)
|
|
21
|
+
- **brief** — instruções prontas para um watchdog (o prompt do subagente), incluindo o
|
|
22
|
+
**escopo do diff** (faixas/símbolos alterados que podem virar finding).
|
|
22
23
|
- **findings** — os achados que o subagente grava em JSON (único output consumido).
|
|
23
24
|
- **candidato (Jev)** — indício automático; **não** é achado (o watchdog confirma ou refuta).
|
|
24
25
|
- **churn** — linhas adicionadas/removidas por arquivo; usado para priorizar a leitura.
|
package/src/skill/SETUP.md
CHANGED
|
@@ -9,7 +9,7 @@ no seu projeto é **autocontida** — não exige nenhuma dependência local do p
|
|
|
9
9
|
- **git** — o diff usa `git diff <base>...<head> --name-status`.
|
|
10
10
|
- **gh** (Opcional) — Obtém contexto da PR e dispara comentários para o GitHub
|
|
11
11
|
- **`HOUNDSMAN_TYPE_SAFE_API_KEY`** (opcional) — pré-análise Jev.
|
|
12
|
-
- **`HOUNDSMAN_DEEP_SEEK_API_KEY`** (opcional) —
|
|
12
|
+
- **`HOUNDSMAN_DEEP_SEEK_API_KEY`** (opcional) — features de IA (briefs, contexto, classificação, resumo técnico).
|
|
13
13
|
- **`HOUNDSMAN_OPENAI_API_KEY`** / **`HOUNDSMAN_ANTHROPIC_API_KEY`** (opcional) — apenas se
|
|
14
14
|
`ai.provider` for `openai`/`anthropic`.
|
|
15
15
|
|
package/src/skill/fix.md
CHANGED
|
@@ -132,7 +132,7 @@ Volte ao passo 1. A fila encolhe a cada volta; o `batch` traz o próximo grupo s
|
|
|
132
132
|
|
|
133
133
|
## Perguntas de decisão ao usuário
|
|
134
134
|
|
|
135
|
-
Pergunte (ferramenta de perguntas do
|
|
135
|
+
Pergunte (ferramenta de perguntas do agente) **antes** de corrigir quando o achado for:
|
|
136
136
|
|
|
137
137
|
- ambíguo ou com **múltiplas correções válidas**;
|
|
138
138
|
- uma mudança de **produto/arquitetura** (não é "só código");
|
package/src/skill/hunt.md
CHANGED
|
@@ -25,12 +25,6 @@ Traduza o pedido do usuário para flags **antes** de começar:
|
|
|
25
25
|
**Quando usar:** revisar PR antes de merge · code review de feature branch · validar
|
|
26
26
|
mudanças antes de push.
|
|
27
27
|
|
|
28
|
-
> **Harness nativo (opcional):** além deste skill, o CLI tem `houndsman hunt` — o mesmo
|
|
29
|
-
> fluxo executado pelo próprio houndsman, com tools read-only, investigação por evidência
|
|
30
|
-
> e `VERIFY` obrigatório. Use `houndsman hunt --dry-run` para inspecionar sem IA. Para uma
|
|
31
|
-
> PR específica, `houndsman hunt --pr 456` roda no worktree automaticamente. Detalhes em
|
|
32
|
-
> `docs/HARNESS.md`.
|
|
33
|
-
|
|
34
28
|
## Quem executa o quê
|
|
35
29
|
|
|
36
30
|
- **Você** só invoca o skill: `/houndsman [hunt]`. Não precisa rodar nenhum script à mão.
|
|
@@ -62,6 +56,8 @@ mudanças antes de push.
|
|
|
62
56
|
- **As regras refletem os padrões APLICADOS hoje** — não validar práticas que o projeto
|
|
63
57
|
não adota. Regra sem aplicação real no código gera falso positivo.
|
|
64
58
|
- Cada watchdog recebe apenas seus arquivos relevantes (brief pronto) — nunca o diff completo.
|
|
59
|
+
- O brief traz o **escopo do diff** (símbolos alterados e faixas revisáveis); achado fora
|
|
60
|
+
dessas faixas é código pré-existente, não escopo desta revisão.
|
|
65
61
|
- Saída estruturada (JSON) por watchdog → consolidada e deduplicada automaticamente.
|
|
66
62
|
|
|
67
63
|
---
|
|
@@ -11,7 +11,7 @@ exports[`baseline — brief defaultRenderBrief (contexto, churn, overflow, candi
|
|
|
11
11
|
|
|
12
12
|
Watchdog baseline.
|
|
13
13
|
|
|
14
|
-
## Escopo
|
|
14
|
+
## Escopo do domínio
|
|
15
15
|
|
|
16
16
|
src/alpha/**
|
|
17
17
|
|
|
@@ -63,10 +63,10 @@ src/alpha/**
|
|
|
63
63
|
## Procedimento
|
|
64
64
|
|
|
65
65
|
1. **FILTER** — descarte o que não for do seu domínio (ver *Prioridade de leitura* e *Não carregar*).
|
|
66
|
-
2. **READ
|
|
67
|
-
3. **GREP** — use as regras críticas/anti-padrões como heurística para localizar violações.
|
|
68
|
-
4. **DEEP-READ** — aprofunde apenas
|
|
69
|
-
5. **CONCLUDE** — grave o JSON no caminho de output, com \`reviewedFiles\` preenchido. Sem achados → \`"findings": []\` (arquivo obrigatório). Não escreva markdown livre."
|
|
66
|
+
2. **READ SCOPE** — carregue apenas as faixas alteradas em *Escopo do diff*; o restante do arquivo é só contexto.
|
|
67
|
+
3. **GREP** — use as regras críticas/anti-padrões como heurística para localizar violações nas faixas alteradas.
|
|
68
|
+
4. **DEEP-READ** — aprofunde apenas as faixas com suspeita de violação dentro do escopo.
|
|
69
|
+
5. **CONCLUDE** — grave o JSON no caminho de output, com \`reviewedFiles\` preenchido e \`line\` dentro das faixas alteradas. Sem achados → \`"findings": []\` (arquivo obrigatório). Não escreva markdown livre."
|
|
70
70
|
`;
|
|
71
71
|
|
|
72
72
|
exports[`baseline — relatório renderReport consolidado 1`] = `
|
|
@@ -405,16 +405,6 @@ export type Rule = {
|
|
|
405
405
|
severity: Severity;
|
|
406
406
|
text: string;
|
|
407
407
|
jev?: JevRuleSpec;
|
|
408
|
-
hints?: RuleHints;
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
/** Sinais opcionais de uma regra (símbolos, imports, decorators, paths, checks). */
|
|
412
|
-
export type RuleHints = {
|
|
413
|
-
symbols?: string[];
|
|
414
|
-
imports?: string[];
|
|
415
|
-
decorators?: string[];
|
|
416
|
-
paths?: string[];
|
|
417
|
-
checks?: string[];
|
|
418
408
|
};
|
|
419
409
|
|
|
420
410
|
/** Como o watchdog casa um arquivo alterado. \`value\` é sempre regex. */
|
|
@@ -509,20 +499,15 @@ export type HoundsmanAiFeatures = {
|
|
|
509
499
|
/** Overrides de IA por watchdog. */
|
|
510
500
|
export type HoundsmanAiWatchdog = {
|
|
511
501
|
model?: string;
|
|
512
|
-
maxSteps?: number;
|
|
513
|
-
maxOutputTokens?: number;
|
|
514
502
|
};
|
|
515
503
|
|
|
516
|
-
/** Configuração da camada de IA (briefs, contexto, classificação
|
|
504
|
+
/** Configuração da camada de IA (briefs, contexto, classificação, pré-análise, verify). */
|
|
517
505
|
export type HoundsmanAi = {
|
|
518
506
|
enabled?: boolean;
|
|
519
507
|
provider?: AiProviderKind;
|
|
520
508
|
model?: string;
|
|
521
509
|
baseURL?: string;
|
|
522
510
|
concurrency?: number;
|
|
523
|
-
maxSteps?: number;
|
|
524
|
-
maxOutputTokens?: number;
|
|
525
|
-
temperature?: number;
|
|
526
511
|
providerOptions?: AiProviderOptions;
|
|
527
512
|
timeoutMs?: number;
|
|
528
513
|
maxCallsPerReview?: number;
|
|
@@ -665,15 +650,15 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 3
|
|
|
665
650
|
},
|
|
666
651
|
{
|
|
667
652
|
"file": ".houndsman/core/skill/OUTPUT-FORMAT.md",
|
|
668
|
-
"sha256": "
|
|
653
|
+
"sha256": "a78ed16d4d211889",
|
|
669
654
|
},
|
|
670
655
|
{
|
|
671
656
|
"file": ".houndsman/core/skill/README.md",
|
|
672
|
-
"sha256": "
|
|
657
|
+
"sha256": "9aadf51b48c7f304",
|
|
673
658
|
},
|
|
674
659
|
{
|
|
675
660
|
"file": ".houndsman/core/skill/SETUP.md",
|
|
676
|
-
"sha256": "
|
|
661
|
+
"sha256": "5e5335870f493aba",
|
|
677
662
|
},
|
|
678
663
|
{
|
|
679
664
|
"file": ".houndsman/core/skill/example-review.md",
|
|
@@ -681,11 +666,11 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 3
|
|
|
681
666
|
},
|
|
682
667
|
{
|
|
683
668
|
"file": ".houndsman/core/skill/fix.md",
|
|
684
|
-
"sha256": "
|
|
669
|
+
"sha256": "5782f4880af78d29",
|
|
685
670
|
},
|
|
686
671
|
{
|
|
687
672
|
"file": ".houndsman/core/skill/hunt.md",
|
|
688
|
-
"sha256": "
|
|
673
|
+
"sha256": "dfaec1a665d7ee65",
|
|
689
674
|
},
|
|
690
675
|
{
|
|
691
676
|
"file": ".houndsman/core/skill/train.md",
|
|
@@ -702,6 +687,7 @@ exports[`baseline — artefatos de prepare/finalize gera classification, brief,
|
|
|
702
687
|
"Discovery: 1 watchdog(s) ativado(s), 3 arquivo(s), 1 deletado(s).
|
|
703
688
|
Briefs: <cwd>/.home/work/<uuid>/briefs/
|
|
704
689
|
Deletado (breaking change): src/alpha/old.ts
|
|
690
|
+
Escopo: 2 arquivo(s), 0 símbolo(s) em 2 faixa(s) alterada(s).
|
|
705
691
|
Jev: pulado (desativado (--no-jev)).
|
|
706
692
|
AST: desabilitado ou sem grammar compatível.
|
|
707
693
|
Sem contexto da PR (rode \`bun run houndsman context\` antes, se houver PR).
|
|
@@ -753,7 +739,29 @@ exports[`baseline — artefatos de prepare/finalize gera classification, brief,
|
|
|
753
739
|
},
|
|
754
740
|
"excludedCount": 0,
|
|
755
741
|
"headSha": "<sha>",
|
|
756
|
-
"mode": "full"
|
|
742
|
+
"mode": "full",
|
|
743
|
+
"changedScope": {
|
|
744
|
+
"src/alpha/a.ts": {
|
|
745
|
+
"ranges": [
|
|
746
|
+
{
|
|
747
|
+
"start": 1,
|
|
748
|
+
"end": 2
|
|
749
|
+
}
|
|
750
|
+
],
|
|
751
|
+
"symbols": [],
|
|
752
|
+
"source": "hunks"
|
|
753
|
+
},
|
|
754
|
+
"src/alpha/b.ts": {
|
|
755
|
+
"ranges": [
|
|
756
|
+
{
|
|
757
|
+
"start": 1,
|
|
758
|
+
"end": 1
|
|
759
|
+
}
|
|
760
|
+
],
|
|
761
|
+
"symbols": [],
|
|
762
|
+
"source": "hunks"
|
|
763
|
+
}
|
|
764
|
+
}
|
|
757
765
|
}
|
|
758
766
|
"
|
|
759
767
|
`;
|
|
@@ -827,10 +835,20 @@ exports[`baseline — artefatos de prepare/finalize gera classification, brief,
|
|
|
827
835
|
|
|
828
836
|
Watchdog baseline.
|
|
829
837
|
|
|
830
|
-
## Escopo
|
|
838
|
+
## Escopo do domínio
|
|
831
839
|
|
|
832
840
|
src/alpha/**
|
|
833
841
|
|
|
842
|
+
## Escopo do diff (símbolos alterados)
|
|
843
|
+
|
|
844
|
+
> **Regra de escopo:** um finding só é válido quando a \`line\` está nas faixas
|
|
845
|
+
> abaixo. Código fora delas é contexto — use para entender o impacto, nunca
|
|
846
|
+
> como achado. Se a mudança quebra código não alterado, ancore o finding na
|
|
847
|
+
> linha alterada que causou o problema.
|
|
848
|
+
|
|
849
|
+
- \`src/alpha/a.ts\` — linhas 1-2 _(faixas do diff)_
|
|
850
|
+
- \`src/alpha/b.ts\` — linhas 1 _(faixas do diff)_
|
|
851
|
+
|
|
834
852
|
## Regras (por severidade)
|
|
835
853
|
|
|
836
854
|
- ❌ **blocking** — Nunca usar \`any\`.
|
|
@@ -852,10 +870,10 @@ src/alpha/**
|
|
|
852
870
|
## Procedimento
|
|
853
871
|
|
|
854
872
|
1. **FILTER** — descarte o que não for do seu domínio (ver *Prioridade de leitura* e *Não carregar*).
|
|
855
|
-
2. **READ
|
|
856
|
-
3. **GREP** — use as regras críticas/anti-padrões como heurística para localizar violações.
|
|
857
|
-
4. **DEEP-READ** — aprofunde apenas
|
|
858
|
-
5. **CONCLUDE** — grave o JSON no caminho de output, com \`reviewedFiles\` preenchido. Sem achados → \`"findings": []\` (arquivo obrigatório). Não escreva markdown livre.
|
|
873
|
+
2. **READ SCOPE** — carregue apenas as faixas alteradas em *Escopo do diff*; o restante do arquivo é só contexto.
|
|
874
|
+
3. **GREP** — use as regras críticas/anti-padrões como heurística para localizar violações nas faixas alteradas.
|
|
875
|
+
4. **DEEP-READ** — aprofunde apenas as faixas com suspeita de violação dentro do escopo.
|
|
876
|
+
5. **CONCLUDE** — grave o JSON no caminho de output, com \`reviewedFiles\` preenchido e \`line\` dentro das faixas alteradas. Sem achados → \`"findings": []\` (arquivo obrigatório). Não escreva markdown livre.
|
|
859
877
|
"
|
|
860
878
|
`;
|
|
861
879
|
|
|
@@ -11,6 +11,8 @@ import { AiPreAnalyzer } from "../core/ai/features/pre-analyzer";
|
|
|
11
11
|
import { AiTechnicalSummarizer } from "../core/ai/features/technical-summarizer";
|
|
12
12
|
import { AiVerifier } from "../core/ai/features/verifier";
|
|
13
13
|
import { briefInstructions, briefPrompt } from "../core/ai/prompts/brief";
|
|
14
|
+
import { contextInstructions } from "../core/ai/prompts/context";
|
|
15
|
+
import { technicalInstructions } from "../core/ai/prompts/technical";
|
|
14
16
|
import { renderBrief } from "../core/brief";
|
|
15
17
|
import { ProjectConfig } from "../core/project-config";
|
|
16
18
|
import { FakeModelProvider } from "./fixtures/fake-model-provider";
|
|
@@ -217,3 +219,12 @@ describe("AiPreAnalyzer", () => {
|
|
|
217
219
|
).toEqual([]);
|
|
218
220
|
});
|
|
219
221
|
});
|
|
222
|
+
|
|
223
|
+
describe("idioma dos resumos", () => {
|
|
224
|
+
test("prompts de contexto e pontos técnicos exigem pt-BR", () => {
|
|
225
|
+
for (const instructions of [contextInstructions(), technicalInstructions()]) {
|
|
226
|
+
expect(instructions).toContain("português do Brasil");
|
|
227
|
+
expect(instructions).toContain("pt-BR");
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
});
|