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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -0
  3. package/dist/adapter/correction.d.ts +14 -0
  4. package/dist/adapter/correction.js +68 -0
  5. package/dist/adapter/correction.js.map +1 -0
  6. package/dist/adapter/parse.d.ts +21 -0
  7. package/dist/adapter/parse.js +261 -0
  8. package/dist/adapter/parse.js.map +1 -0
  9. package/dist/adapter/scrub.d.ts +15 -0
  10. package/dist/adapter/scrub.js +97 -0
  11. package/dist/adapter/scrub.js.map +1 -0
  12. package/dist/adapter/stream.d.ts +14 -0
  13. package/dist/adapter/stream.js +207 -0
  14. package/dist/adapter/stream.js.map +1 -0
  15. package/dist/adapter/taxonomy.d.ts +7 -0
  16. package/dist/adapter/taxonomy.js +21 -0
  17. package/dist/adapter/taxonomy.js.map +1 -0
  18. package/dist/aggregate/leaderboard.d.ts +26 -0
  19. package/dist/aggregate/leaderboard.js +168 -0
  20. package/dist/aggregate/leaderboard.js.map +1 -0
  21. package/dist/cli.d.ts +2 -0
  22. package/dist/cli.js +67 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/config.d.ts +25 -0
  25. package/dist/config.js +191 -0
  26. package/dist/config.js.map +1 -0
  27. package/dist/detector/areas.d.ts +13 -0
  28. package/dist/detector/areas.js +95 -0
  29. package/dist/detector/areas.js.map +1 -0
  30. package/dist/detector/index.d.ts +14 -0
  31. package/dist/detector/index.js +31 -0
  32. package/dist/detector/index.js.map +1 -0
  33. package/dist/detector/record.d.ts +24 -0
  34. package/dist/detector/record.js +29 -0
  35. package/dist/detector/record.js.map +1 -0
  36. package/dist/detector/scoring.d.ts +21 -0
  37. package/dist/detector/scoring.js +140 -0
  38. package/dist/detector/scoring.js.map +1 -0
  39. package/dist/detector/signals.d.ts +6 -0
  40. package/dist/detector/signals.js +221 -0
  41. package/dist/detector/signals.js.map +1 -0
  42. package/dist/egress.d.ts +8 -0
  43. package/dist/egress.js +37 -0
  44. package/dist/egress.js.map +1 -0
  45. package/dist/git.d.ts +16 -0
  46. package/dist/git.js +59 -0
  47. package/dist/git.js.map +1 -0
  48. package/dist/index.d.ts +1 -0
  49. package/dist/index.js +4 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/output/calibrate.d.ts +35 -0
  52. package/dist/output/calibrate.js +149 -0
  53. package/dist/output/calibrate.js.map +1 -0
  54. package/dist/output/human.d.ts +24 -0
  55. package/dist/output/human.js +83 -0
  56. package/dist/output/human.js.map +1 -0
  57. package/dist/output/json.d.ts +17 -0
  58. package/dist/output/json.js +22 -0
  59. package/dist/output/json.js.map +1 -0
  60. package/dist/pipeline.d.ts +24 -0
  61. package/dist/pipeline.js +164 -0
  62. package/dist/pipeline.js.map +1 -0
  63. package/dist/types.d.ts +122 -0
  64. package/dist/types.js +4 -0
  65. package/dist/types.js.map +1 -0
  66. package/dist/walk.d.ts +12 -0
  67. package/dist/walk.js +92 -0
  68. package/dist/walk.js.map +1 -0
  69. package/package.json +49 -0
