@payconductor/houndsman 0.5.0 → 0.7.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 (175) hide show
  1. package/README.md +24 -4
  2. package/assets/grammars/tree-sitter-javascript.wasm +0 -0
  3. package/assets/grammars/tree-sitter-tsx.wasm +0 -0
  4. package/assets/grammars/tree-sitter-typescript.wasm +0 -0
  5. package/assets/grammars/web-tree-sitter.wasm +0 -0
  6. package/package.json +11 -3
  7. package/src/cli/commands/command.ts +5 -0
  8. package/src/cli/commands/doctor-command.ts +116 -24
  9. package/src/cli/commands/hunt-command.ts +25 -0
  10. package/src/cli/commands/server-command.ts +7 -1
  11. package/src/cli/help.ts +18 -2
  12. package/src/cli/index.ts +10 -6
  13. package/src/core/ai/ai-client.ts +221 -0
  14. package/src/core/ai/ai-sdk-model-provider.ts +101 -0
  15. package/src/core/ai/errors.ts +36 -0
  16. package/src/core/ai/features/brief-enricher.ts +62 -0
  17. package/src/core/ai/features/classifier.ts +50 -0
  18. package/src/core/ai/features/context-summarizer.ts +31 -0
  19. package/src/core/ai/features/pre-analyzer.ts +51 -0
  20. package/src/core/ai/features/verifier.ts +45 -0
  21. package/src/core/ai/index.ts +56 -0
  22. package/src/core/ai/model-factory.ts +137 -0
  23. package/src/core/ai/prompts/base.ts +36 -0
  24. package/src/core/ai/prompts/brief.ts +87 -0
  25. package/src/core/ai/prompts/classification.ts +36 -0
  26. package/src/core/ai/prompts/context.ts +29 -0
  27. package/src/core/ai/prompts/pre-analysis.ts +49 -0
  28. package/src/core/ai/prompts/verify.ts +40 -0
  29. package/src/core/ai/schemas.ts +110 -0
  30. package/src/core/ai/types.ts +76 -0
  31. package/src/core/ai/usage-budget.ts +75 -0
  32. package/src/core/ai/usage-recorder.ts +79 -0
  33. package/src/core/assets.ts +16 -0
  34. package/src/core/ast/diff.ts +220 -0
  35. package/src/core/ast/impact.ts +127 -0
  36. package/src/core/ast/parser.ts +167 -0
  37. package/src/core/ast/service.ts +233 -0
  38. package/src/core/ast/symbols.ts +294 -0
  39. package/src/core/ast/types.ts +125 -0
  40. package/src/core/brief.ts +138 -0
  41. package/src/core/commands/findings.ts +65 -2
  42. package/src/core/commands/review.ts +2 -0
  43. package/src/core/commands/scaffold.ts +159 -10
  44. package/src/core/commands/update.ts +5 -3
  45. package/src/core/config.ts +85 -0
  46. package/src/core/defaults.ts +27 -0
  47. package/src/core/domain/watchdog.ts +43 -5
  48. package/src/core/engine/houndsman-engine.ts +16 -1
  49. package/src/core/engine/review-scope.ts +52 -6
  50. package/src/core/engine/review-session.ts +27 -2
  51. package/src/core/env.ts +101 -0
  52. package/src/core/file-classifier.ts +25 -4
  53. package/src/core/git-runner.ts +25 -13
  54. package/src/core/github-review.ts +10 -7
  55. package/src/core/jev/jev-client.ts +8 -7
  56. package/src/core/jev/jev-verify.ts +2 -1
  57. package/src/core/logging/index.ts +17 -0
  58. package/src/core/logging/logger.ts +187 -0
  59. package/src/core/orchestrator.ts +357 -9
  60. package/src/core/project-config.ts +123 -1
  61. package/src/core/registry-check.ts +148 -6
  62. package/src/core/renderers/manifest-writer.ts +33 -0
  63. package/src/core/rule-text.ts +97 -0
  64. package/src/core/services/file-classifier.ts +10 -3
  65. package/src/core/services/gh-client.ts +38 -22
  66. package/src/core/services/verifier.ts +14 -3
  67. package/src/core/storage/paths.ts +6 -5
  68. package/src/core/storage/types.ts +6 -0
  69. package/src/core/storage/work-area.ts +12 -0
  70. package/src/core/types.ts +37 -1
  71. package/src/core/verify.ts +17 -5
  72. package/src/harness/README.md +15 -0
  73. package/src/harness/agent/agent-context.ts +33 -0
  74. package/src/harness/agent/agent-errors.ts +30 -0
  75. package/src/harness/agent/agent-factory.ts +45 -0
  76. package/src/harness/agent/agent-reporter.ts +34 -0
  77. package/src/harness/agent/agent-result.ts +16 -0
  78. package/src/harness/agent/agent-runner.ts +32 -0
  79. package/src/harness/agent/agent-scheduler.ts +37 -0
  80. package/src/harness/agent/review-agent.ts +391 -0
  81. package/src/harness/config/harness-config.ts +84 -0
  82. package/src/harness/hunt/hunt-harness.ts +83 -0
  83. package/src/harness/hunt/hunt-options.ts +75 -0
  84. package/src/harness/hunt/hunt-result.ts +17 -0
  85. package/src/harness/hunt/hunt-session.ts +372 -0
  86. package/src/harness/hunt/review-planner.ts +98 -0
  87. package/src/harness/index.ts +36 -0
  88. package/src/harness/investigation/agent-state.ts +134 -0
  89. package/src/harness/investigation/evidence-verifier.ts +129 -0
  90. package/src/harness/investigation/evidence.ts +110 -0
  91. package/src/harness/investigation/finding-critic.ts +96 -0
  92. package/src/harness/investigation/investigation-planner.ts +118 -0
  93. package/src/harness/investigation/investigation-task.ts +10 -0
  94. package/src/harness/investigation/review-journal.ts +87 -0
  95. package/src/harness/investigation/rule-analyzer.ts +178 -0
  96. package/src/harness/investigation/rule-signals.ts +110 -0
  97. package/src/harness/prompts/agent-schemas.ts +57 -0
  98. package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
  99. package/src/harness/prompts/states/conclude-prompt.ts +16 -0
  100. package/src/harness/prompts/states/investigate-prompt.ts +37 -0
  101. package/src/harness/prompts/states/orient-prompt.ts +15 -0
  102. package/src/harness/prompts/states/verify-prompt.ts +22 -0
  103. package/src/harness/tools/agent-tool.ts +102 -0
  104. package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
  105. package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
  106. package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
  107. package/src/harness/tools/builtin/find-references-tool.ts +66 -0
  108. package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
  109. package/src/harness/tools/builtin/helpers.ts +115 -0
  110. package/src/harness/tools/builtin/impact-tool.ts +48 -0
  111. package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
  112. package/src/harness/tools/builtin/index.ts +59 -0
  113. package/src/harness/tools/builtin/list-files-tool.ts +85 -0
  114. package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
  115. package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
  116. package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
  117. package/src/harness/tools/builtin/search-code-tool.ts +65 -0
  118. package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
  119. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
  120. package/src/harness/tools/path-guard.ts +90 -0
  121. package/src/harness/tools/result-limiter.ts +49 -0
  122. package/src/harness/tools/secret-redactor.ts +54 -0
  123. package/src/harness/tools/tool-cache.ts +50 -0
  124. package/src/harness/tools/tool-call-budget.ts +60 -0
  125. package/src/harness/tools/tool-context.ts +5 -0
  126. package/src/harness/tools/tool-middleware.ts +105 -0
  127. package/src/harness/tools/tool-policy.ts +51 -0
  128. package/src/harness/tools/tool-registry.ts +112 -0
  129. package/src/harness/tools/tool-result.ts +15 -0
  130. package/src/harness/tools/toolbox.ts +36 -0
  131. package/src/harness/validation/finding-validator.ts +119 -0
  132. package/src/index.ts +46 -0
  133. package/src/server/client/App.tsx +38 -12
  134. package/src/server/client/CommentsPanel.tsx +255 -0
  135. package/src/server/client/FindingCard.tsx +12 -4
  136. package/src/server/client/FindingsList.tsx +4 -1
  137. package/src/server/client/Sidebar.tsx +397 -0
  138. package/src/server/client/SubmitModal.tsx +61 -5
  139. package/src/server/client/icons.tsx +19 -0
  140. package/src/server/client/index.tsx +1 -1
  141. package/src/server/client/utils/constants.ts +29 -5
  142. package/src/server/client/utils/format.ts +47 -2
  143. package/src/server/client/utils/types.ts +88 -0
  144. package/src/server/editor.ts +129 -0
  145. package/src/server/index.ts +115 -5
  146. package/src/server/pr-comments.ts +256 -0
  147. package/src/server/pr-inbox.ts +202 -0
  148. package/src/server/run-repository.ts +33 -1
  149. package/src/skill/OUTPUT-FORMAT.md +12 -0
  150. package/src/skill/SETUP.md +6 -2
  151. package/src/skill/hunt.md +14 -2
  152. package/src/skill/verify.md +1 -1
  153. package/src/tests/__snapshots__/baseline.test.ts.snap +96 -14
  154. package/src/tests/ai-core.test.ts +120 -0
  155. package/src/tests/ai-features.test.ts +183 -0
  156. package/src/tests/ast.test.ts +335 -0
  157. package/src/tests/cli.test.ts +33 -0
  158. package/src/tests/env.test.ts +69 -0
  159. package/src/tests/fallback.test.ts +172 -0
  160. package/src/tests/fixtures/fake-agent-tools.ts +69 -0
  161. package/src/tests/fixtures/fake-investigation.ts +64 -0
  162. package/src/tests/fixtures/fake-model-provider.ts +48 -0
  163. package/src/tests/github-review.test.ts +9 -0
  164. package/src/tests/harness-agent.test.ts +122 -0
  165. package/src/tests/harness-investigation.test.ts +206 -0
  166. package/src/tests/harness-tools.test.ts +148 -0
  167. package/src/tests/hunt-command.test.ts +212 -0
  168. package/src/tests/logger.test.ts +83 -0
  169. package/src/tests/review-scope.test.ts +11 -11
  170. package/src/tests/rule-analyzer.test.ts +61 -0
  171. package/src/tests/server-editor.test.ts +100 -0
  172. package/src/tests/server.test.ts +295 -2
  173. package/src/tests/tool-contracts.test.ts +82 -0
  174. package/src/tests/verify.test.ts +1 -1
  175. package/src/server/client/RunList.tsx +0 -75
