@payconductor/houndsman 0.7.0 → 0.10.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.
Files changed (88) hide show
  1. package/README.md +20 -0
  2. package/package.json +1 -1
  3. package/src/cli/commands/command.ts +10 -1
  4. package/src/cli/commands/context-command.ts +40 -2
  5. package/src/cli/commands/hunt-command.ts +28 -2
  6. package/src/cli/commands/review-command.ts +30 -2
  7. package/src/cli/commands/worktree-command.ts +118 -0
  8. package/src/cli/help.ts +12 -2
  9. package/src/cli/index.ts +14 -4
  10. package/src/core/ai/ai-client.ts +21 -0
  11. package/src/core/ai/ai-sdk-model-provider.ts +43 -18
  12. package/src/core/ai/features/technical-summarizer.ts +43 -0
  13. package/src/core/ai/index.ts +4 -1
  14. package/src/core/ai/prompts/context.ts +3 -3
  15. package/src/core/ai/prompts/technical.ts +30 -0
  16. package/src/core/ai/provider-capabilities.ts +45 -0
  17. package/src/core/ai/schemas.ts +19 -3
  18. package/src/core/ai/types.ts +3 -1
  19. package/src/core/ast/service.ts +7 -2
  20. package/src/core/brief.ts +26 -4
  21. package/src/core/commands/context.ts +9 -1
  22. package/src/core/commands/findings.ts +11 -5
  23. package/src/core/commands/review.ts +13 -0
  24. package/src/core/commands/scaffold.ts +9 -2
  25. package/src/core/config.ts +30 -1
  26. package/src/core/defaults.ts +7 -0
  27. package/src/core/engine/houndsman-engine.ts +26 -3
  28. package/src/core/engine/review-scope.ts +17 -22
  29. package/src/core/engine/review-session.ts +25 -2
  30. package/src/core/engine/review-workspace.ts +204 -0
  31. package/src/core/git-runner.ts +8 -1
  32. package/src/core/jev/jev-detector.ts +10 -2
  33. package/src/core/orchestrator.ts +176 -15
  34. package/src/core/paths.ts +3 -0
  35. package/src/core/pr-context.ts +11 -6
  36. package/src/core/project-config.ts +21 -1
  37. package/src/core/renderers/resume-renderer.ts +17 -34
  38. package/src/core/report-builder.ts +6 -6
  39. package/src/core/services/git-repository.ts +46 -0
  40. package/src/core/services/pr-resolver.ts +147 -0
  41. package/src/core/services/worktree-manager.ts +293 -0
  42. package/src/core/storage/file/file-storage.ts +14 -1
  43. package/src/core/storage/paths.ts +30 -1
  44. package/src/core/storage/sqlite/mappers.ts +4 -0
  45. package/src/core/storage/sqlite/schema.ts +13 -3
  46. package/src/core/storage/sqlite/sqlite-storage.ts +28 -8
  47. package/src/core/storage/types.ts +13 -0
  48. package/src/core/storage/work-area.ts +20 -1
  49. package/src/core/technical-summary.ts +398 -0
  50. package/src/core/types.ts +2 -0
  51. package/src/harness/agent/agent-factory.ts +1 -0
  52. package/src/harness/agent/review-agent.ts +11 -1
  53. package/src/harness/config/harness-config.ts +6 -1
  54. package/src/harness/hunt/hunt-options.ts +17 -0
  55. package/src/harness/hunt/hunt-session.ts +7 -20
  56. package/src/index.ts +33 -0
  57. package/src/server/client/App.tsx +51 -12
  58. package/src/server/client/DeleteRunModal.tsx +163 -0
  59. package/src/server/client/PrsSection.tsx +296 -0
  60. package/src/server/client/ResumePanel.tsx +17 -20
  61. package/src/server/client/RunsSection.tsx +73 -0
  62. package/src/server/client/Sidebar.tsx +171 -358
  63. package/src/server/client/icons.tsx +10 -0
  64. package/src/server/client/sidebar-sections.tsx +73 -0
  65. package/src/server/client/utils/constants.ts +7 -2
  66. package/src/server/client/utils/sidebar-prefs.ts +64 -0
  67. package/src/server/client/utils/types.ts +27 -0
  68. package/src/server/index.ts +113 -406
  69. package/src/server/pr-inbox.ts +15 -4
  70. package/src/server/run-repository.ts +103 -2
  71. package/src/server/styles.css +454 -0
  72. package/src/skill/README.md +11 -3
  73. package/src/skill/SKILL.md +44 -10
  74. package/src/skill/hunt.md +33 -7
  75. package/src/tests/__snapshots__/baseline.test.ts.snap +99 -24
  76. package/src/tests/ai-core.test.ts +71 -0
  77. package/src/tests/ai-features.test.ts +36 -0
  78. package/src/tests/baseline.test.ts +4 -1
  79. package/src/tests/cli.test.ts +2 -1
  80. package/src/tests/harness-agent.test.ts +19 -0
  81. package/src/tests/review-resume.test.ts +36 -8
  82. package/src/tests/review-scope.test.ts +67 -0
  83. package/src/tests/review-scripts.test.ts +2 -2
  84. package/src/tests/server.test.ts +54 -1
  85. package/src/tests/sidebar-sections.test.ts +94 -0
  86. package/src/tests/technical-summary.test.ts +176 -0
  87. package/src/tests/worktree.test.ts +404 -0
  88. package/assets/logo-legacy.png +0 -0
