@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
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { mkdtempSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
- import { join } from "node:path";
4
+ import { join, resolve } from "node:path";
5
5
  import type { HoundsmanConfig } from "../core/config";
6
6
  import { buildStateFromFindings, fileReader } from "../core/findings-state";
7
7
  import { dedupeFindings, parseWatchdogReport } from "../core/report-builder";
@@ -10,6 +10,7 @@ import { SQLiteStorageProvider } from "../core/storage/sqlite/sqlite-storage";
10
10
  import type { RepoIdentity } from "../core/storage/types";
11
11
  import { createRequestHandler, startServer } from "../server";
12
12
  import { isSlug } from "../server/artifacts";
13
+ import { clearPrCommentsCache } from "../server/pr-comments";
13
14
  import { fakeHoundsmanConfig } from "./fixtures/fake-project";
14
15
 
15
16
  /** Ambiente fake: banco temporário + review `main` com um finding. */
@@ -220,11 +221,12 @@ describe("server API", () => {
220
221
  const body = (await res.json()) as {
221
222
  available: boolean;
222
223
  target: { number: number };
223
- candidates: unknown[];
224
+ candidates: { decision: string }[];
224
225
  };
225
226
  expect(body.available).toBe(true);
226
227
  expect(body.target.number).toBe(12);
227
228
  expect(body.candidates).toHaveLength(1);
229
+ expect(body.candidates[0].decision).toBe("pending");
228
230
  } finally {
229
231
  env.cleanup();
230
232
  }
@@ -445,3 +447,294 @@ describe("startServer", () => {
445
447
  }
446
448
  });
447
449
  });