@@ -64,6 +64,12 @@ export type ArtifactKind =
64
64
  | "resume"
65
65
  | "domain-review"
66
66
  | "verify"
67
+ | "ast"
68
+ | "ast-impact"
69
+ | "ai"
70
+ | "ai-usage"
71
+ | "ai-brief"
72
+ | "harness-transcript"
67
73
  | "other";
68
74
 
69
75
  /** Metadados de artefato retornados pela API (sem conteúdo). */
@@ -91,6 +97,86 @@ export type FindingsResponse = {
91
97
  summary: Summary;
92
98
  };
93
99
 
100
+ /** Editor detectado pelo servidor (base dos links de código). */
101
+ export type EditorKind = "vscode" | "jetbrains" | "none";
102
+
103
+ export type EditorContext = {
104
+ kind: EditorKind;
105
+ project?: string;
106
+ product?: string;
107
+ };
108
+
109
+ /** Estado da árvore de trabalho (bloqueia o checkout quando suja). */
110
+ export type WorktreeState = {
111
+ available: boolean;
112
+ clean: boolean;
113
+ changes: number;
114
+ sample: string[];
115
+ };
116
+
117
+ /** PR aberta exibida na sidebar. */
118
+ export type PrListItem = {
119
+ number: number;
120
+ title: string;
121
+ headRefName: string;
122
+ baseRefName: string;
123
+ author: string;
124
+ updatedAt?: string;
125
+ isDraft: boolean;
126
+ reviewDecision?: string;
127
+ url?: string;
128
+ runSlug?: string;
129
+ };
130
+
131
+ export type PrListResponse = {
132
+ available: boolean;
133
+ reason?: string;
134
+ currentBranch: string | null;
135
+ worktree: WorktreeState;
136
+ prs: PrListItem[];
137
+ };
138
+
139
+ export type CheckoutResponse =
140
+ | { ok: true; number: number; branch: string | null }
141
+ | {
142
+ ok: false;
143
+ reason: "dirty" | "git" | "gh";
144
+ detail: string;
145
+ changes?: string[];
146
+ };
147
+
148
+ /** Comentário normalizado da PR (thread ou geral). */
149
+ export type PrCommentView = {
150
+ id: number;
151
+ author: string;
152
+ authorType: string;
153
+ body: string;
154
+ createdAt?: string;
155
+ url?: string;
156
+ path?: string;
157
+ line: number | null;
158
+ absFile?: string;
159
+ replyTo?: number | null;
160
+ };
161
+
162
+ export type PrCommentThread = {
163
+ id: number;
164
+ path: string;
165
+ line: number | null;
166
+ absFile: string;
167
+ comments: PrCommentView[];
168
+ };
169
+
170
+ export type PrCommentsResponse = {
171
+ available: boolean;
172
+ reason?: string;
173
+ target?: { number: number; repo: string; url?: string };
174
+ fetchedAt: string;
175
+ threads: PrCommentThread[];
176
+ issueComments: PrCommentView[];
177
+ counts: { threads: number; inline: number; issue: number };
178
+ };
179
+
94
180
  /** Manifesto da run (subset consumido pela aba de resumo). */
