karajan-code 1.59.0 → 2.0.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 (115) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -1
  3. package/scripts/install.js +18 -15
  4. package/src/agents/aider-agent.js +0 -7
  5. package/src/agents/claude-agent.js +34 -6
  6. package/src/agents/codex-agent.js +2 -6
  7. package/src/agents/gemini-agent.js +0 -4
  8. package/src/agents/opencode-agent.js +0 -4
  9. package/src/{becaria → ci}/dispatch.js +13 -13
  10. package/src/{becaria → ci}/pr-diff.js +1 -1
  11. package/src/cli.js +3 -2
  12. package/src/commands/agents.js +0 -1
  13. package/src/commands/doctor.js +22 -40
  14. package/src/commands/init.js +8 -8
  15. package/src/commands/report.js +0 -13
  16. package/src/commands/review.js +17 -17
  17. package/src/commands/run.js +1 -2
  18. package/src/commands/status.js +1 -7
  19. package/src/config.js +10 -52
  20. package/src/domains/domain-loader.js +85 -4
  21. package/src/git/automation.js +9 -9
  22. package/src/guards/output-guard.js +0 -17
  23. package/src/mcp/handlers/direct-handlers.js +1 -1
  24. package/src/mcp/handlers/run-handler.js +1 -2
  25. package/src/mcp/sovereignty-guard.js +2 -2
  26. package/src/mcp/tools.js +3 -2
  27. package/src/orchestrator/brain-coordinator.js +165 -0
  28. package/src/orchestrator/ci-integration.js +139 -0
  29. package/src/orchestrator/config-init.js +435 -0
  30. package/src/orchestrator/direct-actions.js +186 -0
  31. package/src/orchestrator/feedback-enrichment.js +140 -0
  32. package/src/orchestrator/feedback-queue.js +152 -0
  33. package/src/orchestrator/flow-control.js +159 -0
  34. package/src/orchestrator/post-loop-stages.js +20 -15
  35. package/src/orchestrator/preflight-checks.js +7 -3
  36. package/src/orchestrator/role-output-compressor.js +190 -0
  37. package/src/orchestrator/session-journal.js +344 -0
  38. package/src/orchestrator/solomon-escalation.js +38 -5
  39. package/src/orchestrator/solomon-rules.js +32 -33
  40. package/src/orchestrator/stages/reviewer-stage.js +16 -5
  41. package/src/orchestrator/stages/sonar-stage.js +24 -1
  42. package/src/orchestrator/verification-gate.js +146 -0
  43. package/src/orchestrator.js +435 -661
  44. package/src/planning-game/client.js +2 -2
  45. package/src/planning-game/pipeline-adapter.js +1 -1
  46. package/src/prompts/coder.js +17 -3
  47. package/src/prompts/reviewer.js +2 -2
  48. package/src/prompts/rtk-snippet.js +3 -5
  49. package/src/review/diff-generator.js +66 -17
  50. package/src/review/snapshot-diff.js +122 -0
  51. package/src/review/tdd-policy.js +4 -2
  52. package/src/roles/agent-role.js +135 -0
  53. package/src/roles/architect-role.js +61 -108
  54. package/src/roles/audit-role.js +25 -71
  55. package/src/roles/coder-role.js +32 -57
  56. package/src/roles/discover-role.js +30 -71
  57. package/src/roles/hu-reviewer-role.js +51 -99
  58. package/src/roles/impeccable-role.js +34 -91
  59. package/src/roles/index.js +2 -0
  60. package/src/roles/karajan-brain-role.js +134 -0
  61. package/src/roles/planner-role.js +40 -75
  62. package/src/roles/refactorer-role.js +22 -64
  63. package/src/roles/researcher-role.js +52 -118
  64. package/src/roles/reviewer-role.js +69 -126
  65. package/src/roles/security-role.js +38 -102
  66. package/src/roles/solomon-role.js +71 -15
  67. package/src/roles/tester-role.js +121 -95
  68. package/src/roles/triage-role.js +50 -111
  69. package/src/session-store.js +1 -1
  70. package/src/skills/skill-detector.js +11 -0
  71. package/src/sonar/config-resolver.js +20 -1
  72. package/src/sonar/credentials.js +17 -0
  73. package/src/sonar/project-key.js +1 -1
  74. package/src/sonar/scanner.js +1 -1
  75. package/src/utils/banner.js +66 -0
  76. package/src/utils/display.js +35 -17
  77. package/src/utils/injection-guard.js +23 -8
  78. package/src/utils/logger.js +6 -1
  79. package/src/utils/process.js +3 -4
  80. package/src/utils/role-assigner.js +131 -0
  81. package/src/utils/update-check.js +5 -1
  82. package/src/utils/welcome.js +3 -8
  83. package/src/webperf/devtools-detect.js +70 -23
  84. package/templates/roles/coder.md +7 -0
  85. package/templates/roles/karajan-brain.md +95 -0
  86. package/templates/roles/solomon.md +67 -106
  87. package/templates/workflows/automerge.yml +4 -4
  88. package/templates/workflows/houston-override.yml +4 -4
  89. package/templates/workflows/{becaria-gateway.yml → kj-ci-gateway.yml} +12 -12
  90. package/src/proxy/adapters/anthropic.js +0 -131
  91. package/src/proxy/adapters/gemini.js +0 -106
  92. package/src/proxy/adapters/index.js +0 -51
  93. package/src/proxy/adapters/openai.js +0 -71
  94. package/src/proxy/compression/ai-compressor.js +0 -209
  95. package/src/proxy/compression/dedup-cache.js +0 -100
  96. package/src/proxy/compression/deterministic/bash-build.js +0 -60
  97. package/src/proxy/compression/deterministic/bash-git.js +0 -101
  98. package/src/proxy/compression/deterministic/bash-infra.js +0 -64
  99. package/src/proxy/compression/deterministic/bash-misc.js +0 -53
  100. package/src/proxy/compression/deterministic/bash-pkg.js +0 -49
  101. package/src/proxy/compression/deterministic/bash-test.js +0 -61
  102. package/src/proxy/compression/deterministic/glob.js +0 -26
  103. package/src/proxy/compression/deterministic/grep.js +0 -53
  104. package/src/proxy/compression/deterministic/index.js +0 -58
  105. package/src/proxy/compression/deterministic/read.js +0 -22
  106. package/src/proxy/compression/deterministic/utils.js +0 -33
  107. package/src/proxy/compression/pipeline.js +0 -270
  108. package/src/proxy/middleware/prompt-injector.js +0 -165
  109. package/src/proxy/middleware/response-interceptor.js +0 -223
  110. package/src/proxy/progress-monitor.js +0 -98
  111. package/src/proxy/proxy-lifecycle.js +0 -198
  112. package/src/proxy/proxy-server.js +0 -239
  113. package/src/proxy/security.js +0 -45
  114. /package/src/{becaria → ci}/index.js +0 -0
  115. /package/src/{becaria → ci}/repo.js +0 -0