450
+
451
+ describe("server — editor, PRs e comentários", () => {
452
+ const gitOk = (stdout = "") => ({ ok: true, code: 0, stdout, stderr: "" });
453
+
454
+ test("GET /api/editor devolve o contexto injetado", async () => {
455
+ const env = makeEnv();
456
+ try {
457
+ const handler = createRequestHandler(env.config, {
458
+ storage: env.storage,
459
+ repo: env.repo,
460
+ editor: { kind: "jetbrains", project: "app", product: "webstorm" },
461
+ });
462
+ const body = await (await get(handler, "/api/editor")).json();
463
+ expect(body).toEqual({
464
+ kind: "jetbrains",
465
+ project: "app",
466
+ product: "webstorm",
467
+ });
468
+ } finally {
469
+ env.cleanup();
470
+ }
471
+ });
472
+
473
+ test("GET /api/prs lista PRs abertas e casa com a run da branch", async () => {
474
+ const env = makeEnv();
475
+ try {
476
+ env.storage.getOrCreateReview({
477
+ repo: env.repo,
478
+ branch: "feat/x",
479
+ slug: "feat-x",
480
+ });
481
+ const calls: string[][] = [];
482
+ const handler = createRequestHandler(env.config, {
483
+ storage: env.storage,
484
+ repo: env.repo,
485
+ gitRunner: (args) => {
486
+ if (args[0] === "status") return gitOk("");
487
+ if (args[0] === "branch") return gitOk("main\n");
488
+ return gitOk("");
489
+ },
490
+ ghRunner: (args) => {
491
+ calls.push(args);
492
+ return ghJson([
493
+ {
494
+ number: 7,
495
+ title: "Feat X",
496
+ headRefName: "feat/x",
497
+ baseRefName: "main",
498
+ author: { login: "ana" },
499
+ updatedAt: "2026-01-02T00:00:00Z",
500
+ isDraft: false,
501
+ reviewDecision: "APPROVED",
502
+ url: "https://github.com/acme/app/pull/7",
503
+ },
504
+ {
505
+ number: 8,
506
+ title: "Outra",
507
+ headRefName: "feat/y",
508
+ baseRefName: "main",
509
+ author: { login: "bia" },
510
+ updatedAt: "2026-01-03T00:00:00Z",
511
+ isDraft: true,
512
+ reviewDecision: "",
513
+ url: "https://github.com/acme/app/pull/8",
514
+ },
515
+ ]);
516
+ },
517
+ });
518
+
519
+ const res = await get(handler, "/api/prs");
520
+ expect(res.status).toBe(200);
521
+ const body = (await res.json()) as {
522
+ available: boolean;
523
+ currentBranch: string;
524
+ worktree: { clean: boolean };
525
+ prs: { number: number; runSlug?: string }[];
526
+ };
527
+ expect(body.available).toBe(true);
528
+ expect(body.currentBranch).toBe("main");
529
+ expect(body.worktree.clean).toBe(true);
530
+ expect(body.prs.map((pr) => pr.number)).toEqual([8, 7]);
531
+ expect(body.prs.find((pr) => pr.number === 7)?.runSlug).toBe("feat-x");
532
+ expect(calls[0]).toEqual([
533
+ "pr",
534
+ "list",
535
+ "--state",
536
+ "open",
537
+ "--limit",
538
+ "50",
539
+ "--json",
540
+ "number,title,headRefName,baseRefName,author,updatedAt,isDraft,reviewDecision,url",
541
+ ]);
542
+ } finally {
543
+ env.cleanup();
544
+ }
545
+ });
546
+
547
+ test("POST /api/prs/checkout bloqueia árvore suja sem chamar gh", async () => {
548
+ const env = makeEnv();
549
+ try {
550
+ let ghCalls = 0;
551
+ const handler = createRequestHandler(env.config, {
552
+ storage: env.storage,
553
+ repo: env.repo,
554
+ gitRunner: (args) =>
555
+ args[0] === "status" ? gitOk(" M src/alpha/f.ts\n") : gitOk(""),
556
+ ghRunner: () => {
557
+ ghCalls++;
558
+ return gitOk("");
559
+ },
560
+ });
561
+
562
+ const res = await handler(
563
+ new Request("http://localhost/api/prs/checkout", {
564
+ method: "POST",
565
+ headers: { "content-type": "application/json" },
566
+ body: JSON.stringify({ number: 7 }),
567
+ }),
568
+ );
569
+ const body = (await res.json()) as {
570
+ ok: boolean;
571
+ reason: string;
572
+ changes?: string[];
573
+ };
574
+ expect(body.ok).toBe(false);
575
+ expect(body.reason).toBe("dirty");
576
+ expect(body.changes).toEqual([" M src/alpha/f.ts"]);
577
+ expect(ghCalls).toBe(0);
578
+ } finally {
579
+ env.cleanup();
580
+ }
581
+ });
582
+
583
+ test("POST /api/prs/checkout usa gh pr checkout quando a árvore está limpa", async () => {
584
+ const env = makeEnv();
585
+ try {
586
+ const calls: string[][] = [];
587
+ const handler = createRequestHandler(env.config, {
588
+ storage: env.storage,
589
+ repo: env.repo,
590
+ gitRunner: (args) => {
591
+ if (args[0] === "status") return gitOk("");
592
+ if (args[0] === "branch") return gitOk("feat/x\n");
593
+ return gitOk("");
594
+ },
595
+ ghRunner: (args) => {
596
+ calls.push(args);
597
+ return gitOk("");
598
+ },
599
+ });
600
+
601
+ const res = await handler(
602
+ new Request("http://localhost/api/prs/checkout", {
603
+ method: "POST",
604
+ headers: { "content-type": "application/json" },
605
+ body: JSON.stringify({ number: 7 }),
606
+ }),
607
+ );
608
+ expect(await res.json()).toEqual({
609
+ ok: true,
610
+ number: 7,
611
+ branch: "feat/x",
612
+ });
613
+ expect(calls).toContainEqual(["pr", "checkout", "7"]);
614
+ } finally {
615
+ env.cleanup();
616
+ }
617
+ });
618
+
619
+ test("GET comments agrupa threads, usa cache e respeita ?refresh=1", async () => {
620
+ const env = makeEnv();
621
+ try {
622
+ clearPrCommentsCache();
623
+ let inlineCalls = 0;
624
+ let issueCalls = 0;
625
+ const handler = createRequestHandler(env.config, {
626
+ storage: env.storage,
627
+ repo: env.repo,
628
+ ghRunner: (args) => {
629
+ if (args[0] === "pr") {
630
+ return ghJson({
631
+ number: 12,
632
+ url: "https://github.com/acme/app/pull/12",
633
+ headRefOid: "deadbeef",
634
+ });
635
+ }
636
+ const path = args[args.length - 1];
637
+ if (path === "repos/acme/app/pulls/12/comments") {
638
+ inlineCalls++;
639
+ return ghJson([
640
+ [
641
+ {
642
+ id: 1,
643
+ body: "raiz",
644
+ path: "src/alpha/f.ts",
645
+ line: 2,
646
+ created_at: "2026-01-01T00:00:00Z",
647
+ html_url:
648
+ "https://github.com/acme/app/pull/12#discussion_r1",
649
+ user: { login: "ana", type: "User" },
650
+ },
651
+ {
652
+ id: 2,
653
+ body: "resposta",
654
+ path: "src/alpha/f.ts",
655
+ line: 2,
656
+ in_reply_to_id: 1,
657
+ created_at: "2026-01-01T00:01:00Z",
658
+ user: { login: "bia", type: "User" },
659
+ },
660
+ ],
661
+ ]);
662
+ }
663
+ if (path === "repos/acme/app/issues/12/comments") {
664
+ issueCalls++;
665
+ return ghJson([
666
+ [
667
+ {
668
+ id: 3,
669
+ body: "geral",
670
+ created_at: "2026-01-01T00:02:00Z",
671
+ user: { login: "copilot[bot]", type: "Bot" },
672
+ },
673
+ ],
674
+ ]);
675
+ }
676
+ return { ok: false, code: 404, stdout: "", stderr: "not found" };
677
+ },
678
+ });
679
+
680
+ const first = (await (
681
+ await get(handler, "/api/runs/main/comments")
682
+ ).json()) as {
683
+ available: boolean;
684
+ counts: { threads: number; inline: number; issue: number };
685
+ threads: {
686
+ path: string;
687
+ absFile: string;
688
+ comments: { author: string }[];
689
+ }[];
690
+ issueComments: { authorType: string }[];
691
+ };
692
+ expect(first.available).toBe(true);
693
+ expect(first.counts).toEqual({ threads: 1, inline: 2, issue: 1 });
694
+ expect(first.threads[0].path).toBe("src/alpha/f.ts");
695
+ expect(first.threads[0].absFile).toBe(
696
+ resolve(env.repo.root, "src/alpha/f.ts"),
697
+ );
698
+ expect(first.threads[0].comments.map((c) => c.author)).toEqual([
699
+ "ana",
700
+ "bia",
701
+ ]);
702
+ expect(first.issueComments[0].authorType).toBe("Bot");
703
+
704
+ await get(handler, "/api/runs/main/comments");
705
+ expect(inlineCalls).toBe(1);
706
+ expect(issueCalls).toBe(1);
707
+
708
+ await get(handler, "/api/runs/main/comments?refresh=1");
709
+ expect(inlineCalls).toBe(2);
710
+ expect(issueCalls).toBe(2);
711
+ } finally {
712
+ clearPrCommentsCache();
713
+ env.cleanup();
714
+ }
715
+ });
716
+
717
+ test("GET comments é fail-open quando o gh não está disponível", async () => {
718
+ const env = makeEnv();
719
+ try {
720
+ clearPrCommentsCache();
721
+ const handler = createRequestHandler(env.config, {
722
+ storage: env.storage,
723
+ repo: env.repo,
724
+ ghRunner: () => ({
725
+ ok: false,
726
+ code: 127,
727
+ stdout: "",
728
+ stderr: "gh: not found",
729
+ }),
730
+ });
731
+ const body = (await (
732
+ await get(handler, "/api/runs/main/comments")
733
+ ).json()) as { available: boolean; reason: string };
734
+ expect(body.available).toBe(false);
735
+ expect(body.reason).toContain("gh");
736
+ } finally {
737
+ env.cleanup();
738
+ }
739
+ });
740
+ });
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Contract test das tools built-in: metadata, schema e erros estruturados.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { AstService } from "../core/ast/service";
7
+ import { Watchdog } from "../core/domain/watchdog";
8
+ import { SilentReporter } from "../harness/agent/agent-reporter";
9
+ import { AgentState } from "../harness/investigation/agent-state";
10
+ import type { ToolContext } from "../harness/tools/agent-tool";
11
+ import { BuiltinToolsModule } from "../harness/tools/builtin";
12
+ import { ToolCache } from "../harness/tools/tool-cache";
13
+ import { ToolCallBudget } from "../harness/tools/tool-call-budget";
14
+ import { ToolMiddleware } from "../harness/tools/tool-middleware";
15
+ import { ToolRegistry } from "../harness/tools/tool-registry";
16
+ import { fakeAgentContext } from "./fixtures/fake-investigation";
17
+ import { fakeHoundsmanConfig } from "./fixtures/fake-project";
18
+
19
+ function registry(): ToolRegistry {
20
+ const value = new ToolRegistry();
21
+ value.registerModule(new BuiltinToolsModule());
22
+ return value;
23
+ }
24
+
25
+ function context(): ToolContext {
26
+ return {
27
+ repoRoot: process.cwd(),
28
+ watchdog: new Watchdog(fakeHoundsmanConfig.watchdogs.alpha),
29
+ diff: { byFile: new Map(), churn: new Map() },
30
+ ast: new AstService(fakeHoundsmanConfig),
31
+ state: new AgentState(),
32
+ budget: new ToolCallBudget(20, 10),
33
+ cache: new ToolCache(),
34
+ logger: new SilentReporter(),
35
+ agent: fakeAgentContext({ repoRoot: process.cwd() }),
36
+ rules: new Map(),
37
+ };
38
+ }
39
+
40
+ describe("contratos das tools", () => {
41
+ test("todas declaram metadata read-only e capabilities", () => {
42
+ for (const tool of registry().list()) {
43
+ expect(tool.metadata.name.length).toBeGreaterThan(0);
44
+ expect(tool.metadata.description.length).toBeGreaterThan(0);
45
+ expect(tool.metadata.readOnly).toBe(true);
46
+ expect(tool.metadata.capabilities.length).toBeGreaterThan(0);
47
+ expect(tool.metadata.cost).toBeGreaterThanOrEqual(1);
48
+ }
49
+ });
50
+
51
+ test("nenhuma tool devolve arquivo inteiro por padrão", () => {
52
+ // `read_file_range` exige faixa explícita.
53
+ const range = registry().get("read_file_range");
54
+ expect(range?.inputSchema.safeParse({ path: "a.ts" }).success).toBe(false);
55
+ // `file_outline` só devolve metadados (não tem parâmetro de corpo).
56
+ const outline = registry().get("file_outline");
57
+ expect(outline?.inputSchema.safeParse({ path: "a.ts" }).success).toBe(true);
58
+ });
59
+
60
+ test("tools de arquivo devolvem erro estruturado (não lançam)", async () => {
61
+ const middleware = new ToolMiddleware();
62
+ for (const [name, input] of [
63
+ ["read_file_range", { path: "../x", startLine: 1, endLine: 2 }],
64
+ ["file_outline", { path: "../x" }],
65
+ ["imports_exports", { file: "../x" }],
66
+ ] as const) {
67
+ const result = await registry().execute(name, input, context(), middleware);
68
+ expect(result.ok).toBe(false);
69
+ expect(typeof result.error).toBe("string");
70
+ }
71
+ });
72
+
73
+ test("rule_signals devolve erro quando a regra não existe", async () => {
74
+ const result = await registry().execute(
75
+ "rule_signals",
76
+ { rule: "regra inexistente" },
77
+ context(),
78
+ new ToolMiddleware(),
79
+ );
80
+ expect(result.ok).toBe(false);
81
+ });
82
+ });
@@ -158,7 +158,7 @@ describe("JevVerifier", () => {
158
158
  ]);