95
181
  export type RunManifest = {
96
182
  branch?: string;
@@ -143,6 +229,8 @@ export type SubmitCandidate = {
143
229
  file: string;
144
230
  line: number | null;
145
231
  severity: Severity;
232
+ /** Decisão da triagem (`execute` = "a executar"). */
233
+ decision: Decision;
146
234
  execState: ExecState;
147
235
  rule: string;
148
236
  message: string;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Detecção do editor que hospeda o terminal do servidor (biblioteca do servidor).
3
+ *
4
+ * VS Code e JetBrains expõem variáveis próprias no terminal integrado. O
5
+ * contexto alimenta os links de código da UI (`vscode://` / `jetbrains://`).
6
+ * A detecção é pura e fail-open: sem pistas, o editor fica `none`.
7
+ */
8
+
9
+ import { basename, resolve } from "node:path";
10
+
11
+ /** Editores com esquema de URL próprio. */
12
+ export type EditorKind = "vscode" | "jetbrains" | "none";
13
+
14
+ /** Contexto usado pelo cliente para montar os links de código. */
15
+ export type EditorContext = {
16
+ kind: EditorKind;
17
+ /** Nome do projeto aberto no IDE (basename da raiz do repositório). */
18
+ project?: string;
19
+ /** Código do produto JetBrains (`idea`, `webstorm`, `pycharm`…). */
20
+ product?: string;
21
+ };
22
+
23
+ type Env = Record<string, string | undefined>;
24
+
25
+ /** Bundle macOS → código do produto usado no esquema `jetbrains://`. */
26
+ const BUNDLE_PRODUCTS: Record<string, string> = {
27
+ "com.jetbrains.intellij": "idea",
28
+ "com.jetbrains.intellij.ce": "idea",
29
+ "com.jetbrains.pycharm": "pycharm",
30
+ "com.jetbrains.pycharm.ce": "pycharm",
31
+ "com.jetbrains.webstorm": "webstorm",
32
+ "com.jetbrains.goland": "goland",
33
+ "com.jetbrains.clion": "clion",
34
+ "com.jetbrains.rider": "rider",
35
+ "com.jetbrains.phpstorm": "phpstorm",
36
+ "com.jetbrains.rubymine": "rubymine",
37
+ "com.jetbrains.datagrip": "datagrip",
38
+ "com.jetbrains.appcode": "appcode",
39
+ "com.jetbrains.rustrover": "rustrover",
40
+ };
41
+
42
+ /** Variáveis `*_INITIAL_DIRECTORY` → produto JetBrains. */
43
+ const DIR_PRODUCTS: [string, string][] = [
44
+ ["IDEA_INITIAL_DIRECTORY", "idea"],
45
+ ["PYCHARM_INITIAL_DIRECTORY", "pycharm"],
46
+ ["WEBSTORM_INITIAL_DIRECTORY", "webstorm"],
47
+ ["GOLAND_INITIAL_DIRECTORY", "goland"],
48
+ ["CLION_INITIAL_DIRECTORY", "clion"],
49
+ ["RIDER_INITIAL_DIRECTORY", "rider"],
50
+ ["PHPSTORM_INITIAL_DIRECTORY", "phpstorm"],
51
+ ["RUBYMINE_INITIAL_DIRECTORY", "rubymine"],
52
+ ["DATAGRIP_INITIAL_DIRECTORY", "datagrip"],
53
+ ["RUSTROVER_INITIAL_DIRECTORY", "rustrover"],
54
+ ];
55
+
56
+ function projectName(repoRoot: string): string {
57
+ return basename(resolve(repoRoot));
58
+ }
59
+
60
+ /** Produto JetBrains detectado no ambiente (undefined quando ambíguo). */
61
+ export function detectJetBrainsProduct(env: Env): string | undefined {
62
+ const bundle = env.__CFBundleIdentifier?.toLowerCase();
63
+ if (bundle) {
64
+ const exact = BUNDLE_PRODUCTS[bundle];
65
+ if (exact) return exact;
66
+ const prefix = Object.keys(BUNDLE_PRODUCTS).find((key) => bundle.startsWith(key));
67
+ if (prefix) return BUNDLE_PRODUCTS[prefix];
68
+ }
69
+ for (const [name, product] of DIR_PRODUCTS) {
70
+ if (env[name]) return product;
71
+ }
72
+ return undefined;
73
+ }
74
+
75
+ function isJetBrains(env: Env): boolean {
76
+ const bundle = env.__CFBundleIdentifier?.toLowerCase() ?? "";
77
+ return (
78
+ /jetbrains/i.test(env.TERMINAL_EMULATOR ?? "") ||
79
+ bundle.startsWith("com.jetbrains.") ||
80
+ DIR_PRODUCTS.some(([name]) => Boolean(env[name]))
81
+ );
82
+ }
83
+
84
+ function isVsCode(env: Env): boolean {
85
+ return (
86
+ env.TERM_PROGRAM === "vscode" ||
87
+ env.TERM_PROGRAM === "vscode-insiders" ||
88
+ Boolean(env.VSCODE_PID || env.VSCODE_IPC_HOOK_CLI || env.VSCODE_GIT_IPC_HANDLE)
89
+ );
90
+ }
91
+
92
+ /** Detecta o editor a partir do ambiente do terminal integrado. */
93
+ export function detectEditor(env: Env, repoRoot: string): EditorContext {
94
+ const project = projectName(repoRoot);
95
+ if (isJetBrains(env)) {
96
+ return {
97
+ kind: "jetbrains",
98
+ project,
99
+ product: detectJetBrainsProduct(env) ?? "idea",
100
+ };
101
+ }
102
+ if (isVsCode(env)) return { kind: "vscode", project };
103
+ return { kind: "none", project };
104
+ }
105
+
106
+ /**
107
+ * Resolve a preferência do `--editor`.
108
+ *
109
+ * `auto` (ou ausente) detecta do ambiente; `vscode`/`jetbrains`/`none` forçam
110
+ * o comportamento. Valores desconhecidos caem na detecção automática.
111
+ */
112
+ export function resolveEditor(
113
+ preference: string | undefined,
114
+ env: Env,
115
+ repoRoot: string,
116
+ ): EditorContext {
117
+ const value = preference?.trim().toLowerCase();
118
+ const project = projectName(repoRoot);
119
+ if (value === "vscode") return { kind: "vscode", project };
120
+ if (value === "jetbrains") {
121
+ return {
122
+ kind: "jetbrains",
123
+ project,
124
+ product: detectJetBrainsProduct(env) ?? "idea",
125
+ };
126
+ }
127
+ if (value === "none") return { kind: "none", project };
128
+ return detectEditor(env, repoRoot);
129
+ }
@@ -9,15 +9,20 @@
9
9
  import { mediaPath } from "../core/assets";
10
10
  import type { HoundsmanConfig } from "../core/config";
11
11
  import type { FindingUpdate } from "../core/findings-state";
12
+ import { createGitRunner, type GitRunner } from "../core/git-runner";
12
13
  import {
13
14
  defaultGhRunner,
14
15
  type GhRunner,
15
16
  type SubmitSelection,
16
17
  } from "../core/github-review";
18
+ import { logger } from "../core/logging";
19
+ import { GitRepository } from "../core/services/git-repository";
17
20
  import { createStorage } from "../core/storage/factory";
18
21
  import { resolveRepoIdentity } from "../core/storage/repo-identity";
19
22
  import type { RepoIdentity, StorageProvider } from "../core/storage/types";
20
23
  import { buildClientBundle } from "./client-build";
24
+ import { type EditorContext, resolveEditor } from "./editor";
25
+ import { PrInbox } from "./pr-inbox";
21
26
  import { RunRepository } from "./run-repository";
22
27
 
23
28
  type BunServer = ReturnType<typeof Bun.serve>;
@@ -58,8 +63,13 @@ function notFound(detail = "não encontrado"): Response {
58
63
  /** Repositório das runs (dados) — ver `run-repository.ts`. */
59
64
  type ServerDeps = {
60
65
  ghRunner?: GhRunner;
66
+ gitRunner?: GitRunner;
61
67
  storage?: StorageProvider;
62
68
  repo?: RepoIdentity;
69
+ /** Contexto do editor injetado (testes). */
70
+ editor?: EditorContext;
71
+ /** Preferência do `--editor` (CLI). */
72
+ editorPreference?: string;
63
73
  };
64
74
 
65
75
  // ---------------------------------------------------------------------------
@@ -82,6 +92,8 @@ async function route(
82
92
  request: Request,
83
93
  runs: RunRepository,
84
94
  ghRunner: GhRunner,
95
+ prs: PrInbox,
96
+ editor: EditorContext,
85
97
  ): Promise<Response> {
86
98
  const url = new URL(request.url);
87
99
  const path = url.pathname;
@@ -94,6 +106,21 @@ async function route(
94
106
  }
95
107
  if (path === `/assets/${LOGO_FILE}`) return serveLogo();
96
108
  if (path === "/api/health") return json({ ok: true });
109
+ if (path === "/api/editor" && request.method === "GET") return json(editor);
110
+ if (path === "/api/prs" && request.method === "GET") {
111
+ return json(prs.list(runs.branchIndex()));
112
+ }
113
+ if (path === "/api/prs/checkout" && request.method === "POST") {
114
+ const body = (await request.json().catch(() => ({}))) as { number?: unknown };
115
+ const number = Number(body.number);
116
+ if (!Number.isInteger(number) || number <= 0) {
117
+ return json(
118
+ { ok: false, reason: "invalid", detail: "Número de PR inválido." },
119
+ 400,
120
+ );
121
+ }
122
+ return json(prs.checkout(number));
123
+ }
97
124
 
98
125
  if (path === "/api/runs" && request.method === "GET") {
99
126
  return json({ runs: runs.list() });
@@ -107,6 +134,11 @@ async function route(
107
134
 
108
135
  if (rest === "" || rest === "/") return json(runs.detail(slug));
109
136
 
137
+ if (rest === "/comments" && request.method === "GET") {
138
+ return json(
139
+ runs.comments(slug, { refresh: url.searchParams.get("refresh") === "1" }),
140
+ );
141
+ }
110
142
  if (rest === "/findings" && request.method === "GET") {
111
143
  return json(runs.findings(slug, url.searchParams));
112
144
  }
@@ -118,6 +150,7 @@ async function route(
118
150
  const body = (await request.json().catch(() => ({}))) as SubmitSelection;
119
151
  const selection: SubmitSelection = {
120
152
  severities: body.severities,
153
+ decisions: body.decisions,
121
154
  execStates: body.execStates,
122
155
  includeSent: body.includeSent,
123
156
  };
@@ -160,19 +193,27 @@ async function route(
160
193
  export class HoundsmanServer {
161
194
  private readonly runs: RunRepository;
162
195
  private readonly ghRunner: GhRunner;
196
+ private readonly prs: PrInbox;
197
+ private readonly editor: EditorContext;
163
198
 
164
199
  constructor(config: HoundsmanConfig, deps: ServerDeps = {}) {
200
+ const repo = deps.repo ?? resolveRepoIdentity();
201
+ this.ghRunner = deps.ghRunner ?? defaultGhRunner;
165
202
  this.runs = new RunRepository(
166
203
  deps.storage ?? createStorage(config),
167
- deps.repo ?? resolveRepoIdentity(),
204
+ repo,
205
+ this.ghRunner,
168
206
  );
169
- this.ghRunner = deps.ghRunner ?? defaultGhRunner;
207
+ this.editor =
208
+ deps.editor ?? resolveEditor(deps.editorPreference, process.env, repo.root);
209
+ const git = new GitRepository(deps.gitRunner ?? createGitRunner(repo.root));
210
+ this.prs = new PrInbox(this.ghRunner, git);
170
211
  }
171
212
 
172
213
  /** Handler de requisições (testável sem abrir porta). */
173
214
  readonly handler = async (request: Request): Promise<Response> => {
174
215
  try {
175
- return await route(request, this.runs, this.ghRunner);
216
+ return await route(request, this.runs, this.ghRunner, this.prs, this.editor);
176
217
  } catch (error) {
177
218
  return json(
178
219
  { error: error instanceof Error ? error.message : String(error) },
@@ -190,8 +231,8 @@ export class HoundsmanServer {
190
231
  try {
191
232
  this.runs.maintain();
192
233
  } catch (error) {
193
- console.warn(
194
- `[houndsman] VACUUM falhou (seguindo sem compactar): ${
234
+ logger.warn(
235
+ `VACUUM falhou (seguindo sem compactar): ${
195
236
  error instanceof Error ? error.message : String(error)
196
237
  }`,
197
238
  );
@@ -222,17 +263,26 @@ export type ServerOptions = {
222
263
  host?: string;
223
264
  /** Runner do `gh` (injetável nos testes). */
224
265
  ghRunner?: GhRunner;
266
+ /** Runner do git (injetável nos testes). */
267
+ gitRunner?: GitRunner;
225
268
  /** Provider/storage injetável (testes). */
226
269
  storage?: StorageProvider;
227
270
  repo?: RepoIdentity;
271
+ /** Contexto de editor injetado (testes). */
272
+ editor?: EditorContext;
273
+ /** Preferência do `--editor` (auto|vscode|jetbrains|none). */
274
+ editorPreference?: string;
228
275
  };
229
276
 
230
277
  /** Sobe o servidor e devolve a instância + URL. */
231
278
  export function startServer(options: ServerOptions): { server: BunServer; url: string } {
232
279
  return new HoundsmanServer(options.config, {
233
280
  ghRunner: options.ghRunner,
281
+ gitRunner: options.gitRunner,
234
282
  storage: options.storage,
235
283
  repo: options.repo,
284
+ editor: options.editor,
285
+ editorPreference: options.editorPreference,
236
286
  }).start({
237
287
  port: options.port,
238
288
  host: options.host,
@@ -294,6 +344,66 @@ code, pre, .mono { font-family:var(--font-mono); tab-size:4; }
294
344
  .status-review_done { color:var(--suggestion); }
295
345
  .status-fix_done { color:var(--success); }
296
346
  .status-timeout { color:var(--blocking); }
347
+ .side-tabs { display:flex; gap:4px; padding:4px; background:var(--surface-2); border:1px solid var(--border); border-radius:999px; }
348
+ .side-tab { flex:1; display:inline-flex; align-items:center; justify-content:center; gap:7px; font:inherit; font-size:12px; font-weight:600; cursor:pointer; color:var(--muted);
349
+ padding:7px 10px; background:transparent; border:1px solid transparent; border-radius:999px; transition:color .15s ease, background .15s ease; }
350
+ .side-tab:hover { color:var(--fg); }
351
+ .side-tab.active { color:#1b1305; background:linear-gradient(180deg, var(--accent-strong), var(--accent)); }
352
+ .side-count { padding:1px 6px; font-family:var(--font-mono); font-size:10px; color:var(--muted); background:rgba(255,255,255,.08); border-radius:999px; }
353
+ .side-tab.active .side-count { color:#1b1305; background:rgba(0,0,0,.14); }
354
+ .icon-btn { display:inline-grid; place-items:center; width:26px; height:26px; color:var(--muted); background:transparent; border:1px solid var(--border); border-radius:8px; cursor:pointer; text-decoration:none; }
355
+ .icon-btn:hover { color:var(--fg); border-color:var(--border-strong); }
356
+ .icon-btn:disabled { opacity:.5; cursor:not-allowed; }
357
+ .pr-toolbar { display:flex; align-items:center; justify-content:space-between; gap:8px; }
358
+ .pr-toolbar .section-label { margin:0; }
359
+ .prs { display:flex; flex-direction:column; gap:8px; }
360
+ .pr { display:flex; flex-direction:column; gap:7px; padding:11px 12px; background:var(--card); border:1px solid var(--border); border-radius:var(--radius); }
361
+ .pr.current { background:var(--accent-soft); border-color:var(--accent-line); }
362
+ .pr-head { display:flex; align-items:center; gap:7px; font-family:var(--font-mono); font-size:11px; color:var(--muted); }
363
+ .pr-icon { color:var(--muted-2); }
364
+ .pr.current .pr-icon { color:var(--accent); }
365
+ .pr-number { font-weight:600; color:var(--fg); }
366
+ .pr-current { padding:1px 6px; font-size:9px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; color:var(--accent); background:rgba(240,165,44,.12); border-radius:999px; }
367
+ .pr-title { margin:0; font-size:12.5px; font-weight:600; line-height:1.4; word-break:break-word; }
368
+ .pr-meta { display:flex; flex-wrap:wrap; align-items:center; gap:1px 5px; margin:0; font-family:var(--font-mono); font-size:10.5px; color:var(--muted-2); }
369
+ .pr-branch { color:var(--suggestion); }
370
+ .pr-review { margin:0; font-size:10.5px; font-weight:600; }
371
+ .pr-review-APPROVED { color:var(--success); }
372
+ .pr-review-CHANGES_REQUESTED { color:var(--blocking); }
373
+ .pr-review-REVIEW_REQUIRED { color:var(--attention); }
374
+ .pr-actions { display:flex; flex-wrap:wrap; gap:6px; margin-top:2px; }
375
+ .pr-btn { display:inline-flex; align-items:center; gap:5px; font:inherit; font-size:10.5px; font-weight:600; cursor:pointer; padding:4px 9px; color:var(--muted);
376
+ background:transparent; border:1px solid var(--border-strong); border-radius:999px; text-decoration:none; }
377
+ .pr-btn:hover { color:var(--fg); border-color:var(--accent-line); }
378
+ .pr-btn:disabled { opacity:.45; cursor:not-allowed; }
379
+ .pr-btn.danger { color:var(--accent); background:var(--accent-soft); border-color:var(--accent-line); }
380
+ .pr-warning { margin:0; padding:8px 10px; font-size:11px; line-height:1.5; color:var(--attention);
381
+ background:rgba(237,161,58,.08); border:1px solid rgba(237,161,58,.24); border-radius:10px; }
382
+ .pr-notice { margin:0; padding:8px 10px; font-size:11px; line-height:1.5; color:var(--success);
383
+ background:rgba(69,192,122,.08); border:1px solid rgba(69,192,122,.24); border-radius:10px; }
384
+ .pr-current-branch { margin:0; font-family:var(--font-mono); font-size:10.5px; color:var(--muted-2); }
385
+ .pr-current-branch code { color:var(--fg); }
386
+ .comments { display:flex; flex-direction:column; gap:18px; }
387
+ .comments-toolbar { display:flex; align-items:center; justify-content:space-between; gap:12px; }
388
+ .comments-counts { display:flex; flex-wrap:wrap; align-items:center; gap:12px; font-size:11.5px; color:var(--muted); }
389
+ .comments-counts b { font-family:var(--font-mono); font-weight:600; color:var(--fg); }
390
+ .comment-group { display:flex; flex-direction:column; gap:10px; }
391
+ .comment-group-title { margin:0; font-size:11px; font-weight:700; letter-spacing:.16em; text-transform:uppercase; color:var(--muted-2); }
392
+ .comment-file-group { display:flex; flex-direction:column; gap:8px; }
393
+ .comment-group-label { display:flex; align-items:center; gap:8px; margin:0; font-family:var(--font-mono); font-size:11.5px; color:var(--suggestion); }
394
+ .comment-thread { display:flex; flex-direction:column; overflow:hidden; background:var(--card); border:1px solid var(--border); border-radius:var(--radius-lg); }
395
+ .comment-thread-head { display:flex; align-items:center; gap:8px; padding:9px 13px; font-family:var(--font-mono); font-size:11px; color:var(--muted);
396
+ background:var(--surface-2); border-bottom:1px solid var(--border); }
397
+ .comment-file { overflow:hidden; color:var(--fg); text-overflow:ellipsis; white-space:nowrap; }
398
+ .pr-comment { display:flex; flex-direction:column; gap:7px; padding:12px 14px; border-top:1px solid var(--border); }
399
+ .comment-thread .pr-comment:first-of-type { border-top:0; }
400
+ .pr-comment.reply { margin-left:26px; background:rgba(255,255,255,.012); border-left:2px solid var(--accent-line); }
401
+ .pr-comment-head { display:flex; align-items:center; gap:9px; font-size:11px; color:var(--muted-2); }
402
+ .pr-comment-author { font-size:12px; font-weight:600; color:var(--fg); }
403
+ .pr-comment-link { display:inline-flex; align-items:center; gap:5px; font-size:11px; color:var(--muted); text-decoration:none; }
404
+ .pr-comment-link:hover { color:var(--accent); }
405
+ .pr-comment-body { font-size:12.5px; line-height:1.6; color:#f0ece6; }
406
+ .pr-comment-body .md-p, .pr-comment-body .md-list { font-size:12.5px; }
297
407
  .main { flex:1; min-width:0; display:flex; flex-direction:column; overflow-y:auto; }
298
408
  .topbar { position:sticky; top:0; z-index:10; display:flex; align-items:center; justify-content:space-between; gap:16px;
299
409
  padding:13px 24px; border-bottom:1px solid var(--border); background:rgba(16,16,20,.74);