@@ -0,0 +1,83 @@
1
+ // Pure human-readable leaderboard formatter (the §8 table). Turns area rows +
2
+ // summary + warnings into a column-aligned string. No I/O, no raw prose — only
3
+ // area keys, counts, scores, and signal display strings appear in the output.
4
+ // Fixed column widths for the leaderboard table.
5
+ const AREA_W = 32;
6
+ const FLAGGED_W = 8;
7
+ const MEAN_W = 11;
8
+ // Fixed canonical order + human labels for the warnings line. Zero-count
9
+ // categories are omitted from the output.
10
+ const WARNING_PARTS = [
11
+ { key: 'malformed_lines', label: 'malformed lines' },
12
+ { key: 'oversized_lines', label: 'oversized lines' },
13
+ { key: 'skipped_sessions', label: 'skipped sessions' },
14
+ { key: 'truncated_sessions', label: 'truncated sessions' },
15
+ { key: 'symlinks_rejected', label: 'symlinks rejected' },
16
+ { key: 'unresolvable_cwd', label: 'unresolvable cwd' },
17
+ ];
18
+ /**
19
+ * Format the scan leaderboard as a human-readable string. Pure.
20
+ *
21
+ * Layout: header line, column-aligned table (or a "no flagged areas" line when
22
+ * empty), summary line, and a warnings line (only non-zero categories). The
23
+ * bootstrap count in the summary line is `sessionCount` when mode is bootstrap,
24
+ * else 0.
25
+ */
26
+ export function formatHuman(input) {
27
+ const { repo, mode, sessionCount, areas, summary, warnings } = input;
28
+ const lines = [];
29
+ lines.push(`harnessgap scan — repo: ${repo} · ${sessionCount} sessions · mode: ${mode}`);
30
+ if (areas.length === 0) {
31
+ lines.push('No flagged areas.');
32
+ }
33
+ else {
34
+ lines.push(tableHeader());
35
+ for (const area of areas) {
36
+ lines.push(tableRow(area));
37
+ }
38
+ }
39
+ const bootCount = mode === 'bootstrap' ? sessionCount : 0;
40
+ lines.push(`${summary.flagged} areas flagged · ${summary.unflagged} unflagged · ${summary.unlocalized} unlocalized · bootstrap: ${bootCount} sessions`);
41
+ const wLine = warningsLine(warnings);
42
+ if (wLine !== null)
43
+ lines.push(wLine);
44
+ return lines.join('\n');
45
+ }
46
+ /** The column-header row, aligned to match the data rows. */
47
+ function tableHeader() {
48
+ return `${'AREA'.padEnd(AREA_W)} | ${'FLAGGED'.padStart(FLAGGED_W)} | ${'MEAN SCORE'.padStart(MEAN_W)} | TOP SIGNALS`;
49
+ }
50
+ /** One area row, column-aligned. */
51
+ function tableRow(area) {
52
+ const areaCol = fit(area.key, AREA_W);
53
+ const flaggedCol = String(area.sessions_flagged).padStart(FLAGGED_W);
54
+ const meanCol = area.mean_score.toFixed(1).padStart(MEAN_W);
55
+ const signalsCol = area.top_signals.map((t) => t.display).join(', ');
56
+ return `${areaCol} | ${flaggedCol} | ${meanCol} | ${signalsCol}`;
57
+ }
58
+ /**
59
+ * Fit a string to `width`: pad with spaces if shorter, truncate with "..." if
60
+ * longer. Keeps the column alignment intact.
61
+ */
62
+ function fit(s, width) {
63
+ if (s.length <= width)
64
+ return s.padEnd(width);
65
+ return s.slice(0, width - 3) + '...';
66
+ }
67
+ /**
68
+ * Build the warnings line as `warnings: <n> <label>, <n> <label>`, omitting any
69
+ * category whose count is 0. Returns null when every category is 0 (line
70
+ * omitted entirely).
71
+ */
72
+ function warningsLine(warnings) {
73
+ const parts = [];
74
+ for (const { key, label } of WARNING_PARTS) {
75
+ const count = warnings[key];
76
+ if (count > 0)
77
+ parts.push(`${count} ${label}`);
78
+ }
79
+ if (parts.length === 0)
80
+ return null;
81
+ return `warnings: ${parts.join(', ')}`;
82
+ }
83
+ //# sourceMappingURL=human.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"human.js","sourceRoot":"","sources":["../../src/output/human.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { AreaRow, JsonOutput, ScoringMode, StruggleRecord, Warnings } from '../types.js';
2
+ /** Inputs to `buildJsonEnvelope`; all fields projected verbatim onto JsonOutput. */
3
+ interface JsonEnvelopeInput {
4
+ repo: string;
5
+ mode: ScoringMode;
6
+ session_count: number;
7
+ warnings: Warnings;
8
+ sessions: StruggleRecord[];
9
+ areas: AreaRow[];
10
+ }
11
+ /**
12
+ * Assemble the `JsonOutput` envelope. Pure: returns a new object whose fields
13
+ * are the inputs plus `schema_version: 1`. Does not clone `sessions`/`areas`
14
+ * (callers may share references; the envelope is read-only by contract).
15
+ */
16
+ export declare function buildJsonEnvelope(input: JsonEnvelopeInput): JsonOutput;
17
+ export {};
@@ -0,0 +1,22 @@
1
+ // Pure JSON envelope assembler. Turns the detector + aggregator outputs into
2
+ // the `JsonOutput` shape consumed by the `--json` CLI flag. No I/O, no
3
+ // transformation beyond direct field projection: sessions and areas are passed
4
+ // through as-is (already scrubbed of prose upstream), warnings are integer
5
+ // counts. schema_version is pinned to 1.
6
+ /**
7
+ * Assemble the `JsonOutput` envelope. Pure: returns a new object whose fields
8
+ * are the inputs plus `schema_version: 1`. Does not clone `sessions`/`areas`
9
+ * (callers may share references; the envelope is read-only by contract).
10
+ */
11
+ export function buildJsonEnvelope(input) {
12
+ return {
13
+ schema_version: 1,
14
+ repo: input.repo,
15
+ mode: input.mode,
16
+ session_count: input.session_count,
17
+ warnings: input.warnings,
18
+ sessions: input.sessions,
19
+ areas: input.areas,
20
+ };
21
+ }
22
+ //# sourceMappingURL=json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.js","sourceRoot":"","sources":["../../src/output/json.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,uEAAuE;AACvE,+EAA+E;AAC/E,2EAA2E;AAC3E,yCAAyC;AAoBzC;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAwB;IACxD,OAAO;QACL,cAAc,EAAE,CAAC;QACjB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,aAAa,EAAE,KAAK,CAAC,aAAa;QAClC,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,KAAK,EAAE,KAAK,CAAC,KAAK;KACnB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,24 @@
1
+ import type { ScoringMode, Warnings } from './types.js';
2
+ export interface ScanOptions {
3
+ repo?: string;
4
+ since?: string;
5
+ limit?: number;
6
+ json?: boolean;
7
+ calibrate?: boolean;
8
+ bootstrap?: boolean;
9
+ configPath?: string;
10
+ claudeDir?: string;
11
+ }
12
+ export interface ScanResult {
13
+ output: string;
14
+ mode: ScoringMode;
15
+ sessionCount: number;
16
+ warnings: Warnings;
17
+ exitCode: 0 | 1;
18
+ }
19
+ /**
20
+ * Orchestrate the full harnessgap scan. Async (awaits streamSession +
21
+ * resolveToplevel). Returns the output string, mode, session count, aggregated
22
+ * warnings, and exitCode (always 0 — misconfig throws propagate to the CLI).
23
+ */
24
+ export declare function runScan(opts: ScanOptions): Promise<ScanResult>;
@@ -0,0 +1,164 @@
1
+ // Stateless scan pipeline — the thin I/O shell that orchestrates walk → stream
2
+ // → git-resolve → detect → aggregate → output. All pure logic lives in the
3
+ // modules it composes; this file threads them together, applies filters, and
4
+ // branches the output form. No disk writes, no network. Async because
5
+ // streamSession and resolveToplevel are async.
6
+ //
7
+ // Filter / mode / output-branching choices (documented here, pinned to the
8
+ // task-16 brief + resolution notes):
9
+ //
10
+ // - repo filter when opts.repo is unset: resolve `resolveToplevel(process.cwd())`.
11
+ // If it resolves, filter envelopes to that toplevel; if it does NOT resolve
12
+ // (process.cwd() is not a repo), keep ALL envelopes with a non-empty repo.
13
+ // - mode when no records: "bootstrap" (consistent with scoreSessions: 0 sessions
14
+ // < bootstrap_session_floor → bootstrap).
15
+ // - repo for output: the filtered repo (opts.repo, or the resolved process.cwd()
16
+ // toplevel, or "" if neither resolves).
17
+ // - --since: parseDuration(opts.since) → ms (Infinity → no filter). Filter:
18
+ // Date.parse(started_at) >= now − sinceMs. Empty/unparseable started_at →
19
+ // excluded (can't date it).
20
+ // - --limit: applied AFTER all filtering. Negative limits are ignored.
21
+ // - empty cwd (streamSession returned cwd=''): treated as unresolvable
22
+ // (unresolvable_cwd++, skipped) — resolveToplevel('') would wrongly resolve
23
+ // process.cwd()'s repo.
24
+ // - ConfigError from loadConfig / parseDuration is NOT caught here — it
25
+ // propagates to the CLI for non-zero exit. runScan's exitCode is always 0.
26
+ import { loadConfig, parseDuration } from './config.js';
27
+ import { discoverTranscripts, defaultClaudeDir } from './walk.js';
28
+ import { streamSession } from './adapter/stream.js';
29
+ import { resolveToplevel } from './git.js';
30
+ import { runDetector } from './detector/index.js';
31
+ import { aggregateAreas } from './aggregate/leaderboard.js';
32
+ import { buildJsonEnvelope } from './output/json.js';
33
+ import { formatHuman } from './output/human.js';
34
+ import { buildCalibrateObject, formatCalibrateTable } from './output/calibrate.js';
35
+ /**
36
+ * Orchestrate the full harnessgap scan. Async (awaits streamSession +
37
+ * resolveToplevel). Returns the output string, mode, session count, aggregated
38
+ * warnings, and exitCode (always 0 — misconfig throws propagate to the CLI).
39
+ */
40
+ export async function runScan(opts) {
41
+ // 1. Load config (ConfigError propagates — not caught here).
42
+ const cfg = loadConfig(opts.configPath);
43
+ // 2. Discover transcripts.
44
+ const claudeDir = opts.claudeDir ?? defaultClaudeDir();
45
+ const { files, symlinks_rejected } = discoverTranscripts(claudeDir);
46
+ const warnings = {
47
+ malformed_lines: 0,
48
+ oversized_lines: 0,
49
+ skipped_sessions: 0,
50
+ truncated_sessions: 0,
51
+ symlinks_rejected,
52
+ unresolvable_cwd: 0,
53
+ };
54
+ // 3. Stream each file, resolve repo, accumulate warnings. Thread a single
55
+ // git-cache across all sessions (cwd repeats are common across transcripts).
56
+ const cache = new Map();
57
+ const envelopes = [];
58
+ for (const file of files) {
59
+ const { envelope, cwd, warnings: streamWarnings } = await streamSession(file);
60
+ warnings.malformed_lines += streamWarnings.malformed_lines;
61
+ warnings.oversized_lines += streamWarnings.oversized_lines;
62
+ warnings.truncated_sessions += streamWarnings.truncated_sessions;
63
+ // Empty cwd → can't resolve a repo (resolveToplevel('') would resolve
64
+ // process.cwd()'s repo, which is wrong). Skip as unresolvable.
65
+ if (cwd === '') {
66
+ warnings.unresolvable_cwd += 1;
67
+ warnings.skipped_sessions += 1;
68
+ continue;
69
+ }
70
+ const toplevel = await resolveToplevel(cwd, cache);
71
+ if (!toplevel) {
72
+ warnings.unresolvable_cwd += 1;
73
+ warnings.skipped_sessions += 1;
74
+ continue;
75
+ }
76
+ envelope.repo = toplevel;
77
+ envelopes.push(envelope);
78
+ }
79
+ // 4. Filter by repo.
80
+ let filterRepo;
81
+ if (opts.repo !== undefined) {
82
+ filterRepo = opts.repo;
83
+ }
84
+ else {
85
+ const cwdToplevel = await resolveToplevel(process.cwd(), cache);
86
+ filterRepo = cwdToplevel ?? '';
87
+ }
88
+ let filtered;
89
+ if (filterRepo !== '') {
90
+ filtered = envelopes.filter((e) => e.repo === filterRepo);
91
+ }
92
+ else {
93
+ // process.cwd() didn't resolve and no opts.repo — keep all envelopes with
94
+ // a non-empty repo.
95
+ filtered = envelopes.filter((e) => e.repo !== '');
96
+ }
97
+ // 5. Apply --since (filter by started_at ≥ now − duration).
98
+ if (opts.since !== undefined) {
99
+ const sinceMs = parseDuration(opts.since);
100
+ if (sinceMs !== Infinity) {
101
+ const cutoff = Date.now() - sinceMs;
102
+ filtered = filtered.filter((e) => {
103
+ if (e.started_at === '')
104
+ return false;
105
+ const t = Date.parse(e.started_at);
106
+ return !Number.isNaN(t) && t >= cutoff;
107
+ });
108
+ }
109
+ }
110
+ // Apply --limit (cap count, AFTER all filtering).
111
+ if (opts.limit !== undefined && opts.limit >= 0) {
112
+ filtered = filtered.slice(0, opts.limit);
113
+ }
114
+ // 6. Run detector.
115
+ const forceBootstrap = opts.bootstrap ?? false;
116
+ const records = runDetector(filtered, cfg, forceBootstrap);
117
+ // 7. Aggregate areas.
118
+ const { rows, summary } = aggregateAreas(records, cfg);
119
+ // 8. Determine mode (consistent across all records; default bootstrap when
120
+ // no records — matches scoreSessions' selectMode for n=0).
121
+ const mode = records.length > 0 ? records[0].mode : 'bootstrap';
122
+ // repo for output: the repo envelopes were filtered to (or "" if none).
123
+ const outputRepo = filterRepo;
124
+ // 9. Build output — branch by --calibrate / --json / human.
125
+ let output;
126
+ if (opts.calibrate) {
127
+ const calObj = buildCalibrateObject({
128
+ mode,
129
+ session_count: records.length,
130
+ flag_pct: cfg.detector.flag_pct,
131
+ signals: records.map((r) => r.signals),
132
+ bootstrap_thresholds: cfg.detector.bootstrap_thresholds,
133
+ });
134
+ output = opts.json ? JSON.stringify(calObj) : formatCalibrateTable(calObj);
135
+ }
136
+ else if (opts.json) {
137
+ output = JSON.stringify(buildJsonEnvelope({
138
+ repo: outputRepo,
139
+ mode,
140
+ session_count: records.length,
141
+ warnings,
142
+ sessions: records,
143
+ areas: rows,
144
+ }));
145
+ }
146
+ else {
147
+ output = formatHuman({
148
+ repo: outputRepo,
149
+ mode,
150
+ sessionCount: records.length,
151
+ areas: rows,
152
+ summary,
153
+ warnings,
154
+ });
155
+ }
156
+ return {
157
+ output,
158
+ mode,
159
+ sessionCount: records.length,
160
+ warnings,
161
+ exitCode: 0,
162
+ };
163
+ }
164
+ //# sourceMappingURL=pipeline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,122 @@
1
+ export type ToolKind = 'read' | 'search' | 'list' | 'edit' | 'exec' | 'other';
2
+ export type EventKind = 'user_msg' | 'assistant_msg' | 'tool_call';
3
+ export type SignalName = 'explore_ratio' | 'reread' | 'failure_streak' | 'corrections' | 'abandonment' | 'oscillation' | 'wall_clock_per_line';
4
+ export type ScoringMode = 'percentile' | 'bootstrap';
5
+ export interface InputDigest {
6
+ files: string[];
7
+ cmd: string | null;
8
+ query: string | null;
9
+ lines_changed: number | null;
10
+ }
11
+ export interface Correction {
12
+ matched: boolean;
13
+ shape: string | null;
14
+ }
15
+ export interface NormalizedEvent {
16
+ t: string;
17
+ kind: EventKind;
18
+ tool: ToolKind | null;
19
+ input_digest: InputDigest;
20
+ ok: boolean;
21
+ interrupted: boolean;
22
+ duration_ms: number;
23
+ correction: Correction | null;
24
+ }
25
+ export interface NormalizedEnvelope {
26
+ schema_version: 1;
27
+ session_id: string;
28
+ agent: 'claude-code';
29
+ repo: string;
30
+ started_at: string;
31
+ duration_ms: number;
32
+ events: NormalizedEvent[];
33
+ truncated: boolean;
34
+ event_count: number;
35
+ }
36
+ export interface SignalValues {
37
+ explore_ratio: number | null;
38
+ reread: number;
39
+ failure_streak: number;
40
+ corrections: number;
41
+ abandonment: boolean;
42
+ oscillation: number;
43
+ wall_clock_per_line_ms: number | null;
44
+ }
45
+ export interface StruggleRecord {
46
+ session_id: string;
47
+ repo: string;
48
+ started_at: string;
49
+ duration_ms: number;
50
+ score_pct: number;
51
+ mode: ScoringMode;
52
+ flagged: boolean;
53
+ truncated: boolean;
54
+ event_count: number;
55
+ areas: {
56
+ key: string;
57
+ weight: number;
58
+ }[];
59
+ signals: SignalValues;
60
+ }
61
+ export interface Warnings {
62
+ malformed_lines: number;
63
+ oversized_lines: number;
64
+ skipped_sessions: number;
65
+ truncated_sessions: number;
66
+ symlinks_rejected: number;
67
+ unresolvable_cwd: number;
68
+ }
69
+ export interface AreaRow {
70
+ key: string;
71
+ sessions_total: number;
72
+ sessions_flagged: number;
73
+ mean_score: number;
74
+ top_signals: {
75
+ name: SignalName;
76
+ value: number | boolean;
77
+ display: string;
78
+ }[];
79
+ }
80
+ export interface JsonOutput {
81
+ schema_version: 1;
82
+ repo: string;
83
+ mode: ScoringMode;
84
+ session_count: number;
85
+ warnings: Warnings;
86
+ sessions: StruggleRecord[];
87
+ areas: AreaRow[];
88
+ }
89
+ export interface Config {
90
+ detector: {
91
+ thresholds_as: 'percentile' | 'absolute';
92
+ flag_pct: number;
93
+ bootstrap_session_floor: number;
94
+ bootstrap_flag_pct: number;
95
+ reread_threshold: number;
96
+ correction_window_ms: number;
97
+ signal_weights: Record<SignalName, number>;
98
+ bootstrap_thresholds: {
99
+ explore_ratio: number;
100
+ reread: number;
101
+ failure_streak: number;
102
+ corrections: number;
103
+ abandonment: boolean;
104
+ oscillation: number;
105
+ wall_clock_per_line_ms: number;
106
+ };
107
+ };
108
+ areas: {
109
+ ignore: string[];
110
+ min_weight: number;
111
+ min_depth: number;
112
+ touch_weights: {
113
+ edit: number;
114
+ read: number;
115
+ exec: number;
116
+ };
117
+ tail_fraction: number;
118
+ explore_ratio_min: number;
119
+ suppress_abandonment_when_no_exec: boolean;
120
+ test_cmd_patterns: string[];
121
+ };
122
+ }
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ // Shared type catalog for harnessgap. Every later task imports from here.
2
+ // Field names and shapes are contracts pinned to the design spec.
3
+ export {};
4
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,kEAAkE"}
package/dist/walk.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ /** Default Claude config directory: `~/.claude`. */
2
+ export declare function defaultClaudeDir(): string;
3
+ /**
4
+ * Discover all .jsonl transcripts under <claudeDir>/projects/<slug>/<file>.jsonl
5
+ * (exactly one session-dir level under projects/). Rejects symlinks (lstat,
6
+ * never followed) and confines results to the projects/ prefix. Fail-open on
7
+ * missing projects/ dir.
8
+ */
9
+ export declare function discoverTranscripts(claudeDir: string): {
10
+ files: string[];
11
+ symlinks_rejected: number;
12
+ };
package/dist/walk.js ADDED
@@ -0,0 +1,92 @@
1
+ // Transcript discovery — finds every *.jsonl under
2
+ // <claudeDir>/projects/*/*.jsonl (exactly one session-dir level under
3
+ // projects/). Security-critical: NEVER follows symlinks.
4
+ //
5
+ // - Directory traversal uses readdir(withFileTypes): Dirent.isDirectory() is
6
+ // false for symlinks (even symlinks-to-dirs), so symlinked session-dirs are
7
+ // skipped at the directory level — never entered, never realpath'd.
8
+ // - Each .jsonl candidate is lstat'd (not stat'd): a symlink .jsonl is rejected
9
+ // and counted in symlinks_rejected regardless of where it points.
10
+ // - A defensive prefix-confinement check rejects any resolved path that escapes
11
+ // <claudeDir>/projects/ (guards against `..`-style traversal in dir names,
12
+ // which readdir itself never produces but we verify anyway).
13
+ //
14
+ // Does NOT read file contents (streamSession in adapter/stream.ts does that).
15
+ // Fail-open: a missing or unreadable projects/ dir yields an empty result,
16
+ // never a throw.
17
+ //
18
+ // Sync I/O is chosen deliberately: the pipeline walks a bounded number of
19
+ // files and sync code avoids an await at every call site. Output is sorted
20
+ // lexicographically so the pipeline sees a stable input order.
21
+ import * as fs from 'node:fs';
22
+ import * as path from 'node:path';
23
+ import * as os from 'node:os';
24
+ /** Default Claude config directory: `~/.claude`. */
25
+ export function defaultClaudeDir() {
26
+ return path.join(os.homedir(), '.claude');
27
+ }
28
+ /**
29
+ * Discover all .jsonl transcripts under <claudeDir>/projects/<slug>/<file>.jsonl
30
+ * (exactly one session-dir level under projects/). Rejects symlinks (lstat,
31
+ * never followed) and confines results to the projects/ prefix. Fail-open on
32
+ * missing projects/ dir.
33
+ */
34
+ export function discoverTranscripts(claudeDir) {
35
+ const files = [];
36
+ let symlinks_rejected = 0;
37
+ const projectsDir = path.resolve(claudeDir, 'projects');
38
+ const prefix = projectsDir + path.sep;
39
+ let topEntries;
40
+ try {
41
+ topEntries = fs.readdirSync(projectsDir, { withFileTypes: true });
42
+ }
43
+ catch {
44
+ // Missing or unreadable projects/ dir — fail open.
45
+ return { files, symlinks_rejected };
46
+ }
47
+ for (const dirent of topEntries) {
48
+ // Exactly one session-dir level under projects/. isDirectory() is false
49
+ // for symlinks, so symlinked session-dirs are skipped here without being
50
+ // traversed or realpath'd.
51
+ if (!dirent.isDirectory())
52
+ continue;
53
+ const sessionDir = path.join(projectsDir, dirent.name);
54
+ let sessionEntries;
55
+ try {
56
+ sessionEntries = fs.readdirSync(sessionDir, { withFileTypes: true });
57
+ }
58
+ catch {
59
+ continue; // unreadable session-dir — skip
60
+ }
61
+ for (const fe of sessionEntries) {
62
+ if (!fe.name.endsWith('.jsonl'))
63
+ continue;
64
+ const candidate = path.join(sessionDir, fe.name);
65
+ let st;
66
+ try {
67
+ st = fs.lstatSync(candidate);
68
+ }
69
+ catch {
70
+ continue; // unreadable entry — skip
71
+ }
72
+ // lstat (not stat): never follows symlinks. A symlink .jsonl is rejected
73
+ // and counted, regardless of its target.
74
+ if (st.isSymbolicLink()) {
75
+ symlinks_rejected++;
76
+ continue;
77
+ }
78
+ if (!st.isFile())
79
+ continue; // dirs / sockets / fifos named *.jsonl
80
+ // Defensive confinement: the resolved absolute path must live under
81
+ // projects/. Belt-and-suspenders against path-traversal via `..` in dir
82
+ // names (readdir never emits such names, but we verify anyway).
83
+ const resolved = path.resolve(candidate);
84
+ if (!resolved.startsWith(prefix))
85
+ continue;
86
+ files.push(resolved);
87
+ }
88
+ }
89
+ files.sort();
90
+ return { files, symlinks_rejected };
91
+ }
92
+ //# sourceMappingURL=walk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"walk.js","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "harnessgap",
3
+ "version": "0.1.0",
4
+ "description": "Stateless detection-only CLI that finds harness gaps in Claude Code transcripts",
5
+ "license": "MIT",
6
+ "author": "Dmitry Teryaev",
7
+ "homepage": "https://github.com/HumanBean17/harnessgap#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/HumanBean17/harnessgap.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/HumanBean17/harnessgap/issues"
14
+ },
15
+ "keywords": [
16
+ "claude-code",
17
+ "cli",
18
+ "transcripts",
19
+ "developer-tools",
20
+ "observability",
21
+ "agent-harness"
22
+ ],
23
+ "type": "module",
24
+ "engines": {
25
+ "node": ">=22.12"
26
+ },
27
+ "bin": {
28
+ "harnessgap": "dist/cli.js"
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "scripts": {
34
+ "build": "tsc",
35
+ "dev": "tsx src/cli.ts",
36
+ "test": "vitest run",
37
+ "typecheck": "tsc --noEmit"
38
+ },
39
+ "dependencies": {
40
+ "commander": "^15.0.0",
41
+ "yaml": "^2.9.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^26.1.1",
45
+ "tsx": "^4.23.0",
46
+ "typescript": "^7.0.2",
47
+ "vitest": "^4.1.10"
48
+ }
49
+ }