harnessgap 0.1.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/LICENSE +21 -0
- package/README.md +160 -0
- package/dist/adapter/correction.d.ts +14 -0
- package/dist/adapter/correction.js +68 -0
- package/dist/adapter/correction.js.map +1 -0
- package/dist/adapter/parse.d.ts +21 -0
- package/dist/adapter/parse.js +261 -0
- package/dist/adapter/parse.js.map +1 -0
- package/dist/adapter/scrub.d.ts +15 -0
- package/dist/adapter/scrub.js +97 -0
- package/dist/adapter/scrub.js.map +1 -0
- package/dist/adapter/stream.d.ts +14 -0
- package/dist/adapter/stream.js +207 -0
- package/dist/adapter/stream.js.map +1 -0
- package/dist/adapter/taxonomy.d.ts +7 -0
- package/dist/adapter/taxonomy.js +21 -0
- package/dist/adapter/taxonomy.js.map +1 -0
- package/dist/aggregate/leaderboard.d.ts +26 -0
- package/dist/aggregate/leaderboard.js +168 -0
- package/dist/aggregate/leaderboard.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +67 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +191 -0
- package/dist/config.js.map +1 -0
- package/dist/detector/areas.d.ts +13 -0
- package/dist/detector/areas.js +95 -0
- package/dist/detector/areas.js.map +1 -0
- package/dist/detector/index.d.ts +14 -0
- package/dist/detector/index.js +31 -0
- package/dist/detector/index.js.map +1 -0
- package/dist/detector/record.d.ts +24 -0
- package/dist/detector/record.js +29 -0
- package/dist/detector/record.js.map +1 -0
- package/dist/detector/scoring.d.ts +21 -0
- package/dist/detector/scoring.js +140 -0
- package/dist/detector/scoring.js.map +1 -0
- package/dist/detector/signals.d.ts +6 -0
- package/dist/detector/signals.js +221 -0
- package/dist/detector/signals.js.map +1 -0
- package/dist/egress.d.ts +8 -0
- package/dist/egress.js +37 -0
- package/dist/egress.js.map +1 -0
- package/dist/git.d.ts +16 -0
- package/dist/git.js +59 -0
- package/dist/git.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/output/calibrate.d.ts +35 -0
- package/dist/output/calibrate.js +149 -0
- package/dist/output/calibrate.js.map +1 -0
- package/dist/output/human.d.ts +24 -0
- package/dist/output/human.js +83 -0
- package/dist/output/human.js.map +1 -0
- package/dist/output/json.d.ts +17 -0
- package/dist/output/json.js +22 -0
- package/dist/output/json.js.map +1 -0
- package/dist/pipeline.d.ts +24 -0
- package/dist/pipeline.js +164 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/types.d.ts +122 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/walk.d.ts +12 -0
- package/dist/walk.js +92 -0
- package/dist/walk.js.map +1 -0
- package/package.json +49 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Pattern-catalog secret scrubber — the privacy backbone for slice-2 persistence.
|
|
2
|
+
// Pure functions, no I/O, no entropy heuristic. Seven rules applied in a fixed
|
|
3
|
+
// order so earlier rules (heredoc blocks) are not partially mangled by later
|
|
4
|
+
// ones (env-var). The same sentinel literal is used everywhere.
|
|
5
|
+
const SENTINEL = '***REDACTED***';
|
|
6
|
+
const MAX_CMD_QUERY = 512;
|
|
7
|
+
const MAX_FILES = 50;
|
|
8
|
+
/**
|
|
9
|
+
* Apply the full 7-rule pattern catalog to a command/query string. Rules run in
|
|
10
|
+
* the documented order: heredoc keys → env-vars → auth headers → URL creds →
|
|
11
|
+
* flag secrets → credential-file paths → known-format tokens.
|
|
12
|
+
*/
|
|
13
|
+
function scrubCatalog(s) {
|
|
14
|
+
// (1) Heredoc / inline private keys — replace the entire PEM block (BEGIN
|
|
15
|
+
// through END, inclusive) with the sentinel.
|
|
16
|
+
s = s.replace(/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*-----/g, SENTINEL);
|
|
17
|
+
// (2) Env-var assignments — keep KEY= when KEY is UPPERCASE_WITH_UNDERSCORES;
|
|
18
|
+
// redact the value. Optional export/set/env prefix is preserved.
|
|
19
|
+
s = s.replace(/(^|\s)((?:export|set|env)\s+)?([A-Z][A-Z0-9_]*)=(?:"[^"]*"|'[^']*'|\S+)/g, (_m, pre, prefix, key) => `${pre}${prefix ?? ''}${key}=${SENTINEL}`);
|
|
20
|
+
// (3) Authorization headers and bare Bearer tokens. Case-insensitive: HTTP
|
|
21
|
+
// headers are case-insensitive (RFC 7230), so `authorization: bearer`
|
|
22
|
+
// must scrub the same as the canonical form.
|
|
23
|
+
s = s.replace(/Authorization:\s*[^\n'"]*/gi, `Authorization: ${SENTINEL}`);
|
|
24
|
+
s = s.replace(/Bearer\s+[A-Za-z0-9._+/=-]+/gi, `Bearer ${SENTINEL}`);
|
|
25
|
+
// (4) URL-embedded credentials — ://user:pass@, ://token@, or ://:pass@
|
|
26
|
+
// (empty username, e.g. redis://:s3cr3t@host) → ://***REDACTED***@.
|
|
27
|
+
// Host/scheme kept; rejects a bare ://@ with no credential component.
|
|
28
|
+
s = s.replace(/(:\/\/)(?:[^\s/@:]+(?::[^\s/@]+)?|:[^\s/@]+)@/g, `$1${SENTINEL}@`);
|
|
29
|
+
// (5) Flag secrets — -p <val>, -u <user:pass>, --password/--secret/--token/
|
|
30
|
+
// --api-key/--access-key (case-insensitive). Value redacted, flag kept.
|
|
31
|
+
s = s.replace(/(^|\s)(--password|--secret|--token|--api-key|--access-key|-p|-u)\s+(\S+)/gi, (_m, pre, flag) => `${pre}${flag} ${SENTINEL}`);
|
|
32
|
+
s = s.replace(/(^|\s)(--password|--secret|--token|--api-key|--access-key)=("[^"]*"|'[^']*'|\S+)/gi, (_m, pre, flag) => `${pre}${flag}=${SENTINEL}`);
|
|
33
|
+
// (6) Credential-file path globs — check each whitespace/quote-delimited
|
|
34
|
+
// token; if it is a credential-file path, replace with sentinel.
|
|
35
|
+
s = s.replace(/[^\s'"]+/g, (token) => isCredentialFile(token) ? SENTINEL : token);
|
|
36
|
+
// (7) Known-format tokens — AWS access-key IDs, GitHub tokens, Slack tokens,
|
|
37
|
+
// JWTs.
|
|
38
|
+
s = s.replace(/AKIA[0-9A-Z]{16}/g, SENTINEL);
|
|
39
|
+
s = s.replace(/gh[oprs]_[A-Za-z0-9]{36}/g, SENTINEL);
|
|
40
|
+
s = s.replace(/xox[baprs]-[A-Za-z0-9-]+/g, SENTINEL);
|
|
41
|
+
s = s.replace(/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, SENTINEL);
|
|
42
|
+
return s;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Test whether a path matches a credential-file glob from the catalog.
|
|
46
|
+
* Globs (basename unless a directory suffix is noted): .env, *.pem, *.key,
|
|
47
|
+
* .aws/credentials (path suffix), .npmrc, id_rsa*, .pgpass, .htpasswd,
|
|
48
|
+
* service-account*.json, credentials.json.
|
|
49
|
+
*/
|
|
50
|
+
function isCredentialFile(p) {
|
|
51
|
+
const base = p.split('/').pop() ?? p;
|
|
52
|
+
if (base === '.env')
|
|
53
|
+
return true;
|
|
54
|
+
if (base.endsWith('.pem'))
|
|
55
|
+
return true;
|
|
56
|
+
if (base.endsWith('.key'))
|
|
57
|
+
return true;
|
|
58
|
+
if (p.endsWith('.aws/credentials'))
|
|
59
|
+
return true;
|
|
60
|
+
if (base === '.npmrc')
|
|
61
|
+
return true;
|
|
62
|
+
if (base.startsWith('id_rsa'))
|
|
63
|
+
return true;
|
|
64
|
+
if (base === '.pgpass')
|
|
65
|
+
return true;
|
|
66
|
+
if (base === '.htpasswd')
|
|
67
|
+
return true;
|
|
68
|
+
if (/^service-account.*\.json$/.test(base))
|
|
69
|
+
return true;
|
|
70
|
+
if (base === 'credentials.json')
|
|
71
|
+
return true;
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Scrub an exec command string: apply the full catalog, then truncate to 512
|
|
76
|
+
* chars (no marker). Replacements use the fixed sentinel `***REDACTED***`.
|
|
77
|
+
*/
|
|
78
|
+
export function scrubCmd(cmd) {
|
|
79
|
+
return scrubCatalog(cmd).slice(0, MAX_CMD_QUERY);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Scrub a search/query string: same catalog and length cap as `scrubCmd`.
|
|
83
|
+
*/
|
|
84
|
+
export function scrubQuery(q) {
|
|
85
|
+
return scrubCatalog(q).slice(0, MAX_CMD_QUERY);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Scrub a list of file paths: replace any path matching a credential-file glob
|
|
89
|
+
* with the sentinel, leave others unchanged, then drop entries beyond 50 (no
|
|
90
|
+
* marker). Only the credential-file rule applies here — not the full catalog.
|
|
91
|
+
*/
|
|
92
|
+
export function scrubFiles(files) {
|
|
93
|
+
return files
|
|
94
|
+
.map((f) => (isCredentialFile(f) ? SENTINEL : f))
|
|
95
|
+
.slice(0, MAX_FILES);
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=scrub.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scrub.js","sourceRoot":"","sources":["../../src/adapter/scrub.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { NormalizedEnvelope, Warnings } from '../types.js';
|
|
2
|
+
type StreamWarnings = Pick<Warnings, 'malformed_lines' | 'oversized_lines' | 'truncated_sessions'>;
|
|
3
|
+
/**
|
|
4
|
+
* Stream one .jsonl transcript → { envelope, cwd, warnings }. Reads line-by-line
|
|
5
|
+
* via node:readline (never slurps). Enforces 1 MB line / 5000 event / 50 MB byte
|
|
6
|
+
* caps. Fail-open: malformed JSON and oversized lines are skipped + counted,
|
|
7
|
+
* never thrown. Deterministic given the file.
|
|
8
|
+
*/
|
|
9
|
+
export declare function streamSession(filePath: string): Promise<{
|
|
10
|
+
envelope: NormalizedEnvelope;
|
|
11
|
+
cwd: string;
|
|
12
|
+
warnings: StreamWarnings;
|
|
13
|
+
}>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// Streaming transcript reader — the only I/O in the adapter. Reads one .jsonl
|
|
2
|
+
// transcript file line-by-line (never slurps the whole file), enforces size
|
|
3
|
+
// caps, calls normalizeRecord per parsed line, threads ctx.prevToolCall through
|
|
4
|
+
// records in order, merges tool_use + tool_result pairs into single tool_call
|
|
5
|
+
// events (see mergeToolCalls), and returns a NormalizedEnvelope + the session's
|
|
6
|
+
// representative cwd + warning counts. Fail-open: never throws on bad input
|
|
7
|
+
// (malformed/oversized/truncated); skips and counts instead.
|
|
8
|
+
//
|
|
9
|
+
// Field paths: `cwd` is a top-level string on most user/assistant records
|
|
10
|
+
// (confirmed from real transcripts; documented in test/parse.test.ts). We read
|
|
11
|
+
// it from the raw parsed record BEFORE normalizeRecord (which does not emit it).
|
|
12
|
+
import { createReadStream } from 'node:fs';
|
|
13
|
+
import * as readline from 'node:readline';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
import { normalizeRecord } from './parse.js';
|
|
16
|
+
// --- Caps (verbatim from the task brief) ---
|
|
17
|
+
const LINE_CAP = 1_048_576; // 1 MB: lines over this are skipped (oversized_lines++)
|
|
18
|
+
const EVENT_CAP = 5000; // once 5000 events collected, drop the rest (truncated)
|
|
19
|
+
const BYTE_CAP = 52_428_800; // 50 MB: stop reading once cumulative bytes ≥ this (truncated)
|
|
20
|
+
/**
|
|
21
|
+
* Stream one .jsonl transcript → { envelope, cwd, warnings }. Reads line-by-line
|
|
22
|
+
* via node:readline (never slurps). Enforces 1 MB line / 5000 event / 50 MB byte
|
|
23
|
+
* caps. Fail-open: malformed JSON and oversized lines are skipped + counted,
|
|
24
|
+
* never thrown. Deterministic given the file.
|
|
25
|
+
*/
|
|
26
|
+
export async function streamSession(filePath) {
|
|
27
|
+
const events = [];
|
|
28
|
+
let malformed_lines = 0;
|
|
29
|
+
let oversized_lines = 0;
|
|
30
|
+
let truncated = false;
|
|
31
|
+
let cumulativeBytes = 0;
|
|
32
|
+
let cwd = '';
|
|
33
|
+
// Session span tracked across ALL parsed records (for envelope.duration_ms /
|
|
34
|
+
// started_at), regardless of whether they became kept events. Result records'
|
|
35
|
+
// timestamps are included so the span reflects the full session end-to-end.
|
|
36
|
+
let firstRecordT = null;
|
|
37
|
+
let firstRecordTs = null;
|
|
38
|
+
let lastRecordT = null;
|
|
39
|
+
const ctx = { prevToolCall: null };
|
|
40
|
+
// session_id = filename stem (basename without the last extension).
|
|
41
|
+
const session_id = path.basename(filePath).replace(/\.[^.]+$/, '');
|
|
42
|
+
try {
|
|
43
|
+
const rl = readline.createInterface({
|
|
44
|
+
input: createReadStream(filePath, { encoding: 'utf8' }),
|
|
45
|
+
crlfDelay: Infinity,
|
|
46
|
+
});
|
|
47
|
+
try {
|
|
48
|
+
for await (const line of rl) {
|
|
49
|
+
// Bytes read for this line (UTF-8) + 1 for the stripped newline.
|
|
50
|
+
const lineBytes = Buffer.byteLength(line, 'utf8') + 1;
|
|
51
|
+
cumulativeBytes += lineBytes;
|
|
52
|
+
if (lineBytes > LINE_CAP) {
|
|
53
|
+
oversized_lines++;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
let parsed;
|
|
57
|
+
let parseOk = true;
|
|
58
|
+
try {
|
|
59
|
+
parsed = JSON.parse(line);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
parseOk = false;
|
|
63
|
+
}
|
|
64
|
+
if (parseOk) {
|
|
65
|
+
if (parsed !== null && typeof parsed === 'object') {
|
|
66
|
+
const rec = parsed;
|
|
67
|
+
// Extract cwd from the first record that carries a non-empty one.
|
|
68
|
+
if (cwd === '') {
|
|
69
|
+
const c = rec.cwd;
|
|
70
|
+
if (typeof c === 'string' && c.length > 0)
|
|
71
|
+
cwd = c;
|
|
72
|
+
}
|
|
73
|
+
// Track session span across ALL parsed records (system records,
|
|
74
|
+
// tool_use, tool_result — regardless of whether they became kept
|
|
75
|
+
// events), so duration_ms reflects the full session.
|
|
76
|
+
const ts = rec.timestamp;
|
|
77
|
+
if (typeof ts === 'string') {
|
|
78
|
+
const ms = Date.parse(ts);
|
|
79
|
+
if (!Number.isNaN(ms)) {
|
|
80
|
+
if (firstRecordT === null) {
|
|
81
|
+
firstRecordT = ms;
|
|
82
|
+
firstRecordTs = ts;
|
|
83
|
+
}
|
|
84
|
+
lastRecordT = ms;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const ev = normalizeRecord(parsed, ctx);
|
|
89
|
+
if (ev !== null) {
|
|
90
|
+
if (events.length < EVENT_CAP) {
|
|
91
|
+
events.push(ev);
|
|
92
|
+
// Thread prevToolCall: the most recent assistant tool_use (non-null
|
|
93
|
+
// tool) is what the next user_msg sees.
|
|
94
|
+
if (ev.kind === 'tool_call' && ev.tool !== null) {
|
|
95
|
+
ctx.prevToolCall = { tool: ev.tool };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
truncated = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
malformed_lines++;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Stop conditions (checked after each line).
|
|
108
|
+
if (events.length >= EVENT_CAP) {
|
|
109
|
+
truncated = true;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
if (cumulativeBytes >= BYTE_CAP) {
|
|
113
|
+
truncated = true;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
rl.close();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Stream/read error (missing/unreadable file, etc.) — fail open: return
|
|
124
|
+
// whatever was collected so far, no throw.
|
|
125
|
+
}
|
|
126
|
+
// Merge tool_use + tool_result pairs into single tool_call events so downstream
|
|
127
|
+
// signals can see tool + ok on the same event (see mergeToolCalls docs).
|
|
128
|
+
const merged = mergeToolCalls(events);
|
|
129
|
+
const started_at = firstRecordTs ?? (merged.length > 0 ? merged[0].t : '');
|
|
130
|
+
const envelope = {
|
|
131
|
+
schema_version: 1,
|
|
132
|
+
session_id,
|
|
133
|
+
agent: 'claude-code',
|
|
134
|
+
repo: '',
|
|
135
|
+
started_at,
|
|
136
|
+
duration_ms: firstRecordT !== null && lastRecordT !== null ? lastRecordT - firstRecordT : 0,
|
|
137
|
+
events: merged,
|
|
138
|
+
truncated,
|
|
139
|
+
event_count: merged.length,
|
|
140
|
+
};
|
|
141
|
+
return {
|
|
142
|
+
envelope,
|
|
143
|
+
cwd,
|
|
144
|
+
warnings: {
|
|
145
|
+
malformed_lines,
|
|
146
|
+
oversized_lines,
|
|
147
|
+
truncated_sessions: truncated ? 1 : 0,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Post-process: merge each tool_use + tool_result pair into ONE tool_call event.
|
|
153
|
+
*
|
|
154
|
+
* `normalizeRecord` (pure, single-record) emits two events per tool invocation:
|
|
155
|
+
* - tool_use → tool_call (tool !== null, ok=true placeholder, interrupted=false)
|
|
156
|
+
* - tool_result → tool_call (tool=null, ok=!is_error, interrupted=result)
|
|
157
|
+
* Downstream signals need tool + ok on the SAME event (e.g. failure_streak filters
|
|
158
|
+
* `tool==='exec' && ok===false`). This merge closes that gap.
|
|
159
|
+
*
|
|
160
|
+
* Pairing: each tool_result is paired with the most recent unresolved tool_use
|
|
161
|
+
* (stack — correct for Claude Code's sequential tool_use→result ordering, where
|
|
162
|
+
* each call is immediately followed by its own result). The result's
|
|
163
|
+
* ok/interrupted/duration_ms/t are merged onto the tool_use event; the result
|
|
164
|
+
* event is dropped from the list.
|
|
165
|
+
*
|
|
166
|
+
* Orphan results (no preceding unresolved tool_use) → dropped.
|
|
167
|
+
* Tool_uses with no result (session interrupted before result arrived) → kept
|
|
168
|
+
* with the placeholder ok=true, interrupted=false.
|
|
169
|
+
*
|
|
170
|
+
* Time handling: when paired, the merged event's `t` is set to the RESULT's `t`
|
|
171
|
+
* (not the tool_use's `t`). This keeps the session span correct for the
|
|
172
|
+
* wall_clock_per_line signal (which uses `events[last].t − events[0].t`): the
|
|
173
|
+
* last result's time is preserved on the merged event even though the result
|
|
174
|
+
* event itself is removed. Without this, dropping result events would shrink the
|
|
175
|
+
* computed span. When unpaired (no result), `t` stays the tool_use's time.
|
|
176
|
+
*/
|
|
177
|
+
function mergeToolCalls(events) {
|
|
178
|
+
const result = [];
|
|
179
|
+
// Stack of indices into `result` pointing at unresolved tool_use events.
|
|
180
|
+
const unresolved = [];
|
|
181
|
+
for (const ev of events) {
|
|
182
|
+
if (ev.kind === 'tool_call' && ev.tool === null) {
|
|
183
|
+
// tool_result event: merge onto the most recent unresolved tool_use.
|
|
184
|
+
const idx = unresolved.pop();
|
|
185
|
+
if (idx === undefined)
|
|
186
|
+
continue; // orphan result → drop
|
|
187
|
+
const use = result[idx];
|
|
188
|
+
result[idx] = {
|
|
189
|
+
...use,
|
|
190
|
+
ok: ev.ok,
|
|
191
|
+
interrupted: ev.interrupted,
|
|
192
|
+
duration_ms: ev.duration_ms,
|
|
193
|
+
t: ev.t, // result time preserves the session span
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
result.push(ev);
|
|
198
|
+
if (ev.kind === 'tool_call' && ev.tool !== null) {
|
|
199
|
+
// tool_use event: track for later merging with its result.
|
|
200
|
+
unresolved.push(result.length - 1);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Unresolved tool_uses remain with ok=true placeholder, interrupted=false.
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream.js","sourceRoot":"","sources":["../../src/adapter/stream.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ToolKind } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Map a Claude Code tool name to a ToolKind.
|
|
4
|
+
* Unknown names (Task*, WebSearch, WebFetch, mcp__*, etc.) and empty strings
|
|
5
|
+
* map to 'other'. Case-sensitive: Claude Code tool names are PascalCase.
|
|
6
|
+
*/
|
|
7
|
+
export declare function mapToolKind(ccToolName: string): ToolKind;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Claude Code tool-name → ToolKind map. Pure lookup, no I/O.
|
|
2
|
+
// Pinned to the six ToolKind values in src/types.ts. Unknown / empty → 'other'.
|
|
3
|
+
const TOOL_KIND_MAP = {
|
|
4
|
+
Read: 'read',
|
|
5
|
+
Grep: 'search',
|
|
6
|
+
Glob: 'search',
|
|
7
|
+
LS: 'list',
|
|
8
|
+
Edit: 'edit',
|
|
9
|
+
Write: 'edit',
|
|
10
|
+
NotebookEdit: 'edit',
|
|
11
|
+
Bash: 'exec',
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Map a Claude Code tool name to a ToolKind.
|
|
15
|
+
* Unknown names (Task*, WebSearch, WebFetch, mcp__*, etc.) and empty strings
|
|
16
|
+
* map to 'other'. Case-sensitive: Claude Code tool names are PascalCase.
|
|
17
|
+
*/
|
|
18
|
+
export function mapToolKind(ccToolName) {
|
|
19
|
+
return TOOL_KIND_MAP[ccToolName] ?? 'other';
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=taxonomy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"taxonomy.js","sourceRoot":"","sources":["../../src/adapter/taxonomy.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,gFAAgF;AAIhF,MAAM,aAAa,GAA6B;IAC9C,IAAI,EAAE,MAAM;IACZ,IAAI,EAAE,QAAQ;IACd,IAAI,EAAE,QAAQ;IACd,EAAE,EAAE,MAAM;IACV,IAAI,EAAE,MAAM;IACZ,KAAK,EAAE,MAAM;IACb,YAAY,EAAE,MAAM;IACpB,IAAI,EAAE,MAAM;CACb,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,UAAkB;IAC5C,OAAO,aAAa,CAAC,UAAU,CAAC,IAAI,OAAO,CAAC;AAC9C,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AreaRow, Config, StruggleRecord } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Aggregate per-session `StruggleRecord`s into per-area `AreaRow`s plus a summary.
|
|
4
|
+
*
|
|
5
|
+
* 1. For each record, for each of its `areas`, add the record (weighted by
|
|
6
|
+
* `area.weight`) to that area's `sessions_total`; if `flagged`, add to
|
|
7
|
+
* `sessions_flagged` (weighted).
|
|
8
|
+
* 2. `mean_score` = average of `score_pct` over flagged records touching the
|
|
9
|
+
* area (0 if none flagged).
|
|
10
|
+
* 3. `top_signals` (per area, max 3): median raw signal value across the area's
|
|
11
|
+
* flagged sessions, top 3 by value desc, `display` formatted per kind.
|
|
12
|
+
* 4. Sort rows by `sessions_flagged` desc, then `mean_score` desc.
|
|
13
|
+
* 5. `summary.unlocalized` = count of records with empty `areas`;
|
|
14
|
+
* `flagged`/`unflagged` = counts of all records (unlocalized included).
|
|
15
|
+
*
|
|
16
|
+
* `cfg` is part of the signature contract but unused in v1 (raw medians; no
|
|
17
|
+
* weighting knobs read). Reserved for future ranking configuration.
|
|
18
|
+
*/
|
|
19
|
+
export declare function aggregateAreas(records: StruggleRecord[], cfg: Config): {
|
|
20
|
+
rows: AreaRow[];
|
|
21
|
+
summary: {
|
|
22
|
+
flagged: number;
|
|
23
|
+
unflagged: number;
|
|
24
|
+
unlocalized: number;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Pure area aggregation: rolls per-session StruggleRecords up into per-area
|
|
2
|
+
// AreaRows (weighted counting, mean_score, top_signals) plus a summary.
|
|
3
|
+
// No I/O, no mutation of inputs.
|
|
4
|
+
// The seven signals in a fixed canonical order. Ties in the top-signals ranking
|
|
5
|
+
// preserve this order (stable sort), so output is deterministic.
|
|
6
|
+
const SIGNAL_SPECS = [
|
|
7
|
+
{ name: 'explore_ratio', field: 'explore_ratio', kind: 'ratio' },
|
|
8
|
+
{ name: 'reread', field: 'reread', kind: 'count' },
|
|
9
|
+
{ name: 'failure_streak', field: 'failure_streak', kind: 'count' },
|
|
10
|
+
{ name: 'corrections', field: 'corrections', kind: 'count' },
|
|
11
|
+
{ name: 'abandonment', field: 'abandonment', kind: 'boolean' },
|
|
12
|
+
{ name: 'oscillation', field: 'oscillation', kind: 'count' },
|
|
13
|
+
{ name: 'wall_clock_per_line', field: 'wall_clock_per_line_ms', kind: 'duration' },
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Aggregate per-session `StruggleRecord`s into per-area `AreaRow`s plus a summary.
|
|
17
|
+
*
|
|
18
|
+
* 1. For each record, for each of its `areas`, add the record (weighted by
|
|
19
|
+
* `area.weight`) to that area's `sessions_total`; if `flagged`, add to
|
|
20
|
+
* `sessions_flagged` (weighted).
|
|
21
|
+
* 2. `mean_score` = average of `score_pct` over flagged records touching the
|
|
22
|
+
* area (0 if none flagged).
|
|
23
|
+
* 3. `top_signals` (per area, max 3): median raw signal value across the area's
|
|
24
|
+
* flagged sessions, top 3 by value desc, `display` formatted per kind.
|
|
25
|
+
* 4. Sort rows by `sessions_flagged` desc, then `mean_score` desc.
|
|
26
|
+
* 5. `summary.unlocalized` = count of records with empty `areas`;
|
|
27
|
+
* `flagged`/`unflagged` = counts of all records (unlocalized included).
|
|
28
|
+
*
|
|
29
|
+
* `cfg` is part of the signature contract but unused in v1 (raw medians; no
|
|
30
|
+
* weighting knobs read). Reserved for future ranking configuration.
|
|
31
|
+
*/
|
|
32
|
+
export function aggregateAreas(records, cfg) {
|
|
33
|
+
void cfg; // reserved; v1 does not read cfg
|
|
34
|
+
const byArea = new Map();
|
|
35
|
+
let flagged = 0;
|
|
36
|
+
let unflagged = 0;
|
|
37
|
+
let unlocalized = 0;
|
|
38
|
+
for (const rec of records) {
|
|
39
|
+
if (rec.flagged)
|
|
40
|
+
flagged += 1;
|
|
41
|
+
else
|
|
42
|
+
unflagged += 1;
|
|
43
|
+
if (rec.areas.length === 0) {
|
|
44
|
+
unlocalized += 1;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
for (const area of rec.areas) {
|
|
48
|
+
let acc = byArea.get(area.key);
|
|
49
|
+
if (acc === undefined) {
|
|
50
|
+
acc = {
|
|
51
|
+
key: area.key,
|
|
52
|
+
sessionsTotal: 0,
|
|
53
|
+
sessionsFlagged: 0,
|
|
54
|
+
flaggedScores: [],
|
|
55
|
+
flaggedSignals: [],
|
|
56
|
+
};
|
|
57
|
+
byArea.set(area.key, acc);
|
|
58
|
+
}
|
|
59
|
+
acc.sessionsTotal += area.weight;
|
|
60
|
+
if (rec.flagged) {
|
|
61
|
+
acc.sessionsFlagged += area.weight;
|
|
62
|
+
acc.flaggedScores.push(rec.score_pct);
|
|
63
|
+
acc.flaggedSignals.push(rec.signals);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const rows = [];
|
|
68
|
+
for (const acc of byArea.values()) {
|
|
69
|
+
const meanScore = acc.flaggedScores.length > 0
|
|
70
|
+
? acc.flaggedScores.reduce((a, b) => a + b, 0) / acc.flaggedScores.length
|
|
71
|
+
: 0;
|
|
72
|
+
rows.push({
|
|
73
|
+
key: acc.key,
|
|
74
|
+
sessions_total: acc.sessionsTotal,
|
|
75
|
+
sessions_flagged: acc.sessionsFlagged,
|
|
76
|
+
mean_score: meanScore,
|
|
77
|
+
top_signals: topSignalsForArea(acc.flaggedSignals),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
rows.sort((a, b) => {
|
|
81
|
+
if (b.sessions_flagged !== a.sessions_flagged) {
|
|
82
|
+
return b.sessions_flagged - a.sessions_flagged;
|
|
83
|
+
}
|
|
84
|
+
return b.mean_score - a.mean_score;
|
|
85
|
+
});
|
|
86
|
+
return { rows, summary: { flagged, unflagged, unlocalized } };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Pick up to 3 top signals for an area from its flagged sessions' signals.
|
|
90
|
+
*
|
|
91
|
+
* For each signal spec: compute the median of its raw value across the flagged
|
|
92
|
+
* sessions (booleans use majority; nullable signals skip when all null). Rank
|
|
93
|
+
* by median value descending (higher = more struggle; `abandonment: true`
|
|
94
|
+
* counts as 1). Take the top 3. Ties preserve `SIGNAL_SPECS` order (stable).
|
|
95
|
+
* An area with no flagged sessions yields `[]`.
|
|
96
|
+
*/
|
|
97
|
+
function topSignalsForArea(flaggedSignals) {
|
|
98
|
+
if (flaggedSignals.length === 0)
|
|
99
|
+
return [];
|
|
100
|
+
const candidates = [];
|
|
101
|
+
for (const spec of SIGNAL_SPECS) {
|
|
102
|
+
if (spec.kind === 'boolean') {
|
|
103
|
+
const values = flaggedSignals.map((s) => s[spec.field]);
|
|
104
|
+
const med = medianBoolean(values);
|
|
105
|
+
candidates.push({
|
|
106
|
+
name: spec.name,
|
|
107
|
+
value: med,
|
|
108
|
+
display: formatDisplay(spec, med),
|
|
109
|
+
sortKey: med ? 1 : 0,
|
|
110
|
+
});
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const values = flaggedSignals
|
|
114
|
+
.map((s) => s[spec.field])
|
|
115
|
+
.filter((v) => v !== null);
|
|
116
|
+
if (values.length === 0)
|
|
117
|
+
continue; // all null → skip this signal
|
|
118
|
+
const med = median(values);
|
|
119
|
+
if (med === null)
|
|
120
|
+
continue; // unreachable when values.length > 0
|
|
121
|
+
candidates.push({
|
|
122
|
+
name: spec.name,
|
|
123
|
+
value: med,
|
|
124
|
+
display: formatDisplay(spec, med),
|
|
125
|
+
sortKey: med,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
candidates.sort((a, b) => b.sortKey - a.sortKey);
|
|
129
|
+
return candidates.slice(0, 3).map(({ name, value, display }) => ({
|
|
130
|
+
name,
|
|
131
|
+
value,
|
|
132
|
+
display,
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
/** Median of a non-empty numeric array. Returns null only for empty input. */
|
|
136
|
+
function median(values) {
|
|
137
|
+
const n = values.length;
|
|
138
|
+
if (n === 0)
|
|
139
|
+
return null;
|
|
140
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
141
|
+
const mid = Math.floor(n / 2);
|
|
142
|
+
if (n % 2 === 1)
|
|
143
|
+
return sorted[mid];
|
|
144
|
+
return (sorted[mid - 1] + sorted[mid]) / 2;
|
|
145
|
+
}
|
|
146
|
+
/** Median of booleans as strict majority: more than half true → true. */
|
|
147
|
+
function medianBoolean(values) {
|
|
148
|
+
const trues = values.filter(Boolean).length;
|
|
149
|
+
return trues * 2 > values.length;
|
|
150
|
+
}
|
|
151
|
+
/** Format a signal median per its kind for the `display` string. */
|
|
152
|
+
function formatDisplay(spec, value) {
|
|
153
|
+
switch (spec.kind) {
|
|
154
|
+
case 'count':
|
|
155
|
+
return `${spec.name}(${Math.round(value)})`;
|
|
156
|
+
case 'ratio':
|
|
157
|
+
return `${spec.name}(${value.toFixed(1)})`;
|
|
158
|
+
case 'duration': {
|
|
159
|
+
const ms = value;
|
|
160
|
+
if (ms >= 1000)
|
|
161
|
+
return `${spec.name}(${Math.round(ms / 1000)}s)`;
|
|
162
|
+
return `${spec.name}(${Math.round(ms)}ms)`;
|
|
163
|
+
}
|
|
164
|
+
case 'boolean':
|
|
165
|
+
return `${spec.name}(${value ? 'yes' : 'no'})`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=leaderboard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"leaderboard.js","sourceRoot":"","sources":["../../src/aggregate/leaderboard.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Stateless CLI bin entry for harnessgap. Parses args with commander, awaits
|
|
3
|
+
// runScan, writes result.output to stdout, and exits with result.exitCode.
|
|
4
|
+
// ConfigError (and any thrown error from runScan) is caught → a SHORT message
|
|
5
|
+
// to stderr (no stack, no transcript paths), exit 1.
|
|
6
|
+
//
|
|
7
|
+
// Constraints (§11 egress): no network imports. Imports limited to commander,
|
|
8
|
+
// ./pipeline.js, ./config.js, node:process, node:fs. The package.json version
|
|
9
|
+
// is read at runtime via fs + URL (no JSON import attribute, no node:path
|
|
10
|
+
// needed). No disk writes beyond what runScan performs (none).
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import process from 'node:process';
|
|
14
|
+
import { runScan } from './pipeline.js';
|
|
15
|
+
// Resolve package.json relative to this module so the version is correct
|
|
16
|
+
// whether run from dist/cli.js or via the installed bin symlink. URL-based
|
|
17
|
+
// path avoids importing node:path / node:url.
|
|
18
|
+
const pkgUrl = new URL('../package.json', import.meta.url);
|
|
19
|
+
const pkg = JSON.parse(readFileSync(pkgUrl, 'utf8'));
|
|
20
|
+
const program = new Command();
|
|
21
|
+
program
|
|
22
|
+
.name('harnessgap')
|
|
23
|
+
.description('Stateless detection-only CLI for harness gaps')
|
|
24
|
+
.version(pkg.version);
|
|
25
|
+
program
|
|
26
|
+
.command('scan', { isDefault: true })
|
|
27
|
+
.description('Scan Claude Code transcripts for harness gaps')
|
|
28
|
+
.option('--repo <path>', 'filter to a specific repo toplevel')
|
|
29
|
+
.option('--since <dur>', 'only sessions within this lookback (e.g. 30d, 12h)')
|
|
30
|
+
.option('--limit <n>', 'cap the number of sessions', (v) => Number.parseInt(v, 10))
|
|
31
|
+
.option('--json', 'emit the JSON envelope instead of a human table')
|
|
32
|
+
.option('--calibrate', 'emit the calibrate signal view')
|
|
33
|
+
.option('--bootstrap', 'force bootstrap scoring mode')
|
|
34
|
+
.option('--config <path>', 'path to a .harnessgap.yml config file')
|
|
35
|
+
.option('--claude-dir <path>', 'Claude Code config directory (contains projects/)')
|
|
36
|
+
.action(async (opts) => {
|
|
37
|
+
const scanOpts = {
|
|
38
|
+
repo: opts.repo,
|
|
39
|
+
since: opts.since,
|
|
40
|
+
limit: opts.limit,
|
|
41
|
+
json: opts.json,
|
|
42
|
+
calibrate: opts.calibrate,
|
|
43
|
+
bootstrap: opts.bootstrap,
|
|
44
|
+
configPath: opts.config,
|
|
45
|
+
claudeDir: opts.claudeDir,
|
|
46
|
+
};
|
|
47
|
+
try {
|
|
48
|
+
const result = await runScan(scanOpts);
|
|
49
|
+
// Write then exit in the flush callback so piped stdout is never
|
|
50
|
+
// truncated by process.exit.
|
|
51
|
+
process.stdout.write(result.output + '\n', () => process.exit(result.exitCode));
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
// ConfigError carries a clean human message; any other thrown error is
|
|
55
|
+
// surfaced by message only — never the stack.
|
|
56
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
57
|
+
process.stderr.write(`error: ${msg}\n`, () => process.exit(1));
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
// parseAsync returns a promise; the action handles its own errors + exit, so
|
|
61
|
+
// this .catch is a defensive net for anything commander itself rejects (e.g.
|
|
62
|
+
// unknown options — commander prints its own message and exits non-zero there).
|
|
63
|
+
program.parseAsync(process.argv).catch((e) => {
|
|
64
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
65
|
+
process.stderr.write(`error: ${msg}\n`, () => process.exit(1));
|
|
66
|
+
});
|
|
67
|
+
//# sourceMappingURL=cli.js.map
|