159
159
  expect(outcome.checked).toBe(0);
160
160
  expect(outcome.skipped).toBe(1);
161
- expect(outcome.reason).toContain("TYPESAFE_API_KEY");
161
+ expect(outcome.reason).toContain("HOUNDSMAN_TYPE_SAFE_API_KEY");
162
162
  });
163
163
 
164
164
  test("sem casos ⇒ vazio", async () => {
@@ -1,75 +0,0 @@
1
- /**
2
- * Sidebar: logo do houndsman + lista de execuções (mais recentes primeiro),
3
- * com data/hora, relação de findings presentes e badge de correção pronta.
4
- */
5
-
6
- import { BranchIcon, Clock, Sparkles } from "./icons";
7
- import { describeRunStatus } from "./utils/constants";
8
- import { formatRunTime } from "./utils/format";
9
- import type { RunSummary } from "./utils/types";
10
-
11
- export function RunList({
12
- runs,
13
- slug,
14
- onSelect,
15
- }: {
16
- runs: RunSummary[];
17
- slug: string | null;
18
- onSelect: (slug: string) => void;
19
- }) {
20
- return (
21
- <aside class="sidebar">
22
- <div class="brand">
23
- <img class="logo" src="/assets/logo.png" alt="" />
24
- <div>
25
- <h1>Houndsman</h1>
26
- <p class="sub">Revisões e triagem</p>
27
- </div>
28
- </div>
29
- <p class="section-label">Execuções</p>
30
- <nav class="runs">
31
- {runs.length === 0 && <p class="empty">Nenhuma run encontrada.</p>}
32
- {runs.map((run) => {
33
- const status = describeRunStatus(run);
34
- const fixReady = status?.kind === "fix_done";
35
- return (
36
- <button
37
- type="button"
38
- title={`Abrir a run da branch ${run.branch}`}
39
- class={`run${run.slug === slug ? " active" : ""}`}
40
- onClick={() => onSelect(run.slug)}
41
- >
42
- <span class="run-top">
43
- <BranchIcon size={14} class="run-icon" />
44
- <span class="run-branch">{run.branch}</span>
45
- </span>
46
- <span class="run-meta">
47
- <span class="run-when">
48
- <Clock size={12} />
49
- {formatRunTime(run.updatedAt) || "sem data"}
50
- </span>
51
- <span
52
- class="run-count"
53
- title={`${run.summary.present} de ${run.summary.total} findings presentes`}
54
- >
55
- {run.summary.present}/{run.summary.total}
56
- </span>
57
- </span>
58
- {fixReady ? (
59
- <span class="run-badge">
60
- <Sparkles size={12} /> Correção pronta
61
- </span>
62
- ) : (
63
- status && (
64
- <span class={`run-status status-${status.kind}`}>
65
- {status.label}
66
- </span>
67
- )
68
- )}
69
- </button>
70
- );
71
- })}
72
- </nav>
73
- </aside>
74
- );
75
- }