@@ -111,6 +111,44 @@ describe("server API", () => {
111
111
  }
112
112
  });
113
113
 
114
+ test("DELETE /api/runs/:slug apaga review, findings, artefatos e staging", async () => {
115
+ const env = makeEnv();
116
+ try {
117
+ const handler = createRequestHandler(env.config, env);
118
+ const res = await handler(
119
+ new Request("http://localhost/api/runs/main", { method: "DELETE" }),
120
+ );
121
+ expect(res.status).toBe(200);
122
+ const body = (await res.json()) as { deleted: boolean };
123
+ expect(body.deleted).toBe(true);
124
+
125
+ expect(env.storage.getReview(env.reviewId)).toBeNull();
126
+ expect(env.storage.readFindings(env.reviewId)).toBeNull();
127
+ expect(env.storage.listArtifacts(env.reviewId)).toHaveLength(0);
128
+
129
+ const list = await get(handler, "/api/runs");
130
+ const after = (await list.json()) as { runs: unknown[] };
131
+ expect(after.runs).toHaveLength(0);
132
+ } finally {
133
+ env.cleanup();
134
+ }
135
+ });
136
+
137
+ test("DELETE /api/runs/:slug inexistente devolve 404", async () => {
138
+ const env = makeEnv();
139
+ try {
140
+ const handler = createRequestHandler(env.config, env);
141
+ const res = await handler(
142
+ new Request("http://localhost/api/runs/nao-existe", {
143
+ method: "DELETE",
144
+ }),
145
+ );
146
+ expect(res.status).toBe(404);
147
+ } finally {
148
+ env.cleanup();
149
+ }
150
+ });
151
+
114
152
  test("GET findings reconstrói do artefato e PATCH atualiza o estado", async () => {
115
153
  const env = makeEnv();
116
154
  try {
@@ -398,7 +436,19 @@ describe("server API", () => {
398
436
  try {
399
437
  const handler = createRequestHandler(env.config, env);
400
438
  const shell = await get(handler, "/");
401
- expect(await shell.text()).toContain('id="app"');
439
+ const html = await shell.text();
440
+ expect(html).toContain('id="app"');
441
+ expect(html).toContain("/assets/styles.css?v=");
442
+
443
+ // O CSS vive em `styles.css`, servido como arquivo real.
444
+ const css = await get(handler, "/assets/styles.css");
445
+ expect(css.status).toBe(200);
446
+ expect(css.headers.get("content-type")).toContain("text/css");
447
+ const styles = await css.text();
448
+ // Sidebar: rail vertical de seções + painel redimensionável.
449
+ expect(styles).toContain(".side-rail");
450
+ expect(styles).toContain("--side-width");
451
+ expect(styles).not.toContain(".side-tabs");
402
452
 
403
453
  const bundle = await get(handler, "/assets/app.js");
404
454
  expect(bundle.status).toBe(200);
@@ -420,6 +470,9 @@ describe("server API", () => {
420
470
 
421
471
  const bundle = await get(handler, "/assets/app.js");
422
472
  expect(bundle.headers.get("cache-control")).toContain("no-store");
473
+
474
+ const css = await get(handler, "/assets/styles.css");
475
+ expect(css.headers.get("cache-control")).toContain("no-store");
423
476
  } finally {
424
477
  env.cleanup();
425
478
  }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Navegação da sidebar: registry de seções e preferências de layout.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import {
7
+ DEFAULT_SIDEBAR_SECTION,
8
+ getSidebarSection,
9
+ resolveSidebarSectionId,
10
+ SIDEBAR_SECTIONS,
11
+ type SidebarSectionContext,
12
+ } from "../server/client/sidebar-sections";
13
+ import {
14
+ clampSidebarWidth,
15
+ readSidebarPrefs,
16
+ SIDEBAR_DEFAULT_WIDTH,
17
+ SIDEBAR_MAX_WIDTH,
18
+ SIDEBAR_MIN_WIDTH,
19
+ } from "../server/client/utils/sidebar-prefs";
20
+ import type { RunSummary } from "../server/client/utils/types";
21
+
22
+ function makeRun(slug: string): RunSummary {
23
+ return {
24
+ slug,
25
+ branch: slug,
26
+ summary: {
27
+ total: 0,
28
+ present: 0,
29
+ byDecision: { pending: 0, execute: 0, ignore: 0 },
30
+ byExecState: { pending: 0, in_progress: 0, done: 0, stale: 0 },
31
+ bySeverity: { blocking: 0, attention: 0, suggestion: 0, nit: 0 },
32
+ },
33
+ };
34
+ }
35
+
36
+ function makeContext(runs: RunSummary[]): SidebarSectionContext {
37
+ return {
38
+ runs,
39
+ slug: null,
40
+ onSelect: () => undefined,
41
+ onRefreshRuns: () => undefined,
42
+ active: true,
43
+ };
44
+ }
45
+
46
+ describe("SIDEBAR_SECTIONS", () => {
47
+ test("expõe Execuções e PRs na ordem canônica", () => {
48
+ expect(SIDEBAR_SECTIONS.map((section) => section.id)).toEqual(["runs", "prs"]);
49
+ expect(DEFAULT_SIDEBAR_SECTION).toBe("runs");
50
+ });
51
+
52
+ test("cada seção tem identificação e conteúdo", () => {
53
+ const ids = new Set<string>();
54
+ for (const section of SIDEBAR_SECTIONS) {
55
+ expect(section.id).not.toBe("");
56
+ expect(ids.has(section.id)).toBe(false);
57
+ ids.add(section.id);
58
+ expect(section.label.length).toBeGreaterThan(0);
59
+ expect(section.title.length).toBeGreaterThan(0);
60
+ expect(typeof section.icon).toBe("function");
61
+ expect(typeof section.Content).toBe("function");
62
+ }
63
+ });
64
+
65
+ test("o badge de Execuções reflete a contagem de runs", () => {
66
+ const runs = SIDEBAR_SECTIONS.find((section) => section.id === "runs");
67
+ expect(runs?.badge?.(makeContext([]))).toBeNull();
68
+ expect(runs?.badge?.(makeContext([makeRun("a"), makeRun("b")]))).toBe(2);
69
+ });
70
+
71
+ test("resolve ids salvos e cai no default quando inválidos", () => {
72
+ expect(resolveSidebarSectionId("prs")).toBe("prs");
73
+ expect(resolveSidebarSectionId("desconhecida")).toBe(DEFAULT_SIDEBAR_SECTION);
74
+ expect(resolveSidebarSectionId(null)).toBe(DEFAULT_SIDEBAR_SECTION);
75
+ expect(getSidebarSection("desconhecida").id).toBe(DEFAULT_SIDEBAR_SECTION);
76
+ });
77
+ });
78
+
79
+ describe("preferências da sidebar", () => {
80
+ test("clampa a largura entre mínimo e máximo", () => {
81
+ expect(clampSidebarWidth(200)).toBe(SIDEBAR_MIN_WIDTH);
82
+ expect(clampSidebarWidth(900)).toBe(SIDEBAR_MAX_WIDTH);
83
+ expect(clampSidebarWidth(400)).toBe(400);
84
+ expect(clampSidebarWidth(Number.NaN)).toBe(SIDEBAR_DEFAULT_WIDTH);
85
+ });
86
+
87
+ test("lê defaults sem preferências salvas", () => {
88
+ const prefs = readSidebarPrefs();
89
+ expect(prefs.width).toBeGreaterThanOrEqual(SIDEBAR_MIN_WIDTH);
90
+ expect(prefs.width).toBeLessThanOrEqual(SIDEBAR_MAX_WIDTH);
91
+ expect(typeof prefs.collapsed).toBe("boolean");
92
+ if (prefs.section !== null) expect(typeof prefs.section).toBe("string");
93
+ });
94
+ });
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Pontos técnicos estruturais do resumo final — builder determinístico e
3
+ * validação do artefato `technical-summary.json`.
4
+ */
5
+
6
+ import { describe, expect, test } from "bun:test";
7
+ import type { AstAnalysis, AstFacts, AstImpact } from "../core/ast/types";
8
+ import { buildTechnicalSummary, parseTechnicalSummary } from "../core/technical-summary";
9
+
10
+ const facts: AstFacts = {
11
+ language: "typescript",
12
+ symbols: [],
13
+ imports: [],
14
+ exports: [],
15
+ decorators: [],
16
+ hasSyntaxErrors: false,
17
+ };
18
+
19
+ function analysis(partial: Partial<AstAnalysis> = {}): AstAnalysis {
20
+ return {
21
+ facts,
22
+ delta: {
23
+ addedSymbols: [],
24
+ removedSymbols: [],
25
+ modifiedSymbols: [],
26
+ changedSignatures: [],
27
+ addedImports: [],
28
+ removedImports: [],
29
+ changedExports: [],
30
+ },
31
+ ...partial,
32
+ };
33
+ }
34
+
35
+ const classification = {
36
+ files: {
37
+ "src/bin-lookup/service.ts": { status: "added" },
38
+ "src/bin-lookup/provider.ts": { status: "added" },
39
+ },
40
+ churn: {
41
+ "src/bin-lookup/service.ts": { additions: 10, deletions: 0 },
42
+ "src/bin-lookup/provider.ts": { additions: 5, deletions: 0 },
43
+ },
44
+ } as const;
45
+
46
+ describe("buildTechnicalSummary", () => {
47
+ test("extrai contratos, implementações, assinaturas e dependências do AST", () => {
48
+ const ast = new Map<string, AstAnalysis>([
49
+ [
50
+ "src/bin-lookup/service.ts",
51
+ analysis({
52
+ delta: {
53
+ addedSymbols: [
54
+ {
55
+ name: "BinLookupProvider",
56
+ kind: "interface",
57
+ exported: true,
58
+ startLine: 1,
59
+ endLine: 3,
60
+ },
61
+ {
62
+ name: "BinLookupService",
63
+ kind: "class",
64
+ exported: true,
65
+ startLine: 5,
66
+ endLine: 20,
67
+ },
68
+ ],
69
+ removedSymbols: [],
70
+ modifiedSymbols: [],
71
+ changedSignatures: [
72
+ {
73
+ name: "lookup",
74
+ before: "(bin: string)",
75
+ after: "(bin: string, brand: string)",
76
+ exported: true,
77
+ },
78
+ ],
79
+ addedImports: ["./provider", "axios"],
80
+ removedImports: [],
81
+ changedExports: [{ name: "legacy", change: "removed" }],
82
+ },
83
+ }),
84
+ ],
85
+ ]);
86
+
87
+ const summary = buildTechnicalSummary({
88
+ classification,
89
+ ast,
90
+ headSha: "abc",
91
+ });
92
+
93
+ expect(summary.source).toBe("deterministic");
94
+ expect(summary.headSha).toBe("abc");
95
+ const text = summary.highlights.map((item) => `${item.area}: ${item.detail}`);
96
+ expect(text.join("\n")).toContain("Contratos: `BinLookupProvider`");
97
+ expect(text.join("\n")).toContain("Implementações: `BinLookupService`");
98
+ expect(text.join("\n")).toContain("Assinaturas alteradas: `lookup`");
99
+ expect(text.join("\n")).toContain("Novas dependências: `axios`");
100
+ expect(text.join("\n")).not.toContain("./provider");
101
+ expect(text.join("\n")).toContain("Exports alterados: `legacy`");
102
+ });
103
+
104
+ test("sem AST, agrupa arquivos novos por diretório", () => {
105
+ const summary = buildTechnicalSummary({ classification });
106
+ expect(summary.highlights[0]).toEqual({
107
+ area: "Estrutura",
108
+ detail: "Novo diretório `src/bin-lookup` com 2 arquivo(s).",
109
+ });
110
+ });
111
+
112
+ test("registra remoções e impacto", () => {
113
+ const impact: AstImpact = {
114
+ importers: { "src/bin-lookup/service.ts": ["src/a.ts"] },
115
+ affectedFiles: ["src/a.ts"],
116
+ exportedChanges: [
117
+ {
118
+ file: "src/bin-lookup/service.ts",
119
+ symbol: "lookup",
120
+ change: "removed",
121
+ },
122
+ ],
123
+ };
124
+ const summary = buildTechnicalSummary({
125
+ classification: {
126
+ files: {
127
+ "src/old.ts": { status: "deleted" },
128
+ "src/bin-lookup/service.ts": { status: "modified" },
129
+ },
130
+ churn: {},
131
+ },
132
+ impact,
133
+ });
134
+ const text = summary.highlights.map((item) => `${item.area}: ${item.detail}`);
135
+ expect(text.join("\n")).toContain("Remoções: `src/old.ts`");
136
+ expect(text.join("\n")).toContain("Impacto: 1 arquivo(s) importam os alterados");
137
+ });
138
+
139
+ test("não gera destaques quando não há mudança estrutural", () => {
140
+ const summary = buildTechnicalSummary({
141
+ classification: { files: {}, churn: {} },
142
+ });
143
+ expect(summary.highlights).toEqual([]);
144
+ });
145
+ });
146
+
147
+ describe("parseTechnicalSummary", () => {
148
+ test("aceita artefato válido e normaliza o source", () => {
149
+ const summary = parseTechnicalSummary({
150
+ source: "deterministic",
151
+ headSha: "abc",
152
+ overview: "Estrutura",
153
+ highlights: [{ area: "Contratos", detail: "`X`" }],
154
+ });
155
+ expect(summary?.source).toBe("deterministic");
156
+ expect(summary?.highlights[0].area).toBe("Contratos");
157
+ });
158
+
159
+ test("rejeita conteúdo vazio ou inválido", () => {
160
+ expect(parseTechnicalSummary(null)).toBeNull();
161
+ expect(parseTechnicalSummary({})).toBeNull();
162
+ expect(
163
+ parseTechnicalSummary({ highlights: [{ area: "", detail: "" }] }),
164
+ ).toBeNull();
165
+ });
166
+
167
+ test("limita a seis destaques", () => {
168
+ const summary = parseTechnicalSummary({
169
+ highlights: Array.from({ length: 10 }, (_, index) => ({
170
+ area: `Área ${index}`,
171
+ detail: `Detalhe ${index}`,
172
+ })),
173
+ });
174
+ expect(summary?.highlights).toHaveLength(6);
175
+ });
176
+ });