@payconductor/houndsman 0.3.0 → 0.4.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 +18 -6
- package/package.json +1 -1
- package/src/cli/commands/context-command.ts +11 -9
- package/src/cli/commands/doctor-command.ts +5 -0
- package/src/cli/commands/findings-command.ts +13 -3
- package/src/cli/commands/review-command.ts +1 -2
- package/src/cli/help.ts +7 -3
- package/src/cli/index.ts +2 -2
- package/src/core/commands/context.ts +29 -13
- package/src/core/commands/findings.ts +155 -120
- package/src/core/commands/review.ts +7 -8
- package/src/core/commands/scaffold.ts +15 -16
- package/src/core/config.ts +16 -7
- package/src/core/engine/houndsman-engine.ts +15 -1
- package/src/core/engine/review-session.ts +37 -9
- package/src/core/findings-state.ts +32 -71
- package/src/core/jev/jev-candidates.ts +49 -35
- package/src/core/orchestrator.ts +175 -128
- package/src/core/paths.ts +23 -17
- package/src/core/pr-context.ts +19 -5
- package/src/core/project-config.ts +4 -5
- package/src/core/renderers/manifest-writer.ts +29 -18
- package/src/core/run-layout.ts +4 -104
- package/src/core/run-status.ts +28 -53
- package/src/core/services/git-repository.ts +16 -0
- package/src/core/services/project-files.ts +13 -1
- package/src/core/storage/factory.ts +24 -0
- package/src/core/storage/file/file-storage.ts +269 -0
- package/src/core/storage/paths.ts +64 -0
- package/src/core/storage/repo-identity.ts +70 -0
- package/src/core/storage/sqlite/mappers.ts +160 -0
- package/src/core/storage/sqlite/schema.ts +108 -0
- package/src/core/storage/sqlite/sqlite-storage.ts +427 -0
- package/src/core/storage/types.ts +143 -0
- package/src/core/storage/work-area.ts +191 -0
- package/src/index.ts +27 -6
- package/src/server/artifacts.ts +8 -61
- package/src/server/client/ArtifactsPanel.tsx +137 -23
- package/src/server/client/FindingCard.tsx +37 -6
- package/src/server/client/MarkdownText.tsx +101 -0
- package/src/server/client/SubmitModal.tsx +9 -4
- package/src/server/client/code-highlight.ts +422 -0
- package/src/server/client/constants.ts +130 -24
- package/src/server/client/json-highlight.ts +55 -0
- package/src/server/client/markdown.ts +153 -0
- package/src/server/client/types.ts +23 -0
- package/src/server/index.ts +95 -15
- package/src/server/run-repository.ts +144 -136
- package/src/skill/OUTPUT-FORMAT.md +6 -4
- package/src/skill/README.md +22 -14
- package/src/skill/SETUP.md +4 -4
- package/src/skill/fix.md +5 -5
- package/src/skill/hunt.md +27 -18
- package/src/skill/train.md +1 -1
- package/src/skill/verify.md +5 -5
- package/src/tests/__snapshots__/baseline.test.ts.snap +44 -33
- package/src/tests/baseline.test.ts +76 -34
- package/src/tests/cli.test.ts +40 -19
- package/src/tests/code-highlight.test.ts +123 -0
- package/src/tests/domain.test.ts +1 -1
- package/src/tests/findings-commands.test.ts +153 -131
- package/src/tests/findings-state.test.ts +6 -3
- package/src/tests/fixtures/fake-project.ts +0 -1
- package/src/tests/infrastructure.test.ts +33 -38
- package/src/tests/markdown.test.ts +89 -0
- package/src/tests/review-scripts.test.ts +8 -5
- package/src/tests/run-status.test.ts +33 -23
- package/src/tests/server.test.ts +108 -75
- package/src/tests/storage.test.ts +386 -0
package/src/core/orchestrator.ts
CHANGED
|
@@ -5,17 +5,17 @@
|
|
|
5
5
|
* Genérico: recebe o `HoundsmanConfig` por parâmetro (injeção explícita).
|
|
6
6
|
* Entrypoints: `cli/index.ts` (`houndsman`). Aqui não há `process.argv`.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
8
|
+
* Persistência:
|
|
9
|
+
* storage reviews/findings/artefatos (SQLite por default)
|
|
10
|
+
* <home>/work/<review> staging dos subagentes
|
|
11
|
+
* briefs/<id>.md instruções prontas para cada sub-agente
|
|
12
|
+
* findings/<id>.json saída dos sub-agentes (schema skill/OUTPUT-FORMAT.md)
|
|
13
|
+
* context*.json/md contexto da PR (Passo 0)
|
|
14
|
+
*
|
|
15
|
+
* Artefatos persistidos: classification, candidates, manifest, review.md,
|
|
16
|
+
* review.<watchdog>.md, context e context-summary.
|
|
15
17
|
*/
|
|
16
18
|
|
|
17
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
18
|
-
import { join } from "node:path";
|
|
19
19
|
import {
|
|
20
20
|
type HoundsmanConfig,
|
|
21
21
|
resolveJevConcurrency,
|
|
@@ -28,7 +28,7 @@ import { Watchdog } from "./domain/watchdog";
|
|
|
28
28
|
import { mergeCarryover } from "./engine/carryover";
|
|
29
29
|
import { ReviewScopeResolver, type RunClassification } from "./engine/review-scope";
|
|
30
30
|
import type { BudgetSelection, ClassificationResult } from "./file-classifier";
|
|
31
|
-
import { buildStateFromFindings,
|
|
31
|
+
import { buildStateFromFindings, fileReader } from "./findings-state";
|
|
32
32
|
import { resolveSha } from "./git-revision";
|
|
33
33
|
import {
|
|
34
34
|
type JevCandidate,
|
|
@@ -36,9 +36,11 @@ import {
|
|
|
36
36
|
JevCandidatesStore,
|
|
37
37
|
JevClient,
|
|
38
38
|
JevDetector,
|
|
39
|
+
parseCandidates,
|
|
40
|
+
serializeCandidates,
|
|
39
41
|
} from "./jev";
|
|
40
|
-
import {
|
|
41
|
-
import { currentBranch
|
|
42
|
+
import { displayPath, OUTPUT_FORMAT_PATH, REVIEW_FILE } from "./paths";
|
|
43
|
+
import { currentBranch } from "./pr-context";
|
|
42
44
|
import { ProjectConfig } from "./project-config";
|
|
43
45
|
import { BriefRenderer } from "./renderers/brief-renderer";
|
|
44
46
|
import { ManifestWriter } from "./renderers/manifest-writer";
|
|
@@ -47,13 +49,14 @@ import {
|
|
|
47
49
|
type ContextSummary,
|
|
48
50
|
type DomainReport,
|
|
49
51
|
dedupeFindings,
|
|
50
|
-
|
|
52
|
+
parseContextSummary,
|
|
51
53
|
sanitizeBranch,
|
|
52
54
|
type WatchdogReport,
|
|
53
55
|
} from "./report-builder";
|
|
54
|
-
import { RunLayout } from "./run-layout";
|
|
55
56
|
import { writeRunStatus } from "./run-status";
|
|
56
57
|
import { systemClock } from "./services/clock";
|
|
58
|
+
import type { ReviewRecord, StorageProvider } from "./storage/types";
|
|
59
|
+
import { ReviewWorkArea } from "./storage/work-area";
|
|
57
60
|
import type { WatchdogMeta } from "./types";
|
|
58
61
|
import { findWatchdog } from "./watchdog-registry";
|
|
59
62
|
|
|
@@ -61,7 +64,6 @@ export type ReviewOptions = {
|
|
|
61
64
|
base: string;
|
|
62
65
|
head: string;
|
|
63
66
|
branch?: string;
|
|
64
|
-
dir: string;
|
|
65
67
|
finalize: boolean;
|
|
66
68
|
failOnBlocking: boolean;
|
|
67
69
|
clean: boolean;
|
|
@@ -71,11 +73,10 @@ export type ReviewOptions = {
|
|
|
71
73
|
};
|
|
72
74
|
|
|
73
75
|
/** Valores default das opções (o CLI sobrepõe com as flags). */
|
|
74
|
-
export function defaultOptions(
|
|
76
|
+
export function defaultOptions(): ReviewOptions {
|
|
75
77
|
return {
|
|
76
78
|
base: "main",
|
|
77
79
|
head: "HEAD",
|
|
78
|
-
dir: artifactDir,
|
|
79
80
|
finalize: false,
|
|
80
81
|
failOnBlocking: false,
|
|
81
82
|
clean: false,
|
|
@@ -84,20 +85,19 @@ export function defaultOptions(artifactDir: string = ARTIFACT_DIR): ReviewOption
|
|
|
84
85
|
};
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
/**
|
|
88
|
-
type
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
88
|
+
/** Contexto de uma run: storage + review + staging. */
|
|
89
|
+
export type ReviewContext = {
|
|
90
|
+
storage: StorageProvider;
|
|
91
|
+
review: ReviewRecord;
|
|
92
|
+
work: ReviewWorkArea;
|
|
93
|
+
};
|
|
93
94
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
95
|
+
/** Cria o contexto de uma review existente. */
|
|
96
|
+
export function reviewContext(
|
|
97
|
+
storage: StorageProvider,
|
|
98
|
+
review: ReviewRecord,
|
|
99
|
+
): ReviewContext {
|
|
100
|
+
return { storage, review, work: new ReviewWorkArea(review) };
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
// ---------------------------------------------------------------------------
|
|
@@ -120,40 +120,6 @@ function incrementalNote(classification: RunClassification): string {
|
|
|
120
120
|
);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
|
-
// Findings
|
|
125
|
-
// ---------------------------------------------------------------------------
|
|
126
|
-
|
|
127
|
-
function readFindings(
|
|
128
|
-
findingsDir: string,
|
|
129
|
-
activeIds: string[],
|
|
130
|
-
): { reports: WatchdogReport[]; pending: string[]; warnings: string[] } {
|
|
131
|
-
const reports: WatchdogReport[] = [];
|
|
132
|
-
const pending: string[] = [];
|
|
133
|
-
const warnings: string[] = [];
|
|
134
|
-
for (const id of activeIds) {
|
|
135
|
-
const path = join(findingsDir, `${id}.json`);
|
|
136
|
-
if (!existsSync(path)) {
|
|
137
|
-
pending.push(id);
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
141
|
-
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
142
|
-
const report = parseWatchdogReport(raw, path, (message) =>
|
|
143
|
-
warnings.push(message),
|
|
144
|
-
);
|
|
145
|
-
if (report) reports.push(report);
|
|
146
|
-
else pending.push(id);
|
|
147
|
-
} catch (err) {
|
|
148
|
-
warnings.push(
|
|
149
|
-
`[houndsman] ${relFromRoot(path)} inválido: ${err instanceof Error ? err.message : err}`,
|
|
150
|
-
);
|
|
151
|
-
pending.push(id);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
return { reports, pending, warnings };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
123
|
// ---------------------------------------------------------------------------
|
|
158
124
|
// Orçamento (triage de PRs grandes)
|
|
159
125
|
// ---------------------------------------------------------------------------
|
|
@@ -169,7 +135,7 @@ function budgetFor(
|
|
|
169
135
|
|
|
170
136
|
/** Grava o manifesto da run (delegado ao `ManifestWriter`). */
|
|
171
137
|
function writeManifest(
|
|
172
|
-
|
|
138
|
+
ctx: ReviewContext,
|
|
173
139
|
config: HoundsmanConfig,
|
|
174
140
|
opts: ReviewOptions,
|
|
175
141
|
classification: RunClassification,
|
|
@@ -177,12 +143,13 @@ function writeManifest(
|
|
|
177
143
|
pending: string[],
|
|
178
144
|
jev: JevCandidatesFile,
|
|
179
145
|
): void {
|
|
180
|
-
new ManifestWriter(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
146
|
+
new ManifestWriter(
|
|
147
|
+
ctx.storage,
|
|
148
|
+
ctx.review,
|
|
149
|
+
ctx.work,
|
|
150
|
+
new ProjectConfig(config),
|
|
151
|
+
opts,
|
|
152
|
+
).write(classification, activeIds, pending, jev);
|
|
186
153
|
}
|
|
187
154
|
|
|
188
155
|
// ---------------------------------------------------------------------------
|
|
@@ -192,12 +159,13 @@ function writeManifest(
|
|
|
192
159
|
function writeBriefs(
|
|
193
160
|
activeIds: string[],
|
|
194
161
|
classification: ClassificationResult,
|
|
195
|
-
|
|
162
|
+
ctx: ReviewContext,
|
|
196
163
|
branch: string,
|
|
197
164
|
context: ContextSummary | null,
|
|
198
165
|
candidates: JevCandidate[],
|
|
199
166
|
config: HoundsmanConfig,
|
|
200
167
|
): void {
|
|
168
|
+
const work = ctx.work;
|
|
201
169
|
const docs = discoverDocs(Object.keys(classification.files), config);
|
|
202
170
|
const maxFilesPerBrief = resolveMaxFilesPerBrief(config);
|
|
203
171
|
const renderer = new BriefRenderer();
|
|
@@ -213,13 +181,20 @@ function writeBriefs(
|
|
|
213
181
|
overflow,
|
|
214
182
|
deleted: classification.deleted,
|
|
215
183
|
branch,
|
|
216
|
-
outputPath:
|
|
184
|
+
outputPath: work.findingFile(id),
|
|
217
185
|
outputFormatPath: OUTPUT_FORMAT_PATH,
|
|
218
186
|
docs,
|
|
219
187
|
context,
|
|
220
188
|
candidates: candidates.filter((c) => c.watchdogId === id),
|
|
221
189
|
});
|
|
222
|
-
|
|
190
|
+
const content = `${brief}\n`;
|
|
191
|
+
work.writeBrief(id, content);
|
|
192
|
+
// Auditoria: o brief também fica no storage (o staging é limpo no finalize).
|
|
193
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
194
|
+
name: `briefs/${id}.md`,
|
|
195
|
+
kind: "brief",
|
|
196
|
+
content,
|
|
197
|
+
});
|
|
223
198
|
}
|
|
224
199
|
}
|
|
225
200
|
|
|
@@ -234,13 +209,16 @@ function writeBriefs(
|
|
|
234
209
|
async function resolveCandidates(
|
|
235
210
|
opts: ReviewOptions,
|
|
236
211
|
classification: ClassificationResult,
|
|
237
|
-
|
|
212
|
+
ctx: ReviewContext,
|
|
238
213
|
config: HoundsmanConfig,
|
|
239
214
|
diff: DiffSource,
|
|
240
215
|
): Promise<JevCandidatesFile> {
|
|
241
|
-
const store = new JevCandidatesStore(dirs.candidatesFile, config.watchdogIds);
|
|
242
216
|
const save = (data: JevCandidatesFile): JevCandidatesFile => {
|
|
243
|
-
|
|
217
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
218
|
+
name: "candidates.json",
|
|
219
|
+
kind: "candidates",
|
|
220
|
+
content: serializeCandidates(data),
|
|
221
|
+
});
|
|
244
222
|
return data;
|
|
245
223
|
};
|
|
246
224
|
|
|
@@ -295,7 +273,7 @@ function discoveryMessages(
|
|
|
295
273
|
): string[] {
|
|
296
274
|
const messages = [
|
|
297
275
|
`Discovery: ${activeIds.length} watchdog(s) ativado(s), ${Object.keys(classification.files).length} arquivo(s), ${classification.deleted.length} deletado(s).`,
|
|
298
|
-
`Briefs: ${
|
|
276
|
+
`Briefs: ${displayPath(briefsDir)}/`,
|
|
299
277
|
];
|
|
300
278
|
for (const file of classification.deleted) {
|
|
301
279
|
messages.push(`Deletado (breaking change): ${file}`);
|
|
@@ -327,31 +305,37 @@ export async function prepare(
|
|
|
327
305
|
opts: ReviewOptions,
|
|
328
306
|
branch: string,
|
|
329
307
|
config: HoundsmanConfig,
|
|
330
|
-
|
|
308
|
+
ctx: ReviewContext,
|
|
331
309
|
): Promise<PrepareResult> {
|
|
332
310
|
const messages: string[] = [];
|
|
333
|
-
|
|
311
|
+
ctx.work.ensure();
|
|
312
|
+
if (opts.clean) ctx.work.cleanFindings();
|
|
313
|
+
ctx.storage.updateReview(ctx.review.id, { base: opts.base, head: opts.head });
|
|
334
314
|
|
|
335
315
|
// Marca o run como "revisando" (reinicia o ciclo) — a UI mostra o status.
|
|
336
|
-
writeRunStatus(
|
|
316
|
+
writeRunStatus(ctx.storage, ctx.review.id, {
|
|
317
|
+
status: "reviewing",
|
|
318
|
+
resetStartedAt: true,
|
|
319
|
+
});
|
|
337
320
|
|
|
338
321
|
// Marca o commit revisado e decide entre review completo e incremental.
|
|
339
322
|
const headSha = resolveSha(opts.head);
|
|
340
|
-
const previous =
|
|
323
|
+
const previous = ctx.storage.readFindings(ctx.review.id);
|
|
341
324
|
const { classification, diff } = new ReviewScopeResolver(opts, config).resolve(
|
|
342
325
|
previous,
|
|
343
326
|
headSha,
|
|
344
327
|
);
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
328
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
329
|
+
name: "classification.json",
|
|
330
|
+
kind: "classification",
|
|
331
|
+
content: `${JSON.stringify(classification, null, 2)}\n`,
|
|
332
|
+
});
|
|
349
333
|
|
|
350
|
-
const jev = await resolveCandidates(opts, classification,
|
|
334
|
+
const jev = await resolveCandidates(opts, classification, ctx, config, diff);
|
|
351
335
|
|
|
352
336
|
const activeIds = Object.keys(classification.watchdogs);
|
|
353
337
|
if (Object.keys(classification.files).length === 0) {
|
|
354
|
-
writeRunStatus(
|
|
338
|
+
writeRunStatus(ctx.storage, ctx.review.id, { status: "review_done" });
|
|
355
339
|
messages.push(
|
|
356
340
|
classification.mode === "incremental"
|
|
357
341
|
? `Nenhuma alteração desde a última revisão (${shortSha(classification.reviewedFrom ?? "")}).`
|
|
@@ -360,7 +344,7 @@ export async function prepare(
|
|
|
360
344
|
return { messages };
|
|
361
345
|
}
|
|
362
346
|
if (activeIds.length === 0) {
|
|
363
|
-
writeRunStatus(
|
|
347
|
+
writeRunStatus(ctx.storage, ctx.review.id, { status: "review_done" });
|
|
364
348
|
messages.push(
|
|
365
349
|
"Nenhum arquivo revisável (tudo excluído, deletado ou irrelevante).",
|
|
366
350
|
);
|
|
@@ -372,13 +356,14 @@ export async function prepare(
|
|
|
372
356
|
);
|
|
373
357
|
}
|
|
374
358
|
|
|
375
|
-
const context = readContextSummary(
|
|
359
|
+
const context = ctx.work.readContextSummary() ?? readPersistedContextSummary(ctx);
|
|
360
|
+
persistContext(ctx);
|
|
376
361
|
|
|
377
|
-
writeBriefs(activeIds, classification,
|
|
378
|
-
writeManifest(
|
|
362
|
+
writeBriefs(activeIds, classification, ctx, branch, context, jev.candidates, config);
|
|
363
|
+
writeManifest(ctx, config, opts, classification, activeIds, activeIds, jev);
|
|
379
364
|
|
|
380
365
|
messages.push(
|
|
381
|
-
...discoveryMessages(activeIds, classification,
|
|
366
|
+
...discoveryMessages(activeIds, classification, ctx.work.briefsDir),
|
|
382
367
|
...budgetMessage(classification, config),
|
|
383
368
|
...jevSummary(jev),
|
|
384
369
|
);
|
|
@@ -392,28 +377,78 @@ export async function prepare(
|
|
|
392
377
|
}
|
|
393
378
|
|
|
394
379
|
messages.push(
|
|
395
|
-
`\nPróximo passo: despachar os sub-agentes (um por brief em ${
|
|
380
|
+
`\nPróximo passo: despachar os sub-agentes (um por brief em ${displayPath(ctx.work.briefsDir)}/).`,
|
|
396
381
|
"Quando os findings estiverem gravados: bun run houndsman --finalize",
|
|
397
382
|
);
|
|
398
383
|
|
|
399
384
|
return { messages };
|
|
400
385
|
}
|
|
386
|
+
/** Persiste o contexto da PR (bruto + resumo) como artefatos, quando existirem. */
|
|
387
|
+
function persistContext(ctx: ReviewContext): void {
|
|
388
|
+
const raw = ctx.work.readContextRaw();
|
|
389
|
+
if (raw !== null) {
|
|
390
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
391
|
+
name: "context.json",
|
|
392
|
+
kind: "context",
|
|
393
|
+
content: raw,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
const summary = ctx.work.readContextSummaryRaw();
|
|
397
|
+
if (summary !== null) {
|
|
398
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
399
|
+
name: "context-summary.json",
|
|
400
|
+
kind: "context-summary",
|
|
401
|
+
content: summary,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
const brief = ctx.work.readContextBriefRaw();
|
|
405
|
+
if (brief !== null) {
|
|
406
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
407
|
+
name: "context-brief.md",
|
|
408
|
+
kind: "context-brief",
|
|
409
|
+
content: brief,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
401
413
|
|
|
402
414
|
// ---------------------------------------------------------------------------
|
|
403
415
|
// Passo 2 — finalizar
|
|
404
416
|
// ---------------------------------------------------------------------------
|
|
405
417
|
|
|
406
|
-
/** Lê o classification.json
|
|
407
|
-
function readClassification(
|
|
408
|
-
|
|
418
|
+
/** Lê o `classification.json` persistido pelo prepare (lança se ausente). */
|
|
419
|
+
function readClassification(ctx: ReviewContext): RunClassification {
|
|
420
|
+
const artifact = ctx.storage.readArtifact(ctx.review.id, "classification.json");
|
|
421
|
+
if (!artifact) {
|
|
409
422
|
throw new Error(
|
|
410
|
-
`Não encontrei
|
|
423
|
+
`Não encontrei a classificação da review. Rode \`bun run houndsman\` (preparar) primeiro.`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
return JSON.parse(artifact.content) as RunClassification;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Lê os candidatos Jev persistidos (unavailable se ausente/inválido). */
|
|
430
|
+
function readCandidates(ctx: ReviewContext, config: HoundsmanConfig): JevCandidatesFile {
|
|
431
|
+
const artifact = ctx.storage.readArtifact(ctx.review.id, "candidates.json");
|
|
432
|
+
if (!artifact) return JevCandidatesStore.unavailable("candidates.json ausente");
|
|
433
|
+
try {
|
|
434
|
+
return (
|
|
435
|
+
parseCandidates(JSON.parse(artifact.content), config.watchdogIds) ??
|
|
436
|
+
JevCandidatesStore.unavailable("candidates.json inválido")
|
|
411
437
|
);
|
|
438
|
+
} catch {
|
|
439
|
+
return JevCandidatesStore.unavailable("candidates.json inválido");
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Resumo do contexto persistido (usado quando o staging já foi limpo). */
|
|
444
|
+
function readPersistedContextSummary(ctx: ReviewContext): ContextSummary | null {
|
|
445
|
+
const artifact = ctx.storage.readArtifact(ctx.review.id, "context-summary.json");
|
|
446
|
+
if (!artifact) return null;
|
|
447
|
+
try {
|
|
448
|
+
return parseContextSummary(JSON.parse(artifact.content));
|
|
449
|
+
} catch {
|
|
450
|
+
return null;
|
|
412
451
|
}
|
|
413
|
-
const parsed = JSON.parse(
|
|
414
|
-
readFileSync(classificationPath, "utf8"),
|
|
415
|
-
) as RunClassification;
|
|
416
|
-
return parsed;
|
|
417
452
|
}
|
|
418
453
|
|
|
419
454
|
/**
|
|
@@ -421,7 +456,7 @@ function readClassification(classificationPath: string): RunClassification {
|
|
|
421
456
|
* Mantém o `review.md` consolidado navegável em PRs grandes (centenas de achados).
|
|
422
457
|
*/
|
|
423
458
|
function writeDomainReports(
|
|
424
|
-
|
|
459
|
+
ctx: ReviewContext,
|
|
425
460
|
branch: string,
|
|
426
461
|
reports: WatchdogReport[],
|
|
427
462
|
classification: ClassificationResult,
|
|
@@ -435,16 +470,17 @@ function writeDomainReports(
|
|
|
435
470
|
if (findings.length === 0) continue;
|
|
436
471
|
const file = `review.${sanitizeBranch(report.watchdogId)}.md`;
|
|
437
472
|
const expected = classification.watchdogs[report.watchdogId] ?? [];
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
473
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
474
|
+
name: file,
|
|
475
|
+
kind: "domain-review",
|
|
476
|
+
content: `${renderer.renderDomain(
|
|
441
477
|
branch,
|
|
442
478
|
report,
|
|
443
479
|
findings,
|
|
444
480
|
expected.length,
|
|
445
481
|
context,
|
|
446
482
|
)}\n`,
|
|
447
|
-
);
|
|
483
|
+
});
|
|
448
484
|
domains.push({
|
|
449
485
|
id: report.watchdogId,
|
|
450
486
|
name: nameOf(report.watchdogId),
|
|
@@ -472,23 +508,21 @@ export function finalize(
|
|
|
472
508
|
opts: ReviewOptions,
|
|
473
509
|
branch: string,
|
|
474
510
|
config: HoundsmanConfig,
|
|
475
|
-
|
|
511
|
+
ctx: ReviewContext,
|
|
476
512
|
): FinalizeResult {
|
|
477
|
-
const classification = readClassification(
|
|
513
|
+
const classification = readClassification(ctx);
|
|
478
514
|
const activeIds = Object.keys(classification.watchdogs);
|
|
479
515
|
|
|
480
516
|
const messages: string[] = [];
|
|
481
|
-
const { reports, pending, warnings } = readFindings(
|
|
517
|
+
const { reports, pending, warnings } = ctx.work.readFindings(activeIds);
|
|
482
518
|
messages.push(...warnings);
|
|
483
|
-
const jev =
|
|
484
|
-
|
|
485
|
-
JevCandidatesStore.unavailable("candidates.json ausente");
|
|
486
|
-
writeManifest(dirs, config, opts, classification, activeIds, pending, jev);
|
|
519
|
+
const jev = readCandidates(ctx, config);
|
|
520
|
+
writeManifest(ctx, config, opts, classification, activeIds, pending, jev);
|
|
487
521
|
|
|
488
522
|
if (pending.length > 0) {
|
|
489
523
|
throw new Error(
|
|
490
524
|
`Faltam ${pending.length}/${activeIds.length} findings: ${pending.join(", ")}.\n` +
|
|
491
|
-
`Cada watchdog deve gravar ${
|
|
525
|
+
`Cada watchdog deve gravar ${displayPath(ctx.work.findingsDir)}/<id>.json (schema ${OUTPUT_FORMAT_PATH}).`,
|
|
492
526
|
);
|
|
493
527
|
}
|
|
494
528
|
|
|
@@ -508,27 +542,29 @@ export function finalize(
|
|
|
508
542
|
// anexa o `id` de cada finding aos itens do relatório. A linha é cache —
|
|
509
543
|
// recalculada por `houndsman findings sync` após edições. O commit revisado
|
|
510
544
|
// (`reviewed.headSha`) habilita o review incremental e o diff focado do verify.
|
|
511
|
-
const store = FindingsStateStore.at(opts.dir, branch);
|
|
512
545
|
const state = buildStateFromFindings(merged, {
|
|
513
546
|
branch,
|
|
514
547
|
readFile: fileReader(process.cwd()),
|
|
515
|
-
previous:
|
|
548
|
+
previous: ctx.storage.readFindings(ctx.review.id),
|
|
516
549
|
now,
|
|
517
550
|
headSha: classification.headSha,
|
|
518
551
|
reviewed: classification.headSha
|
|
519
552
|
? { headSha: classification.headSha, at: now }
|
|
520
553
|
: undefined,
|
|
521
554
|
});
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
555
|
+
ctx.storage.writeFindings(ctx.review.id, state);
|
|
556
|
+
ctx.storage.updateReview(ctx.review.id, {
|
|
557
|
+
base: opts.base,
|
|
558
|
+
head: opts.head,
|
|
559
|
+
headSha: classification.headSha,
|
|
560
|
+
});
|
|
561
|
+
writeRunStatus(ctx.storage, ctx.review.id, { status: "review_done" });
|
|
527
562
|
|
|
528
|
-
|
|
563
|
+
persistContext(ctx);
|
|
564
|
+
const context = ctx.work.readContextSummary() ?? readPersistedContextSummary(ctx);
|
|
529
565
|
const nameOf = (id: string): string => findWatchdog(config, id)?.name ?? id;
|
|
530
566
|
const domains = writeDomainReports(
|
|
531
|
-
|
|
567
|
+
ctx,
|
|
532
568
|
branch,
|
|
533
569
|
reports,
|
|
534
570
|
classification,
|
|
@@ -544,16 +580,27 @@ export function finalize(
|
|
|
544
580
|
jev.available ? jev.candidates : undefined,
|
|
545
581
|
domains,
|
|
546
582
|
);
|
|
547
|
-
|
|
548
|
-
|
|
583
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
584
|
+
name: REVIEW_FILE,
|
|
585
|
+
kind: "review",
|
|
586
|
+
content: `${incrementalNote(classification)}${markdown}\n`,
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const location = ctx.storage.location(ctx.review.id);
|
|
590
|
+
messages.push(
|
|
591
|
+
`Triagem: ${location} — ${Object.keys(state.findings).length} finding(s).`,
|
|
592
|
+
);
|
|
549
593
|
|
|
550
594
|
const blockers = merged.filter((f) => f.severity === "blocking").length;
|
|
551
595
|
const attention = merged.filter((f) => f.severity === "attention").length;
|
|
552
596
|
messages.push(
|
|
553
|
-
`Relatório: ${
|
|
597
|
+
`Relatório: ${location} — ${blockers} bloqueante(s), ${attention} atenção.`,
|
|
554
598
|
);
|
|
555
599
|
|
|
556
|
-
|
|
600
|
+
// O staging já cumpriu o papel (briefs/findings/contexto estão no storage).
|
|
601
|
+
ctx.work.clean();
|
|
602
|
+
|
|
603
|
+
return { reportPath: location, blockers, attention, messages };
|
|
557
604
|
}
|
|
558
605
|
|
|
559
606
|
/** Resolve a branch (opção explícita ou a atual do git). */
|
package/src/core/paths.ts
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
* Caminhos do houndsman (biblioteca): raiz relativa para logs + layout fixo da
|
|
3
3
|
* camada de projeto (`.houndsman/`).
|
|
4
4
|
*
|
|
5
|
-
* A camada gerada tem local fixo, então nada aqui é configurável pelo projeto
|
|
6
|
-
*
|
|
5
|
+
* A camada gerada tem local fixo, então nada aqui é configurável pelo projeto.
|
|
6
|
+
* Dados de execução (reviews/findings/artefatos) vivem no storage global; aqui
|
|
7
|
+
* ficam só os paths da camada e os nomes dos artefatos de staging.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
|
-
import {
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { relative, sep } from "node:path";
|
|
10
12
|
import { OUTPUT_FORMAT_FILE } from "./assets";
|
|
11
13
|
|
|
12
14
|
/** Caminho relativo ao cwd, sempre com `/` (portável/legível em logs). */
|
|
@@ -14,6 +16,16 @@ export function relFromRoot(absPath: string): string {
|
|
|
14
16
|
return relative(process.cwd(), absPath).replace(/\\/g, "/");
|
|
15
17
|
}
|
|
16
18
|
|
|
19
|
+
/** Caminho legível para mensagens: `~/...` quando sob o home, senão absoluto. */
|
|
20
|
+
export function displayPath(absPath: string): string {
|
|
21
|
+
const home = homedir();
|
|
22
|
+
if (absPath === home) return "~";
|
|
23
|
+
if (absPath.startsWith(`${home}${sep}`)) {
|
|
24
|
+
return `~${absPath.slice(home.length).replace(/\\/g, "/")}`;
|
|
25
|
+
}
|
|
26
|
+
return absPath.replace(/\\/g, "/");
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
/** Diretório da camada de projeto (regras + artefatos). */
|
|
18
30
|
export const PROJECT_DIR = ".houndsman";
|
|
19
31
|
|
|
@@ -23,26 +35,20 @@ export const CORE_DIR = `${PROJECT_DIR}/core`;
|
|
|
23
35
|
/** Skill canônico (markdown) dentro da camada gerada. */
|
|
24
36
|
export const CORE_SKILL_DIR = `${CORE_DIR}/skill`;
|
|
25
37
|
|
|
26
|
-
/** Artefatos da revisão (briefs, findings, review.md) — default de `artifactDir`. */
|
|
27
|
-
export const ARTIFACT_DIR = `${PROJECT_DIR}/.runs`;
|
|
28
|
-
|
|
29
38
|
/** Schema de output dos watchdogs, citado nos briefs. */
|
|
30
39
|
export const OUTPUT_FORMAT_PATH = `${CORE_SKILL_DIR}/${OUTPUT_FORMAT_FILE}`;
|
|
31
40
|
|
|
32
|
-
/**
|
|
33
|
-
export const STATE_FILE = "state.json";
|
|
34
|
-
|
|
35
|
-
/** Status da run (revisão/correção), dentro da pasta da run (`<slug>/run.json`). */
|
|
36
|
-
export const RUN_STATUS_FILE = "run.json";
|
|
37
|
-
|
|
38
|
-
/** Relatório da verificação de findings, dentro da pasta da run (`<slug>/verify.md`). */
|
|
39
|
-
export const VERIFY_FILE = "verify.md";
|
|
40
|
-
|
|
41
|
-
/** Contexto bruto da PR (`gh pr view`), dentro da pasta da run. */
|
|
41
|
+
/** Contexto bruto da PR (`gh pr view`), no staging da review. */
|
|
42
42
|
export const CONTEXT_FILE = "context.json";
|
|
43
43
|
|
|
44
|
-
/** Resumo do contexto gravado pelo subagente,
|
|
44
|
+
/** Resumo do contexto gravado pelo subagente, no staging da review. */
|
|
45
45
|
export const CONTEXT_SUMMARY_FILE = "context-summary.json";
|
|
46
46
|
|
|
47
47
|
/** Brief do subagente que resume o contexto da PR. */
|
|
48
48
|
export const CONTEXT_BRIEF_FILE = "context-brief.md";
|
|
49
|
+
|
|
50
|
+
/** Relatório da verificação de findings (artefato `verify.md`). */
|
|
51
|
+
export const VERIFY_FILE = "verify.md";
|
|
52
|
+
|
|
53
|
+
/** Relatório consolidado da review (artefato `review.md`). */
|
|
54
|
+
export const REVIEW_FILE = "review.md";
|
package/src/core/pr-context.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* OPCIONAL: sem binário, sem login ou sem PR para a branch, grava
|
|
6
6
|
* `context.json` com `{ available: false, reason }` e sai com código 0.
|
|
7
7
|
*
|
|
8
|
-
* Artefatos
|
|
8
|
+
* Artefatos no staging da review (`<HOUNDSMAN_HOME>/work/<reviewId>/`):
|
|
9
9
|
* context.json payload bruto do `gh pr view` (ou marcador de indisponível)
|
|
10
10
|
* context-brief.md instruções para o subagente resumir o contexto
|
|
11
11
|
*
|
|
@@ -22,13 +22,19 @@ import {
|
|
|
22
22
|
} from "./paths";
|
|
23
23
|
import type { ContextSummary } from "./report-builder";
|
|
24
24
|
import { parseContextSummary } from "./report-builder";
|
|
25
|
-
import { type ContextPaths, RunLayout } from "./run-layout";
|
|
26
25
|
import { systemClock } from "./services/clock";
|
|
27
26
|
import { defaultGhRunner, GhClient, type GhRunner } from "./services/gh-client";
|
|
28
27
|
import { GitRepository } from "./services/git-repository";
|
|
29
28
|
|
|
30
29
|
export { CONTEXT_BRIEF_FILE, CONTEXT_FILE, CONTEXT_SUMMARY_FILE };
|
|
31
|
-
|
|
30
|
+
|
|
31
|
+
/** Caminhos dos artefatos de contexto da PR (staging da review). */
|
|
32
|
+
export type ContextPaths = {
|
|
33
|
+
dir: string;
|
|
34
|
+
context: string;
|
|
35
|
+
summary: string;
|
|
36
|
+
brief: string;
|
|
37
|
+
};
|
|
32
38
|
|
|
33
39
|
const GH_JSON_FIELDS =
|
|
34
40
|
"number,title,body,url,baseRefName,headRefName,author,commits,files";
|
|
@@ -60,8 +66,16 @@ export type PrContextRaw = {
|
|
|
60
66
|
};
|
|
61
67
|
|
|
62
68
|
/** Caminhos dos artefatos de contexto para uma branch. */
|
|
63
|
-
export function
|
|
64
|
-
|
|
69
|
+
export function contextFileNames(): {
|
|
70
|
+
context: string;
|
|
71
|
+
summary: string;
|
|
72
|
+
brief: string;
|
|
73
|
+
} {
|
|
74
|
+
return {
|
|
75
|
+
context: CONTEXT_FILE,
|
|
76
|
+
summary: CONTEXT_SUMMARY_FILE,
|
|
77
|
+
brief: CONTEXT_BRIEF_FILE,
|
|
78
|
+
};
|
|
65
79
|
}
|
|
66
80
|
|
|
67
81
|
/** Detecta a branch atual; lança se não houver (o CLI decide o exit). */
|