@gethmy/harness 1.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.
- package/README.md +66 -0
- package/dist/cli.js +2936 -0
- package/dist/index.js +3734 -0
- package/package.json +65 -0
- package/src/artifact-judge.ts +410 -0
- package/src/cli.ts +272 -0
- package/src/command-metric.ts +594 -0
- package/src/error-classifier.ts +95 -0
- package/src/exec-types.ts +109 -0
- package/src/gate-collectors.ts +431 -0
- package/src/gate-config-error.ts +73 -0
- package/src/git-diff-stat.ts +148 -0
- package/src/git-pr.ts +839 -0
- package/src/harmony-client.ts +197 -0
- package/src/index.ts +37 -0
- package/src/log.ts +129 -0
- package/src/model-tier.test.ts +169 -0
- package/src/model-tier.ts +108 -0
- package/src/oracle-collector.ts +148 -0
- package/src/oracle.ts +434 -0
- package/src/pm.ts +73 -0
- package/src/process-group.ts +149 -0
- package/src/project-type.ts +303 -0
- package/src/revert-guard.ts +99 -0
- package/src/review-types.ts +52 -0
- package/src/runner.ts +184 -0
- package/src/sdk-agent-runner.ts +575 -0
- package/src/stage-cli.ts +302 -0
- package/src/stage-run.ts +91 -0
- package/src/verification.ts +711 -0
- package/src/worktree.ts +639 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { log } from "./log.js";
|
|
3
|
+
|
|
4
|
+
const TAG = "git-diff-stat";
|
|
5
|
+
|
|
6
|
+
/** Default cap on the number of changed-file paths captured per episode. */
|
|
7
|
+
export const MAX_CHANGED_FILES = 30;
|
|
8
|
+
|
|
9
|
+
export interface DiffStat {
|
|
10
|
+
/** Changed file paths (authoritative — derived from the diff itself). */
|
|
11
|
+
files: string[];
|
|
12
|
+
/** Total lines added across the diff (best-effort, may be 0). */
|
|
13
|
+
insertions: number;
|
|
14
|
+
/** Total lines removed across the diff (best-effort, may be 0). */
|
|
15
|
+
deletions: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse `git diff --numstat` output into a changed-file list + churn totals.
|
|
20
|
+
* Pure — exported for testing. numstat lines look like:
|
|
21
|
+
* `12\t3\tpath/to/file.ts`
|
|
22
|
+
* Binary files report `-\t-\t<path>`; those contribute 0 churn but still count
|
|
23
|
+
* as a changed file. The file list is capped at `maxFiles`.
|
|
24
|
+
*/
|
|
25
|
+
export function parseNumstat(
|
|
26
|
+
raw: string,
|
|
27
|
+
maxFiles = MAX_CHANGED_FILES,
|
|
28
|
+
): DiffStat {
|
|
29
|
+
const files: string[] = [];
|
|
30
|
+
let insertions = 0;
|
|
31
|
+
let deletions = 0;
|
|
32
|
+
for (const line of raw.split("\n")) {
|
|
33
|
+
const trimmed = line.trim();
|
|
34
|
+
if (trimmed.length === 0) continue;
|
|
35
|
+
const parts = trimmed.split("\t");
|
|
36
|
+
if (parts.length < 3) continue;
|
|
37
|
+
const [add, del, ...pathParts] = parts;
|
|
38
|
+
const path = pathParts.join("\t");
|
|
39
|
+
if (!path) continue;
|
|
40
|
+
const addN = Number.parseInt(add, 10);
|
|
41
|
+
const delN = Number.parseInt(del, 10);
|
|
42
|
+
if (Number.isFinite(addN)) insertions += addN;
|
|
43
|
+
if (Number.isFinite(delN)) deletions += delN;
|
|
44
|
+
if (files.length < maxFiles) files.push(path);
|
|
45
|
+
}
|
|
46
|
+
return { files, insertions, deletions };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Per-file add/remove counts derived from a unified diff. */
|
|
50
|
+
export interface DiffFileSummary {
|
|
51
|
+
path: string;
|
|
52
|
+
added: number;
|
|
53
|
+
removed: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse a unified diff (the text from `git diff`) into a per-file
|
|
58
|
+
* added/removed line tally. Pure — works on whatever diff string the caller
|
|
59
|
+
* already has, so no second git invocation is needed (and it handles diffs
|
|
60
|
+
* stitched together from multiple commits in local-review mode). Used by
|
|
61
|
+
* `formatDiffSummary` to build the compact stat block for the review prompt
|
|
62
|
+
* (#348) instead of inlining the full diff.
|
|
63
|
+
*/
|
|
64
|
+
export function summarizeUnifiedDiff(diff: string): {
|
|
65
|
+
files: DiffFileSummary[];
|
|
66
|
+
totalAdded: number;
|
|
67
|
+
totalRemoved: number;
|
|
68
|
+
} {
|
|
69
|
+
const files: DiffFileSummary[] = [];
|
|
70
|
+
let current: DiffFileSummary | null = null;
|
|
71
|
+
let totalAdded = 0;
|
|
72
|
+
let totalRemoved = 0;
|
|
73
|
+
for (const line of diff.split("\n")) {
|
|
74
|
+
if (line.startsWith("diff --git")) {
|
|
75
|
+
// "diff --git a/<path> b/<path>" — prefer the new (b/) path.
|
|
76
|
+
const m = line.match(/ b\/(.+)$/);
|
|
77
|
+
current = {
|
|
78
|
+
path: m ? m[1] : line.slice("diff --git ".length),
|
|
79
|
+
added: 0,
|
|
80
|
+
removed: 0,
|
|
81
|
+
};
|
|
82
|
+
files.push(current);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!current) continue;
|
|
86
|
+
// Skip the file headers (+++ / ---) — only count actual content lines.
|
|
87
|
+
if (line.startsWith("+++") || line.startsWith("---")) continue;
|
|
88
|
+
if (line.startsWith("+")) {
|
|
89
|
+
current.added++;
|
|
90
|
+
totalAdded++;
|
|
91
|
+
} else if (line.startsWith("-")) {
|
|
92
|
+
current.removed++;
|
|
93
|
+
totalRemoved++;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { files, totalAdded, totalRemoved };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Render a compact `git diff --stat`-style summary from a unified diff: a
|
|
101
|
+
* changed-file list with per-file churn plus a totals line. The review agent
|
|
102
|
+
* reads the actual file contents itself (Read/Grep + read-only `git diff`), so
|
|
103
|
+
* this map is all the prompt needs — far cheaper than inlining up to 80K chars
|
|
104
|
+
* of diff (#348). The file list is capped at `maxFiles`.
|
|
105
|
+
*/
|
|
106
|
+
export function formatDiffSummary(diff: string, maxFiles = 100): string {
|
|
107
|
+
const trimmed = diff.trim();
|
|
108
|
+
if (!trimmed || diff === "(unable to retrieve diff)") {
|
|
109
|
+
return trimmed ? diff : "(no diff available)";
|
|
110
|
+
}
|
|
111
|
+
const { files, totalAdded, totalRemoved } = summarizeUnifiedDiff(diff);
|
|
112
|
+
if (files.length === 0) return "(no file changes detected in diff)";
|
|
113
|
+
const shown = files.slice(0, maxFiles);
|
|
114
|
+
const lines = shown.map((f) => ` ${f.path} | +${f.added} -${f.removed}`);
|
|
115
|
+
if (files.length > maxFiles) {
|
|
116
|
+
lines.push(` ... and ${files.length - maxFiles} more file(s)`);
|
|
117
|
+
}
|
|
118
|
+
lines.push(
|
|
119
|
+
` ${files.length} file(s) changed, ${totalAdded} insertion(s)(+), ${totalRemoved} deletion(s)(-)`,
|
|
120
|
+
);
|
|
121
|
+
return lines.join("\n");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Capture a changed-file list + churn for the work on a branch versus its base,
|
|
126
|
+
* via `git diff --numstat`. Best-effort and guarded: returns null on any
|
|
127
|
+
* failure so callers can fall back to tracked paths (#272). Never throws.
|
|
128
|
+
*/
|
|
129
|
+
export function captureDiffStat(
|
|
130
|
+
worktreePath: string,
|
|
131
|
+
baseBranch: string,
|
|
132
|
+
maxFiles = MAX_CHANGED_FILES,
|
|
133
|
+
): DiffStat | null {
|
|
134
|
+
try {
|
|
135
|
+
const raw = execFileSync(
|
|
136
|
+
"git",
|
|
137
|
+
["diff", "--numstat", `${baseBranch}...HEAD`],
|
|
138
|
+
{ cwd: worktreePath, encoding: "utf-8", timeout: 30_000 },
|
|
139
|
+
);
|
|
140
|
+
return parseNumstat(raw, maxFiles);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
log.warn(TAG, "git diff --numstat failed", {
|
|
143
|
+
event: "diff_stat_failed",
|
|
144
|
+
error: err instanceof Error ? err.message : String(err),
|
|
145
|
+
});
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|