@@ -1,100 +0,0 @@
1
- import { createHash } from "node:crypto";
2
-
3
- /**
4
- * Cross-turn content deduplication using an LRU hash cache.
5
- * Detects repeated content across conversation turns and returns
6
- * compact references instead of duplicated text.
7
- */
8
- export class DedupCache {
9
- #maxEntries;
10
- #cache; // Map preserves insertion order for LRU
11
- #totalChecks;
12
- #hits;
13
- #misses;
14
-
15
- constructor({ maxEntries = 500 } = {}) {
16
- this.#maxEntries = maxEntries;
17
- this.#cache = new Map();
18
- this.#totalChecks = 0;
19
- this.#hits = 0;
20
- this.#misses = 0;
21
- }
22
-
23
- /**
24
- * Check if text has been seen in a prior turn.
25
- * @param {string} text - Content to check
26
- * @param {number} turnIndex - Current turn number
27
- * @returns {{isDuplicate: boolean, reference?: string}}
28
- */
29
- check(text, turnIndex) {
30
- this.#totalChecks++;
31
-
32
- const tokenCount = Math.floor(text.length / 4);
33
- const hash = createHash("sha256").update(text).digest("hex");
34
- const shortHash = hash.slice(0, 6);
35
-
36
- // Small content is never deduped
37
- if (tokenCount <= 200) {
38
- this.#misses++;
39
- this.#upsert(hash, { hash, turnIndex, tokenCount, insertedAt: Date.now() });
40
- return { isDuplicate: false };
41
- }
42
-
43
- const existing = this.#cache.get(hash);
44
-
45
- if (existing && existing.turnIndex < turnIndex) {
46
- // Seen in a prior turn — duplicate
47
- this.#hits++;
48
- // Refresh LRU position
49
- this.#cache.delete(hash);
50
- this.#cache.set(hash, { ...existing, insertedAt: Date.now() });
51
- return {
52
- isDuplicate: true,
53
- reference: `[Content from turn ${existing.turnIndex}, hash ${shortHash}, ${tokenCount} tokens - unchanged]`,
54
- };
55
- }
56
-
57
- // Not seen or same turn — store and return not duplicate
58
- this.#misses++;
59
- this.#upsert(hash, { hash, turnIndex, tokenCount, insertedAt: Date.now() });
60
- return { isDuplicate: false };
61
- }
62
-
63
- /**
64
- * Insert or update an entry, evicting the oldest if at capacity.
65
- */
66
- #upsert(key, entry) {
67
- // If key already exists, delete first to refresh insertion order
68
- if (this.#cache.has(key)) {
69
- this.#cache.delete(key);
70
- }
71
-
72
- // Evict oldest (first inserted) if at capacity
73
- if (this.#cache.size >= this.#maxEntries) {
74
- const oldestKey = this.#cache.keys().next().value;
75
- this.#cache.delete(oldestKey);
76
- }
77
-
78
- this.#cache.set(key, entry);
79
- }
80
-
81
- /**
82
- * @returns {{totalChecks: number, hits: number, misses: number, entries: number}}
83
- */
84
- getStats() {
85
- return {
86
- totalChecks: this.#totalChecks,
87
- hits: this.#hits,
88
- misses: this.#misses,
89
- entries: this.#cache.size,
90
- };
91
- }
92
-
93
- /** Clear all state (call between sessions). */
94
- reset() {
95
- this.#cache.clear();
96
- this.#totalChecks = 0;
97
- this.#hits = 0;
98
- this.#misses = 0;
99
- }
100
- }
@@ -1,60 +0,0 @@
1
- /**
2
- * Deterministic compression for build tool output.
3
- * Handles: tsc, eslint, prettier, next build, webpack, vite build.
4
- */
5
- import { stripAnsi, collapseWhitespace, truncateLines } from "./utils.js";
6
-
7
- const BUILD_PATTERNS = [
8
- /error TS\d+:/,
9
- /\d+ errors?/i,
10
- /\d+ warnings?/i,
11
- /eslint/i,
12
- /prettier/i,
13
- /webpack/i,
14
- /✔|Built in|compiled/i,
15
- /Module Error|SyntaxError|TypeError/,
16
- /next build|vite build/i
17
- ];
18
-
19
- export function looksLike(text) {
20
- const clean = stripAnsi(text);
21
- return BUILD_PATTERNS.some((p) => p.test(clean));
22
- }
23
-
24
- export function compact(text) {
25
- const clean = stripAnsi(text);
26
- const lines = clean.split("\n");
27
-
28
- // Collect error/warning lines and summary lines
29
- const errors = [];
30
- const warnings = [];
31
- const summaries = [];
32
-
33
- for (const line of lines) {
34
- const trimmed = line.trim();
35
- if (!trimmed) continue;
36
-
37
- if (/error TS\d+:|Error:|SyntaxError:|TypeError:|Module Error/i.test(trimmed)) {
38
- errors.push(trimmed);
39
- } else if (/warning|warn\b/i.test(trimmed) && !/node_modules/.test(trimmed)) {
40
- warnings.push(trimmed);
41
- } else if (/\d+ errors?|\d+ warnings?|compiled|Built in|✔|Found \d+/i.test(trimmed)) {
42
- summaries.push(trimmed);
43
- }
44
- }
45
-
46
- const parts = [];
47
- if (errors.length) {
48
- parts.push(`Errors (${errors.length}):\n${truncateLines(errors.join("\n"), 20)}`);
49
- }
50
- if (warnings.length) {
51
- parts.push(`Warnings (${warnings.length}):\n${truncateLines(warnings.join("\n"), 10)}`);
52
- }
53
- if (summaries.length) {
54
- parts.push(summaries.join("\n"));
55
- }
56
-
57
- if (parts.length) return parts.join("\n\n");
58
-
59
- return truncateLines(collapseWhitespace(clean), 40);
60
- }
@@ -1,101 +0,0 @@
1
- /**
2
- * Deterministic compression for git CLI output.
3
- * Handles: git status, git diff, git log, git branch.
4
- */
5
- import { stripAnsi, collapseWhitespace, truncateLines } from "./utils.js";
6
-
7
- const GIT_PATTERNS = [
8
- /^On branch /m,
9
- /^(modified|deleted|new file|renamed):/m,
10
- /^Untracked files:/m,
11
- /^diff --git/m,
12
- /^commit [0-9a-f]{7,40}/m,
13
- /^(Changes to be committed|Changes not staged)/m,
14
- /^\* /m // git branch current branch marker
15
- ];
16
-
17
- export function looksLike(text) {
18
- const clean = stripAnsi(text);
19
- return GIT_PATTERNS.some((p) => p.test(clean));
20
- }
21
-
22
- export function compact(text) {
23
- const clean = stripAnsi(text);
24
-
25
- // git status
26
- if (/^On branch /m.test(clean) || /Changes (to be committed|not staged)/m.test(clean)) {
27
- return compactStatus(clean);
28
- }
29
- // git diff
30
- if (/^diff --git/m.test(clean)) {
31
- return compactDiff(clean);
32
- }
33
- // git log
34
- if (/^commit [0-9a-f]{7,40}/m.test(clean)) {
35
- return compactLog(clean);
36
- }
37
- // git branch
38
- if (/^\* /m.test(clean) && clean.split("\n").every((l) => /^[\s*]/.test(l) || !l.trim())) {
39
- return compactBranch(clean);
40
- }
41
- return collapseWhitespace(clean);
42
- }
43
-
44
- function compactStatus(text) {
45
- const lines = text.split("\n");
46
- const kept = [];
47
- const branchLine = lines.find((l) => l.startsWith("On branch "));
48
- if (branchLine) kept.push(branchLine);
49
-
50
- for (const line of lines) {
51
- const trimmed = line.trim();
52
- if (
53
- /^(modified|deleted|new file|renamed|both modified):/.test(trimmed) ||
54
- /^(Untracked files|Changes to be committed|Changes not staged)/.test(trimmed)
55
- ) {
56
- kept.push(trimmed);
57
- }
58
- }
59
- return kept.join("\n") || text;
60
- }
61
-
62
- function compactDiff(text) {
63
- const files = text.split(/(?=^diff --git)/m);
64
- const summaries = [];
65
- for (const file of files) {
66
- if (!file.trim()) continue;
67
- const header = file.match(/^diff --git a\/(.+?) b\/(.+)/m);
68
- const fname = header ? header[2] : "unknown";
69
- const additions = (file.match(/^\+(?!\+\+)/gm) || []).length;
70
- const deletions = (file.match(/^-(?!--)/gm) || []).length;
71
- const hunkHeaders = file.match(/^@@.*@@.*$/gm) || [];
72
- summaries.push(`${fname}: +${additions}/-${deletions} (${hunkHeaders.length} hunks)`);
73
-
74
- // Keep first few hunk headers for context
75
- for (const h of hunkHeaders.slice(0, 3)) {
76
- summaries.push(` ${h.trim()}`);
77
- }
78
- }
79
- return `git diff summary (${files.filter((f) => f.trim()).length} files):\n${summaries.join("\n")}`;
80
- }
81
-
82
- function compactLog(text) {
83
- const entries = text.split(/(?=^commit )/m).filter((e) => e.trim());
84
- const lines = [];
85
- for (const entry of entries) {
86
- const hashMatch = entry.match(/^commit ([0-9a-f]{7})/);
87
- const msgMatch = entry.match(/^\s{4}(.+)/m);
88
- const hash = hashMatch ? hashMatch[1] : "???????";
89
- const msg = msgMatch ? msgMatch[1].trim() : "(no message)";
90
- lines.push(`${hash} ${msg}`);
91
- }
92
- return truncateLines(lines.join("\n"), 30);
93
- }
94
-
95
- function compactBranch(text) {
96
- const lines = text.split("\n").filter((l) => l.trim());
97
- if (lines.length <= 20) return lines.map((l) => l.trim()).join("\n");
98
- const current = lines.find((l) => l.startsWith("*"));
99
- const count = lines.length;
100
- return `${current ? current.trim() : "(detached)"}\n... (${count} branches total)`;
101
- }
@@ -1,64 +0,0 @@
1
- /**
2
- * Deterministic compression for infrastructure CLI output.
3
- * Handles: docker ps/images/logs, kubectl, terraform plan/apply.
4
- */
5
- import { stripAnsi, collapseWhitespace, truncateLines } from "./utils.js";
6
-
7
- const INFRA_PATTERNS = [
8
- /CONTAINER ID/,
9
- /REPOSITORY\s+TAG/,
10
- /^NAME\s+READY\s+STATUS/m,
11
- /^(kubectl|docker|terraform)\s/m,
12
- /Terraform will perform/,
13
- /Plan:\s+\d+ to add/,
14
- /Apply complete/
15
- ];
16
-
17
- export function looksLike(text) {
18
- const clean = stripAnsi(text);
19
- return INFRA_PATTERNS.some((p) => p.test(clean));
20
- }
21
-
22
- export function compact(text) {
23
- const clean = stripAnsi(text);
24
-
25
- // docker ps / docker images — compact table
26
- if (/CONTAINER ID/.test(clean) || /REPOSITORY\s+TAG/.test(clean)) {
27
- return compactTable(clean);
28
- }
29
-
30
- // terraform plan/apply
31
- if (/Terraform will perform|Plan:\s+\d+|Apply complete/.test(clean)) {
32
- return compactTerraform(clean);
33
- }
34
-
35
- // kubectl — compact table
36
- if (/^NAME\s+READY\s+STATUS/m.test(clean)) {
37
- return compactTable(clean);
38
- }
39
-
40
- // docker logs — truncate
41
- return truncateLines(collapseWhitespace(clean), 30);
42
- }
43
-
44
- function compactTable(text) {
45
- const lines = text.split("\n").filter((l) => l.trim());
46
- if (lines.length <= 15) return lines.map((l) => collapseWhitespace(l)).join("\n");
47
- const header = lines[0];
48
- const rows = lines.slice(1, 11);
49
- return `${collapseWhitespace(header)}\n${rows.map((r) => collapseWhitespace(r)).join("\n")}\n... (${lines.length - 1} total rows)`;
50
- }
51
-
52
- function compactTerraform(text) {
53
- const lines = text.split("\n");
54
- const kept = [];
55
- for (const line of lines) {
56
- if (
57
- /Plan:|Apply complete|Terraform will perform|^\s*[#~+-]/.test(line) &&
58
- !/Refreshing state/.test(line)
59
- ) {
60
- kept.push(line.trim());
61
- }
62
- }
63
- return truncateLines(kept.join("\n"), 40) || truncateLines(collapseWhitespace(text), 30);
64
- }
@@ -1,53 +0,0 @@
1
- /**
2
- * Deterministic compression for miscellaneous Bash output.
3
- * Handles: curl/wget, gh CLI, npx, general long output.
4
- */
5
- import { stripAnsi, collapseWhitespace, truncateLines } from "./utils.js";
6
-
7
- const MISC_PATTERNS = [
8
- /HTTP\/[\d.]+\s+\d{3}/,
9
- /curl:|wget:/i,
10
- /gh (pr|issue|repo|release)/i,
11
- /npx:/i
12
- ];
13
-
14
- export function looksLike(text) {
15
- const clean = stripAnsi(text);
16
- return MISC_PATTERNS.some((p) => p.test(clean));
17
- }
18
-
19
- export function compact(text) {
20
- const clean = stripAnsi(text);
21
-
22
- // curl/wget — keep status + relevant headers
23
- if (/HTTP\/[\d.]+\s+\d{3}/.test(clean)) {
24
- return compactHttp(clean);
25
- }
26
-
27
- // gh CLI — compact lists
28
- if (/gh (pr|issue|repo|release)/i.test(clean)) {
29
- return truncateLines(collapseWhitespace(clean), 20);
30
- }
31
-
32
- return truncateLines(collapseWhitespace(clean), 40);
33
- }
34
-
35
- function compactHttp(text) {
36
- const lines = text.split("\n");
37
- const kept = [];
38
- for (const line of lines) {
39
- if (
40
- /HTTP\/[\d.]+\s+\d{3}/.test(line) ||
41
- /^(content-type|location|x-[-\w]+|authorization):/i.test(line.trim())
42
- ) {
43
- kept.push(line.trim());
44
- }
45
- }
46
- // Also keep body, truncated
47
- const bodyStart = text.indexOf("\r\n\r\n");
48
- if (bodyStart > -1) {
49
- const body = text.slice(bodyStart + 4).trim();
50
- if (body) kept.push(truncateLines(body, 20));
51
- }
52
- return kept.join("\n") || truncateLines(collapseWhitespace(text), 30);
53
- }
@@ -1,49 +0,0 @@
1
- /**
2
- * Deterministic compression for package manager output.
3
- * Handles: npm/yarn/pnpm install, npm list, npm outdated.
4
- */
5
- import { stripAnsi, collapseWhitespace, truncateLines } from "./utils.js";
6
-
7
- const PKG_PATTERNS = [
8
- /added \d+ packages?/i,
9
- /npm warn|npm error/i,
10
- /packages? in \d/,
11
- /├──|└──|│/,
12
- /\d+ vulnerabilities/i,
13
- /npm outdated/i,
14
- /Current\s+Wanted\s+Latest/i
15
- ];
16
-
17
- export function looksLike(text) {
18
- const clean = stripAnsi(text);
19
- return PKG_PATTERNS.some((p) => p.test(clean));
20
- }
21
-
22
- export function compact(text) {
23
- const clean = stripAnsi(text);
24
-
25
- // npm install summary
26
- if (/added \d+ packages?/i.test(clean)) {
27
- const lines = clean.split("\n");
28
- const kept = [];
29
- for (const line of lines) {
30
- const trimmed = line.trim();
31
- if (/added \d+ packages?/i.test(trimmed) || /vulnerabilit/i.test(trimmed)) {
32
- kept.push(trimmed);
33
- }
34
- }
35
- return kept.join("\n") || clean;
36
- }
37
-
38
- // npm list — truncate tree
39
- if (/├──|└──/.test(clean)) {
40
- return truncateLines(clean, 25);
41
- }
42
-
43
- // npm outdated — compact table
44
- if (/Current\s+Wanted\s+Latest/i.test(clean)) {
45
- return truncateLines(collapseWhitespace(clean), 20);
46
- }
47
-
48
- return truncateLines(collapseWhitespace(clean), 30);
49
- }
@@ -1,61 +0,0 @@
1
- /**
2
- * Deterministic compression for test runner output.
3
- * Handles: vitest, jest, mocha, playwright, pytest, cargo test, go test.
4
- */
5
- import { stripAnsi, collapseWhitespace, truncateLines } from "./utils.js";
6
-
7
- const TEST_PATTERNS = [
8
- /Tests?\s+\d+\s+(passed|failed)/i,
9
- /✓|✗|✘|PASS|FAIL/,
10
- /Test Suites?:/,
11
- /test result:/i,
12
- /FAILED|PASSED|ERROR/,
13
- /vitest|jest|mocha|playwright|pytest|cargo test|go test/i,
14
- /\d+ passing/,
15
- /\d+ failing/
16
- ];
17
-
18
- export function looksLike(text) {
19
- const clean = stripAnsi(text);
20
- return TEST_PATTERNS.some((p) => p.test(clean));
21
- }
22
-
23
- export function compact(text) {
24
- const clean = stripAnsi(text);
25
-
26
- // Extract summary line if present
27
- const summaryPatterns = [
28
- /Tests?\s+\d+\s+(passed|failed).*/gi,
29
- /Test Suites?:.*$/gm,
30
- /\d+ passing.*$/gm,
31
- /\d+ failing.*$/gm,
32
- /test result:.*$/gim,
33
- /Tests:.*\d+ total$/gm
34
- ];
35
-
36
- const summaries = [];
37
- for (const p of summaryPatterns) {
38
- const matches = clean.match(p);
39
- if (matches) summaries.push(...matches.map((m) => m.trim()));
40
- }
41
-
42
- // Extract failed test details (name + error)
43
- const failures = [];
44
- const failBlocks = clean.split(/(?=(?:FAIL|✗|✘|×)\s)/);
45
- for (const block of failBlocks) {
46
- if (/^(?:FAIL|✗|✘|×)\s/.test(block)) {
47
- failures.push(truncateLines(block.trim(), 8));
48
- }
49
- }
50
-
51
- const parts = [];
52
- if (summaries.length) parts.push(summaries.join("\n"));
53
- if (failures.length) {
54
- parts.push(`Failed tests:\n${failures.join("\n---\n")}`);
55
- }
56
-
57
- if (parts.length) return parts.join("\n\n");
58
-
59
- // Fallback: collapse and truncate
60
- return truncateLines(collapseWhitespace(clean), 50);
61
- }
@@ -1,26 +0,0 @@
1
- /**
2
- * Deterministic compression for glob/find/ls output.
3
- * Truncates long file listings while keeping count.
4
- */
5
- import { stripAnsi, truncateLines } from "./utils.js";
6
-
7
- const MAX_FILES_SHOWN = 40;
8
-
9
- export function looksLike(text) {
10
- const clean = stripAnsi(text);
11
- const lines = clean.split("\n").filter((l) => l.trim());
12
- if (lines.length < 5) return false;
13
- // Most lines look like file paths
14
- const pathLike = lines.filter((l) => /^[\w./-]+\.\w+$/.test(l.trim()) || /^\//.test(l.trim()));
15
- return pathLike.length >= lines.length * 0.6;
16
- }
17
-
18
- export function compact(text) {
19
- const clean = stripAnsi(text);
20
- const lines = clean.split("\n").filter((l) => l.trim());
21
- if (lines.length <= MAX_FILES_SHOWN) return clean;
22
-
23
- const shown = lines.slice(0, MAX_FILES_SHOWN);
24
- shown.push(`\n... (${lines.length} total files, showing first ${MAX_FILES_SHOWN})`);
25
- return shown.join("\n");
26
- }
@@ -1,53 +0,0 @@
1
- /**
2
- * Deterministic compression for grep/ripgrep output.
3
- * Collapses repetitive results, keeps first N per file + total count.
4
- */
5
- import { stripAnsi, truncateLines } from "./utils.js";
6
-
7
- const MAX_MATCHES_PER_FILE = 3;
8
- const MAX_FILES = 15;
9
-
10
- export function looksLike(text) {
11
- const clean = stripAnsi(text);
12
- // grep-style output: file:line:content or file:content
13
- const lines = clean.split("\n").filter((l) => l.trim());
14
- if (lines.length < 2) return false;
15
- const matchingLines = lines.filter((l) => /^[^\s:]+:\d+:/.test(l) || /^[^\s:]+:/.test(l));
16
- return matchingLines.length >= lines.length * 0.5;
17
- }
18
-
19
- export function compact(text) {
20
- const clean = stripAnsi(text);
21
- const lines = clean.split("\n").filter((l) => l.trim());
22
-
23
- // Group by file
24
- const byFile = new Map();
25
- for (const line of lines) {
26
- const match = line.match(/^([^\s:]+):/);
27
- const file = match ? match[1] : "__other__";
28
- if (!byFile.has(file)) byFile.set(file, []);
29
- byFile.get(file).push(line);
30
- }
31
-
32
- const totalFiles = byFile.size;
33
- const totalMatches = lines.length;
34
- const result = [];
35
- let fileCount = 0;
36
-
37
- for (const [file, matches] of byFile) {
38
- if (fileCount >= MAX_FILES) break;
39
- fileCount++;
40
- const shown = matches.slice(0, MAX_MATCHES_PER_FILE);
41
- result.push(...shown);
42
- if (matches.length > MAX_MATCHES_PER_FILE) {
43
- result.push(` ... (${matches.length - MAX_MATCHES_PER_FILE} more matches in ${file})`);
44
- }
45
- }
46
-
47
- if (totalFiles > MAX_FILES) {
48
- result.push(`\n... (${totalFiles - MAX_FILES} more files)`);
49
- }
50
- result.push(`\nTotal: ${totalMatches} matches in ${totalFiles} files`);
51
-
52
- return result.join("\n");
53
- }
@@ -1,58 +0,0 @@
1
- /**
2
- * Deterministic compression dispatcher.
3
- * Tries known patterns in order of specificity; returns original text if nothing matches.
4
- */
5
- import * as bashGit from "./bash-git.js";
6
- import * as bashTest from "./bash-test.js";
7
- import * as bashBuild from "./bash-build.js";
8
- import * as bashInfra from "./bash-infra.js";
9
- import * as bashPkg from "./bash-pkg.js";
10
- import * as bashMisc from "./bash-misc.js";
11
- import * as grepMod from "./grep.js";
12
- import * as readMod from "./read.js";
13
- import * as globMod from "./glob.js";
14
-
15
- /** Compressors keyed by tool name hint. */
16
- const TOOL_COMPRESSORS = {
17
- Grep: grepMod,
18
- Read: readMod,
19
- Glob: globMod
20
- };
21
-
22
- /** Bash compressors tried in order of specificity. */
23
- const BASH_COMPRESSORS = [
24
- bashGit,
25
- bashTest,
26
- bashBuild,
27
- bashInfra,
28
- bashPkg,
29
- bashMisc
30
- ];
31
-
32
- /**
33
- * Compress tool output deterministically.
34
- * @param {string} text - raw tool output
35
- * @param {string} [toolName] - tool name hint (e.g. "Bash", "Grep", "Read", "Glob")
36
- * @returns {{ text: string, compressed: boolean }}
37
- */
38
- export function compressDeterministic(text, toolName = "") {
39
- if (!text || typeof text !== "string") return { text: text ?? "", compressed: false };
40
-
41
- // Direct tool match
42
- const directCompressor = TOOL_COMPRESSORS[toolName];
43
- if (directCompressor && directCompressor.looksLike(text)) {
44
- return { text: directCompressor.compact(text), compressed: true };
45
- }
46
-
47
- // For Bash or unknown tools, try all bash compressors in order
48
- if (!toolName || toolName === "Bash") {
49
- for (const compressor of BASH_COMPRESSORS) {
50
- if (compressor.looksLike(text)) {
51
- return { text: compressor.compact(text), compressed: true };
52
- }
53
- }
54
- }
55
-
56
- // No pattern matched
57
- return { text, compressed: false };
58
- }
@@ -1,22 +0,0 @@
1
- /**
2
- * Deterministic compression for file read output.
3
- * Truncates very long file contents while preserving structure.
4
- */
5
- import { truncateLines, countTokens } from "./utils.js";
6
-
7
- const MAX_LINES = 200;
8
- const TOKEN_THRESHOLD = 2000;
9
-
10
- export function looksLike(text) {
11
- // Read output is generic text; we use this as a fallback for very long content
12
- // that looks like source code or structured data.
13
- if (countTokens(text) < TOKEN_THRESHOLD) return false;
14
- const lines = text.split("\n");
15
- if (lines.length < 50) return false;
16
- // Heuristic: numbered lines (cat -n style) or code-like content
17
- return /^\s*\d+\t/.test(lines[0]) || /^(import |export |const |function |class |def |package )/.test(lines[0]);
18
- }
19
-
20
- export function compact(text) {
21
- return truncateLines(text, MAX_LINES);
22
- }
@@ -1,33 +0,0 @@
1
- /**
2
- * Shared utilities for deterministic compression.
3
- */
4
-
5
- /** Strip ANSI escape sequences (colors, bold, etc.) */
6
- export function stripAnsi(text) {
7
- // eslint-disable-next-line no-control-regex
8
- return text.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
9
- }
10
-
11
- /** Collapse consecutive whitespace (spaces/tabs) into a single space per line. */
12
- export function collapseWhitespace(text) {
13
- return text
14
- .split("\n")
15
- .map((line) => line.replace(/[ \t]+/g, " ").trim())
16
- .join("\n")
17
- .replace(/\n{3,}/g, "\n\n");
18
- }
19
-
20
- /** Keep at most `max` lines, appending a truncation note. */
21
- export function truncateLines(text, max = 100) {
22
- const lines = text.split("\n");
23
- if (lines.length <= max) return text;
24
- const kept = lines.slice(0, max);
25
- kept.push(`... (${lines.length - max} more lines truncated)`);
26
- return kept.join("\n");
27
- }
28
-
29
- /** Rough token estimate: chars / 4. */
30
- export function countTokens(text) {
31
- if (!text) return 0;
32
- return Math.ceil(text.length / 4);
33
- }