@harness-engineering/cli 1.24.3 → 1.25.1
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/dist/agents/skills/claude-code/harness-architecture-advisor/SKILL.md +10 -6
- package/dist/agents/skills/claude-code/harness-code-review/SKILL.md +40 -5
- package/dist/agents/skills/claude-code/harness-git-workflow/SKILL.md +32 -11
- package/dist/agents/skills/claude-code/harness-planning/SKILL.md +19 -0
- package/dist/agents/skills/claude-code/harness-skill-authoring/SKILL.md +42 -7
- package/dist/agents/skills/claude-code/harness-verification/SKILL.md +25 -0
- package/dist/agents/skills/codex/harness-architecture-advisor/SKILL.md +10 -6
- package/dist/agents/skills/codex/harness-code-review/SKILL.md +40 -5
- package/dist/agents/skills/codex/harness-git-workflow/SKILL.md +32 -11
- package/dist/agents/skills/codex/harness-planning/SKILL.md +19 -0
- package/dist/agents/skills/codex/harness-skill-authoring/SKILL.md +42 -7
- package/dist/agents/skills/codex/harness-verification/SKILL.md +25 -0
- package/dist/agents/skills/cursor/harness-architecture-advisor/SKILL.md +10 -6
- package/dist/agents/skills/cursor/harness-code-review/SKILL.md +40 -5
- package/dist/agents/skills/cursor/harness-git-workflow/SKILL.md +32 -11
- package/dist/agents/skills/cursor/harness-planning/SKILL.md +19 -0
- package/dist/agents/skills/cursor/harness-skill-authoring/SKILL.md +42 -7
- package/dist/agents/skills/cursor/harness-verification/SKILL.md +25 -0
- package/dist/agents/skills/gemini-cli/harness-architecture-advisor/SKILL.md +10 -6
- package/dist/agents/skills/gemini-cli/harness-code-review/SKILL.md +40 -5
- package/dist/agents/skills/gemini-cli/harness-git-workflow/SKILL.md +32 -11
- package/dist/agents/skills/gemini-cli/harness-planning/SKILL.md +19 -0
- package/dist/agents/skills/gemini-cli/harness-skill-authoring/SKILL.md +42 -7
- package/dist/agents/skills/gemini-cli/harness-verification/SKILL.md +25 -0
- package/dist/agents-md-MCUM4SIZ.js +10 -0
- package/dist/{architecture-FBSLURIB.js → architecture-HNIO6AUX.js} +6 -5
- package/dist/{assess-project-74UVWPMB.js → assess-project-6MV5TNY3.js} +2 -1
- package/dist/bin/harness-mcp.js +18 -17
- package/dist/bin/harness.js +47 -31
- package/dist/{check-phase-gate-WY6UICCL.js → check-phase-gate-VCBQHQAC.js} +6 -5
- package/dist/{chunk-Q3XYV5UC.js → chunk-47N6R2F4.js} +5 -1
- package/dist/{chunk-3XAPHB2Z.js → chunk-4NK7Z3BP.js} +1 -1
- package/dist/{chunk-I4QR3HNH.js → chunk-5BQ5BOJL.js} +5 -1
- package/dist/{chunk-LM5Z2WCA.js → chunk-6VZQJ5CX.js} +5 -1
- package/dist/{chunk-NV4FPO7J.js → chunk-AT74HEQM.js} +1 -1
- package/dist/{chunk-TDLOFNNQ.js → chunk-BOQRTARD.js} +1 -1
- package/dist/{chunk-ZAMT24QN.js → chunk-CJXVNDZZ.js} +1537 -737
- package/dist/{chunk-A737JDL4.js → chunk-CXJTVICF.js} +4 -4
- package/dist/{chunk-LOUH2LIC.js → chunk-EHRZMOQ2.js} +5 -1
- package/dist/{chunk-L5UONZ53.js → chunk-F23H3U5U.js} +6 -2
- package/dist/{chunk-YPKKEP3O.js → chunk-FES2YEQU.js} +9 -9
- package/dist/{chunk-SPTKLCKC.js → chunk-FIQL2HND.js} +6 -2
- package/dist/{chunk-FP53DDB5.js → chunk-FSNPBT74.js} +5 -1
- package/dist/{chunk-TB427QOK.js → chunk-GEEYCQDS.js} +13 -9
- package/dist/{chunk-ZOVATVQC.js → chunk-HIK6OEUF.js} +6 -1
- package/dist/chunk-K2SON7TI.js +5382 -0
- package/dist/chunk-KFQGP6VL.js +33 -0
- package/dist/{chunk-V2FGX2KD.js → chunk-M55DGGF3.js} +9 -5
- package/dist/{chunk-H4U2QNY2.js → chunk-MI6MA6OP.js} +16888 -13143
- package/dist/{chunk-YDRB55Q4.js → chunk-P7PANON5.js} +5 -1
- package/dist/chunk-RL4VMEXL.js +53 -0
- package/dist/{chunk-XNEMDKV5.js → chunk-RRHUCDRD.js} +1 -1
- package/dist/{chunk-O6UF33QH.js → chunk-RX7TUMBR.js} +320 -136
- package/dist/{chunk-3VYWO6QN.js → chunk-TYV4EUAD.js} +12 -8
- package/dist/{chunk-FJYP32IV.js → chunk-UQZBZINS.js} +6 -6
- package/dist/{chunk-UFQQBGC3.js → chunk-UV3BZMGT.js} +64 -3
- package/dist/{chunk-5PMRARB5.js → chunk-WIQA4BSH.js} +12 -5
- package/dist/{chunk-UQEUYRBP.js → chunk-WWACIIBA.js} +1 -1
- package/dist/{chunk-LVYPPKZI.js → chunk-XTITAVUR.js} +5 -1
- package/dist/{chunk-WEN5Z7CL.js → chunk-YTP2UDPV.js} +3 -3
- package/dist/ci-workflow-RTM7VVTD.js +10 -0
- package/dist/{constants-5JGUXPEK.js → constants-P4M3C2T3.js} +1 -0
- package/dist/{create-skill-QCXINA5Q.js → create-skill-6QWJHQYS.js} +3 -2
- package/dist/{dist-666AAZQ6.js → dist-3EWNRFFQ.js} +6 -1
- package/dist/{dist-RRMRZRV7.js → dist-ABTKRYZH.js} +1 -0
- package/dist/{dist-QW5G3GX3.js → dist-RADHOOXG.js} +4 -1
- package/dist/{dist-7EBSGAHX.js → dist-WCSJHQPK.js} +106 -3
- package/dist/{docs-H34GBVRS.js → docs-UBOGGHTY.js} +6 -5
- package/dist/engine-MJJAP5CH.js +10 -0
- package/dist/{entropy-ZAY73R6A.js → entropy-EMSXF2PX.js} +5 -4
- package/dist/{feedback-TMEGYMWU.js → feedback-ZLUX72HD.js} +2 -1
- package/dist/{generate-agent-definitions-PQPG6SX5.js → generate-agent-definitions-AWLPJ27C.js} +6 -5
- package/dist/{glob-helper-SRXMZPKM.js → glob-helper-VCQXK5XY.js} +2 -0
- package/dist/{graph-loader-M6FXJAKK.js → graph-loader-JHQVQRUS.js} +2 -1
- package/dist/hooks/adoption-tracker.js +189 -0
- package/dist/hooks/block-no-verify.js +50 -0
- package/dist/hooks/cost-tracker.js +66 -0
- package/dist/hooks/pre-compact-state.js +115 -0
- package/dist/hooks/profiles.ts +48 -0
- package/dist/hooks/protect-config.js +75 -0
- package/dist/hooks/quality-gate.js +131 -0
- package/dist/hooks/sentinel-post.js +159 -0
- package/dist/hooks/sentinel-pre.js +244 -0
- package/dist/hooks/telemetry-reporter.js +248 -0
- package/dist/index.d.ts +13 -7
- package/dist/index.js +30 -29
- package/dist/loader-JVSJZSWZ.js +12 -0
- package/dist/mcp-2553PNUC.js +38 -0
- package/dist/{performance-N67YJJDG.js → performance-7AGWJUY4.js} +6 -5
- package/dist/review-pipeline-ZWVQJTJX.js +13 -0
- package/dist/{runner-TY7DJGQV.js → runner-AAEF2TXY.js} +1 -0
- package/dist/runtime-D6YUQPP2.js +11 -0
- package/dist/{scan-U67OKDRS.js → scan-MPJ6JHUY.js} +2 -1
- package/dist/security-JLZUAQYT.js +14 -0
- package/dist/skill-executor-MOCUIAYS.js +9 -0
- package/dist/templates/base/.agnix.toml +16 -0
- package/dist/templates/orchestrator/WORKFLOW.md +55 -7
- package/dist/templates/orchestrator/template.json +1 -0
- package/dist/tool-tiers-7QGZ3FKY.js +98 -0
- package/dist/validate-TIIHRPMA.js +14 -0
- package/dist/validate-cross-check-ZOWFA3DB.js +10 -0
- package/dist/{version-KFFPOQAX.js → version-CUI434OP.js} +1 -0
- package/package.json +4 -4
- package/dist/agents-md-PBKKTSQY.js +0 -9
- package/dist/chunk-5LMZA5LZ.js +0 -38
- package/dist/chunk-OZIHPMA7.js +0 -5171
- package/dist/ci-workflow-QZRHAIO2.js +0 -9
- package/dist/engine-VUQEAJFZ.js +0 -9
- package/dist/loader-Y6A42WBD.js +0 -11
- package/dist/mcp-LCHC4NZ5.js +0 -37
- package/dist/review-pipeline-YXF5ITL2.js +0 -12
- package/dist/runtime-XNJUJCSG.js +0 -10
- package/dist/security-L2YN3CTI.js +0 -9
- package/dist/skill-executor-GA7BDX3F.js +0 -8
- package/dist/validate-MNE25KLZ.js +0 -13
- package/dist/validate-cross-check-Y4PDR63C.js +0 -9
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// adoption-tracker.js — Stop:* hook
|
|
3
|
+
// Reads .harness/events.jsonl, reconstructs skill invocations,
|
|
4
|
+
// appends SkillInvocationRecord entries to .harness/metrics/adoption.jsonl.
|
|
5
|
+
// Exit codes: 0 = allow (always, log-only hook)
|
|
6
|
+
|
|
7
|
+
import { readFileSync, mkdirSync, appendFileSync, existsSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import process from 'node:process';
|
|
10
|
+
|
|
11
|
+
/** Read and parse a JSON file, returning null on any error. */
|
|
12
|
+
function readJsonSafe(filePath) {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Check if adoption tracking is enabled in harness.config.json. */
|
|
21
|
+
function isAdoptionEnabled(cwd) {
|
|
22
|
+
const config = readJsonSafe(join(cwd, 'harness.config.json'));
|
|
23
|
+
if (!config) return true; // default: enabled
|
|
24
|
+
if (config.adoption && config.adoption.enabled === false) return false;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Parse events.jsonl into an array of event objects. Skips malformed lines. */
|
|
29
|
+
function parseEventsFile(eventsPath) {
|
|
30
|
+
let raw;
|
|
31
|
+
try {
|
|
32
|
+
raw = readFileSync(eventsPath, 'utf-8');
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const events = [];
|
|
38
|
+
const lines = raw.split('\n');
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
const trimmed = line.trim();
|
|
41
|
+
if (!trimmed) continue;
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(trimmed);
|
|
44
|
+
if (parsed.skill && parsed.type && parsed.timestamp) {
|
|
45
|
+
events.push(parsed);
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Skip malformed lines
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return events;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Relevant event types for adoption tracking. */
|
|
55
|
+
const RELEVANT_TYPES = new Set(['phase_transition', 'gate_result', 'handoff', 'error']);
|
|
56
|
+
|
|
57
|
+
/** Derive outcome from a skill's events. */
|
|
58
|
+
function deriveOutcome(events) {
|
|
59
|
+
const hasHandoff = events.some((e) => e.type === 'handoff');
|
|
60
|
+
const hasError = events.some((e) => e.type === 'error');
|
|
61
|
+
|
|
62
|
+
// Check for final phase (VALIDATE is the conventional final phase)
|
|
63
|
+
const phases = events
|
|
64
|
+
.filter((e) => e.type === 'phase_transition')
|
|
65
|
+
.map((e) => (e.data && e.data.to) || '')
|
|
66
|
+
.filter(Boolean);
|
|
67
|
+
const hasFinalPhase = phases.some(
|
|
68
|
+
(p) => p.toLowerCase() === 'validate' || p.toLowerCase() === 'complete'
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (hasHandoff || hasFinalPhase) return 'completed';
|
|
72
|
+
if (hasError) return 'failed';
|
|
73
|
+
return 'abandoned';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Derive phases reached from phase_transition events. */
|
|
77
|
+
function derivePhasesReached(events) {
|
|
78
|
+
const phases = [];
|
|
79
|
+
const seen = new Set();
|
|
80
|
+
for (const event of events) {
|
|
81
|
+
if (event.type === 'phase_transition' && event.data && event.data.to) {
|
|
82
|
+
const phase = event.data.to;
|
|
83
|
+
if (!seen.has(phase)) {
|
|
84
|
+
seen.add(phase);
|
|
85
|
+
phases.push(phase);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return phases;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Derive duration in ms from first to last event timestamp. */
|
|
93
|
+
function deriveDuration(events) {
|
|
94
|
+
if (events.length < 2) return 0;
|
|
95
|
+
const timestamps = events.map((e) => new Date(e.timestamp).getTime()).filter((t) => !isNaN(t));
|
|
96
|
+
if (timestamps.length < 2) return 0;
|
|
97
|
+
const min = Math.min(...timestamps);
|
|
98
|
+
const max = Math.max(...timestamps);
|
|
99
|
+
return max - min;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function main() {
|
|
103
|
+
let raw = '';
|
|
104
|
+
try {
|
|
105
|
+
raw = readFileSync(0, 'utf-8');
|
|
106
|
+
} catch {
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!raw.trim()) {
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let input;
|
|
115
|
+
try {
|
|
116
|
+
input = JSON.parse(raw);
|
|
117
|
+
} catch {
|
|
118
|
+
process.stderr.write('[adoption-tracker] Could not parse stdin — skipping\n');
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const cwd = process.cwd();
|
|
124
|
+
|
|
125
|
+
// Check config
|
|
126
|
+
if (!isAdoptionEnabled(cwd)) {
|
|
127
|
+
process.stderr.write('[adoption-tracker] Adoption tracking disabled — skipping\n');
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Read events.jsonl
|
|
132
|
+
const eventsPath = join(cwd, '.harness', 'events.jsonl');
|
|
133
|
+
if (!existsSync(eventsPath)) {
|
|
134
|
+
process.stderr.write('[adoption-tracker] No events.jsonl found — skipping\n');
|
|
135
|
+
process.exit(0);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const allEvents = parseEventsFile(eventsPath);
|
|
139
|
+
// Filter to relevant event types
|
|
140
|
+
const relevantEvents = allEvents.filter((e) => RELEVANT_TYPES.has(e.type));
|
|
141
|
+
if (relevantEvents.length === 0) {
|
|
142
|
+
process.stderr.write('[adoption-tracker] No relevant skill events — skipping\n');
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Group events by skill
|
|
147
|
+
const skillGroups = new Map();
|
|
148
|
+
for (const event of relevantEvents) {
|
|
149
|
+
if (!skillGroups.has(event.skill)) {
|
|
150
|
+
skillGroups.set(event.skill, []);
|
|
151
|
+
}
|
|
152
|
+
skillGroups.get(event.skill).push(event);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Reconstruct invocation records
|
|
156
|
+
const sessionId = input.session_id ?? 'unknown';
|
|
157
|
+
const metricsDir = join(cwd, '.harness', 'metrics');
|
|
158
|
+
mkdirSync(metricsDir, { recursive: true });
|
|
159
|
+
const adoptionFile = join(metricsDir, 'adoption.jsonl');
|
|
160
|
+
|
|
161
|
+
let written = 0;
|
|
162
|
+
for (const [skill, events] of skillGroups) {
|
|
163
|
+
// Use all events for this skill (including non-relevant) for timing
|
|
164
|
+
const allSkillEvents = allEvents.filter((e) => e.skill === skill);
|
|
165
|
+
|
|
166
|
+
const record = {
|
|
167
|
+
skill,
|
|
168
|
+
session: sessionId,
|
|
169
|
+
startedAt: allSkillEvents[0]?.timestamp ?? events[0].timestamp,
|
|
170
|
+
duration: deriveDuration(allSkillEvents.length > 0 ? allSkillEvents : events),
|
|
171
|
+
outcome: deriveOutcome(events),
|
|
172
|
+
phasesReached: derivePhasesReached(events),
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
appendFileSync(adoptionFile, JSON.stringify(record) + '\n');
|
|
176
|
+
written++;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
process.stderr.write(
|
|
180
|
+
`[adoption-tracker] Wrote ${written} adoption record(s) for session ${sessionId}\n`
|
|
181
|
+
);
|
|
182
|
+
process.exit(0);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
process.stderr.write(`[adoption-tracker] Failed: ${err.message}\n`);
|
|
185
|
+
process.exit(0);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
main();
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// block-no-verify.js — PreToolUse:Bash hook
|
|
3
|
+
// Blocks git commands that use --no-verify to skip hooks.
|
|
4
|
+
// Exit codes: 0 = allow, 2 = block
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import process from 'node:process';
|
|
8
|
+
|
|
9
|
+
function main() {
|
|
10
|
+
let raw = '';
|
|
11
|
+
try {
|
|
12
|
+
raw = readFileSync(0, 'utf-8');
|
|
13
|
+
} catch {
|
|
14
|
+
// No stdin or read error — fail open
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!raw.trim()) {
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let input;
|
|
23
|
+
try {
|
|
24
|
+
input = JSON.parse(raw);
|
|
25
|
+
} catch {
|
|
26
|
+
// Malformed JSON — fail open
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const command = input?.tool_input?.command ?? '';
|
|
32
|
+
if (typeof command !== 'string') {
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (/--no-verify/.test(command) || /\bgit\b.*\bcommit\b.*\s-n\b/.test(command)) {
|
|
37
|
+
process.stderr.write(
|
|
38
|
+
'BLOCKED: --no-verify flag detected. Hooks must not be bypassed.\n'
|
|
39
|
+
);
|
|
40
|
+
process.exit(2);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
process.exit(0);
|
|
44
|
+
} catch {
|
|
45
|
+
// Unexpected error — fail open
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
main();
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cost-tracker.js — Stop:* hook
|
|
3
|
+
// Appends token usage to .harness/metrics/costs.jsonl.
|
|
4
|
+
// Exit codes: 0 = allow (always, log-only hook)
|
|
5
|
+
|
|
6
|
+
import { readFileSync, mkdirSync, appendFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import process from 'node:process';
|
|
9
|
+
|
|
10
|
+
function main() {
|
|
11
|
+
let raw = '';
|
|
12
|
+
try {
|
|
13
|
+
raw = readFileSync(0, 'utf-8');
|
|
14
|
+
} catch {
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!raw.trim()) {
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let input;
|
|
23
|
+
try {
|
|
24
|
+
input = JSON.parse(raw);
|
|
25
|
+
} catch {
|
|
26
|
+
process.stderr.write('[cost-tracker] Could not parse stdin — skipping\n');
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const cwd = process.cwd();
|
|
32
|
+
const metricsDir = join(cwd, '.harness', 'metrics');
|
|
33
|
+
|
|
34
|
+
mkdirSync(metricsDir, { recursive: true });
|
|
35
|
+
|
|
36
|
+
const entry = {
|
|
37
|
+
timestamp: new Date().toISOString(),
|
|
38
|
+
session_id: input.session_id ?? null,
|
|
39
|
+
token_usage: input.token_usage ?? null,
|
|
40
|
+
model: input.model ?? null,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Pass through cache token fields (prefer camelCase input, fall back to snake_case)
|
|
44
|
+
if (input.cacheCreationTokens != null) {
|
|
45
|
+
entry.cacheCreationTokens = input.cacheCreationTokens;
|
|
46
|
+
} else if (input.cache_creation_tokens != null) {
|
|
47
|
+
entry.cacheCreationTokens = input.cache_creation_tokens;
|
|
48
|
+
}
|
|
49
|
+
if (input.cacheReadTokens != null) {
|
|
50
|
+
entry.cacheReadTokens = input.cacheReadTokens;
|
|
51
|
+
} else if (input.cache_read_tokens != null) {
|
|
52
|
+
entry.cacheReadTokens = input.cache_read_tokens;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const costsFile = join(metricsDir, 'costs.jsonl');
|
|
56
|
+
appendFileSync(costsFile, JSON.stringify(entry) + '\n');
|
|
57
|
+
|
|
58
|
+
process.stderr.write(`[cost-tracker] Logged cost entry for session ${entry.session_id}\n`);
|
|
59
|
+
process.exit(0);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
process.stderr.write(`[cost-tracker] Failed to log costs: ${err.message}\n`);
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
main();
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// pre-compact-state.js — PreCompact:* hook
|
|
3
|
+
// Saves a compact session summary before context compaction.
|
|
4
|
+
// Reads from .harness/state.json and .harness/sessions/ to gather context.
|
|
5
|
+
// Writes to .harness/state/pre-compact-summary.json (overwrites on each run).
|
|
6
|
+
// Fail-open: parse errors and unexpected exceptions log to stderr and exit 0.
|
|
7
|
+
// Exit codes: 0 = allow (always, log-only hook)
|
|
8
|
+
|
|
9
|
+
import { readFileSync, mkdirSync, writeFileSync, readdirSync, statSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import process from 'node:process';
|
|
12
|
+
|
|
13
|
+
function readJsonSafe(filePath) {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function findActiveSession(sessionsDir) {
|
|
22
|
+
try {
|
|
23
|
+
const entries = readdirSync(sessionsDir, { withFileTypes: true });
|
|
24
|
+
// Look for the most recently modified session with an autopilot-state.json
|
|
25
|
+
let latest = null;
|
|
26
|
+
let latestMtime = 0;
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (!entry.isDirectory()) continue;
|
|
29
|
+
const statePath = join(sessionsDir, entry.name, 'autopilot-state.json');
|
|
30
|
+
try {
|
|
31
|
+
const stat = statSync(statePath);
|
|
32
|
+
if (stat.mtimeMs > latestMtime) {
|
|
33
|
+
latestMtime = stat.mtimeMs;
|
|
34
|
+
latest = { dir: entry.name, state: readJsonSafe(statePath) };
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// No autopilot-state.json in this session
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return latest;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function main() {
|
|
47
|
+
let raw = '';
|
|
48
|
+
try {
|
|
49
|
+
raw = readFileSync(0, 'utf-8');
|
|
50
|
+
} catch {
|
|
51
|
+
process.stderr.write('[pre-compact-state] Could not read stdin — allowing (fail-open)\n');
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!raw.trim()) {
|
|
56
|
+
process.stderr.write('[pre-compact-state] Empty stdin — allowing (fail-open)\n');
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
JSON.parse(raw); // validate stdin is JSON
|
|
62
|
+
} catch {
|
|
63
|
+
process.stderr.write('[pre-compact-state] Could not parse stdin — allowing (fail-open)\n');
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const cwd = process.cwd();
|
|
69
|
+
const harnessDir = join(cwd, '.harness');
|
|
70
|
+
const stateDir = join(harnessDir, 'state');
|
|
71
|
+
|
|
72
|
+
// Read harness state
|
|
73
|
+
const state = readJsonSafe(join(harnessDir, 'state.json'));
|
|
74
|
+
|
|
75
|
+
// Find active session
|
|
76
|
+
const session = findActiveSession(join(harnessDir, 'sessions'));
|
|
77
|
+
|
|
78
|
+
// Extract recent decisions (last 5)
|
|
79
|
+
const decisions = state?.decisions ?? [];
|
|
80
|
+
const recentDecisions = decisions.slice(-5).map((d) =>
|
|
81
|
+
typeof d === 'string' ? d : (d?.decision ?? d?.summary ?? JSON.stringify(d))
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Extract open questions / blockers
|
|
85
|
+
const openQuestions = state?.blockers ?? [];
|
|
86
|
+
|
|
87
|
+
// Determine current phase from session state
|
|
88
|
+
const currentPhase = session?.state?.currentState
|
|
89
|
+
?? (state?.position?.phase ?? null);
|
|
90
|
+
|
|
91
|
+
// Build summary
|
|
92
|
+
const summary = {
|
|
93
|
+
timestamp: new Date().toISOString(),
|
|
94
|
+
sessionId: session?.dir ?? null,
|
|
95
|
+
activeStream: session?.state?.currentState ?? null,
|
|
96
|
+
recentDecisions,
|
|
97
|
+
openQuestions,
|
|
98
|
+
currentPhase,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
mkdirSync(stateDir, { recursive: true });
|
|
102
|
+
writeFileSync(
|
|
103
|
+
join(stateDir, 'pre-compact-summary.json'),
|
|
104
|
+
JSON.stringify(summary, null, 2) + '\n'
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
process.stderr.write('[pre-compact-state] Saved pre-compact summary\n');
|
|
108
|
+
process.exit(0);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
process.stderr.write(`[pre-compact-state] Failed to save summary: ${String(err?.message ?? err)}\n`);
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
main();
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook profile definitions.
|
|
3
|
+
*
|
|
4
|
+
* Profiles are additive: each higher tier includes all hooks from lower tiers.
|
|
5
|
+
* - minimal: safety floor (block-no-verify only)
|
|
6
|
+
* - standard: + protect-config, quality-gate, pre-compact-state (default)
|
|
7
|
+
* - strict: + cost-tracker, sentinel-pre, sentinel-post
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type HookProfile = 'minimal' | 'standard' | 'strict';
|
|
11
|
+
|
|
12
|
+
export interface HookScript {
|
|
13
|
+
/** Script filename without .js extension */
|
|
14
|
+
name: string;
|
|
15
|
+
/** Claude Code hook event */
|
|
16
|
+
event: 'PreToolUse' | 'PostToolUse' | 'PreCompact' | 'Stop';
|
|
17
|
+
/** Tool matcher pattern */
|
|
18
|
+
matcher: string;
|
|
19
|
+
/** Minimum profile tier that includes this hook */
|
|
20
|
+
minProfile: HookProfile;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const HOOK_SCRIPTS: HookScript[] = [
|
|
24
|
+
{ name: 'block-no-verify', event: 'PreToolUse', matcher: 'Bash', minProfile: 'minimal' },
|
|
25
|
+
{ name: 'protect-config', event: 'PreToolUse', matcher: 'Write|Edit', minProfile: 'standard' },
|
|
26
|
+
{ name: 'quality-gate', event: 'PostToolUse', matcher: 'Edit|Write', minProfile: 'standard' },
|
|
27
|
+
{ name: 'pre-compact-state', event: 'PreCompact', matcher: '*', minProfile: 'standard' },
|
|
28
|
+
{ name: 'adoption-tracker', event: 'Stop', matcher: '*', minProfile: 'standard' },
|
|
29
|
+
{ name: 'telemetry-reporter', event: 'Stop', matcher: '*', minProfile: 'standard' },
|
|
30
|
+
{ name: 'cost-tracker', event: 'Stop', matcher: '*', minProfile: 'strict' },
|
|
31
|
+
{ name: 'sentinel-pre', event: 'PreToolUse', matcher: '*', minProfile: 'strict' },
|
|
32
|
+
{ name: 'sentinel-post', event: 'PostToolUse', matcher: '*', minProfile: 'strict' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const PROFILE_ORDER: HookProfile[] = ['minimal', 'standard', 'strict'];
|
|
36
|
+
|
|
37
|
+
function hooksForProfile(profile: HookProfile): string[] {
|
|
38
|
+
const profileIndex = PROFILE_ORDER.indexOf(profile);
|
|
39
|
+
return HOOK_SCRIPTS.filter((h) => PROFILE_ORDER.indexOf(h.minProfile) <= profileIndex).map(
|
|
40
|
+
(h) => h.name
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const PROFILES: Record<HookProfile, string[]> = {
|
|
45
|
+
minimal: hooksForProfile('minimal'),
|
|
46
|
+
standard: hooksForProfile('standard'),
|
|
47
|
+
strict: hooksForProfile('strict'),
|
|
48
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// protect-config.js — PreToolUse:Write/Edit hook
|
|
3
|
+
// Blocks modifications to linter/formatter config files.
|
|
4
|
+
// Fail-open: parse errors and unexpected exceptions log to stderr and exit 0.
|
|
5
|
+
// Exit codes: 0 = allow, 2 = block
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { basename } from 'node:path';
|
|
9
|
+
import process from 'node:process';
|
|
10
|
+
|
|
11
|
+
// Protected config file patterns
|
|
12
|
+
const PROTECTED_PATTERNS = [
|
|
13
|
+
/^\.eslintrc/,
|
|
14
|
+
/^eslint\.config\./,
|
|
15
|
+
/^\.prettierrc/,
|
|
16
|
+
/^prettier\.config\./,
|
|
17
|
+
/^biome\.json$/,
|
|
18
|
+
/^biome\.jsonc$/,
|
|
19
|
+
/^\.ruff\.toml$/,
|
|
20
|
+
/^ruff\.toml$/,
|
|
21
|
+
/^\.stylelintrc/,
|
|
22
|
+
/^\.markdownlint/,
|
|
23
|
+
/^deno\.json$/,
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function isProtected(filePath) {
|
|
27
|
+
const base = basename(filePath);
|
|
28
|
+
return PROTECTED_PATTERNS.some((pattern) => pattern.test(base));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function main() {
|
|
32
|
+
let raw;
|
|
33
|
+
try {
|
|
34
|
+
raw = readFileSync(0, 'utf-8');
|
|
35
|
+
} catch {
|
|
36
|
+
process.stderr.write('[protect-config] Could not read stdin — allowing (fail-open)\n');
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!raw.trim()) {
|
|
41
|
+
process.stderr.write('[protect-config] Empty stdin — allowing (fail-open)\n');
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let input;
|
|
46
|
+
try {
|
|
47
|
+
input = JSON.parse(raw);
|
|
48
|
+
} catch {
|
|
49
|
+
process.stderr.write('[protect-config] Could not parse stdin JSON — allowing (fail-open)\n');
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const filePath = input?.tool_input?.file_path;
|
|
55
|
+
|
|
56
|
+
if (typeof filePath !== 'string' || !filePath) {
|
|
57
|
+
process.stderr.write('[protect-config] Missing file_path in tool input — allowing (fail-open)\n');
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (isProtected(filePath)) {
|
|
62
|
+
process.stderr.write(
|
|
63
|
+
`BLOCKED: Modification to protected config file: ${basename(filePath)}. Linter/formatter configs must not be weakened.\n`
|
|
64
|
+
);
|
|
65
|
+
process.exit(2);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
process.exit(0);
|
|
69
|
+
} catch {
|
|
70
|
+
process.stderr.write('[protect-config] Unexpected error — allowing (fail-open)\n');
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
main();
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// quality-gate.js — PostToolUse:Edit/Write hook
|
|
3
|
+
// Runs project formatter/linter after edits and warns on violations.
|
|
4
|
+
// Never blocks (always exits 0). Warnings go to stderr.
|
|
5
|
+
// Exit codes: 0 = allow (always)
|
|
6
|
+
|
|
7
|
+
import { readFileSync, accessSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
10
|
+
import process from 'node:process';
|
|
11
|
+
|
|
12
|
+
// Detection order: first match wins
|
|
13
|
+
const DETECTORS = [
|
|
14
|
+
{
|
|
15
|
+
configs: ['biome.json', 'biome.jsonc'],
|
|
16
|
+
cmd: 'npx',
|
|
17
|
+
args: ['biome', 'check'],
|
|
18
|
+
name: 'Biome',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
configs: [
|
|
22
|
+
'.prettierrc',
|
|
23
|
+
'.prettierrc.json',
|
|
24
|
+
'.prettierrc.yml',
|
|
25
|
+
'.prettierrc.yaml',
|
|
26
|
+
'.prettierrc.js',
|
|
27
|
+
'.prettierrc.cjs',
|
|
28
|
+
'.prettierrc.mjs',
|
|
29
|
+
'prettier.config.js',
|
|
30
|
+
'prettier.config.cjs',
|
|
31
|
+
'prettier.config.mjs',
|
|
32
|
+
],
|
|
33
|
+
cmd: 'npx',
|
|
34
|
+
args: ['prettier', '--check'],
|
|
35
|
+
name: 'Prettier',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
configs: ['.ruff.toml', 'ruff.toml'],
|
|
39
|
+
cmd: 'ruff',
|
|
40
|
+
args: ['check'],
|
|
41
|
+
name: 'Ruff',
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
function detectFormatter(cwd) {
|
|
46
|
+
for (const detector of DETECTORS) {
|
|
47
|
+
for (const config of detector.configs) {
|
|
48
|
+
try {
|
|
49
|
+
accessSync(join(cwd, config));
|
|
50
|
+
return detector;
|
|
51
|
+
} catch {
|
|
52
|
+
// Config not found, try next
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function main() {
|
|
60
|
+
let raw = '';
|
|
61
|
+
try {
|
|
62
|
+
raw = readFileSync(0, 'utf-8');
|
|
63
|
+
} catch {
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!raw.trim()) {
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let input;
|
|
72
|
+
try {
|
|
73
|
+
input = JSON.parse(raw);
|
|
74
|
+
} catch {
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const filePath = input?.tool_input?.file_path ?? '';
|
|
80
|
+
const cwd = process.cwd();
|
|
81
|
+
|
|
82
|
+
// Special case: .go files use gofmt
|
|
83
|
+
if (typeof filePath === 'string' && filePath.endsWith('.go')) {
|
|
84
|
+
try {
|
|
85
|
+
const result = execFileSync('gofmt', ['-l', filePath], {
|
|
86
|
+
encoding: 'utf-8',
|
|
87
|
+
cwd,
|
|
88
|
+
timeout: 10000,
|
|
89
|
+
});
|
|
90
|
+
if (result.trim()) {
|
|
91
|
+
process.stderr.write(
|
|
92
|
+
`[quality-gate] gofmt found formatting issues in: ${result.trim()}\n`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// gofmt not available or failed — warn and continue
|
|
97
|
+
process.stderr.write('[quality-gate] gofmt check failed (tool may not be installed)\n');
|
|
98
|
+
}
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const detector = detectFormatter(cwd);
|
|
103
|
+
if (!detector) {
|
|
104
|
+
// No formatter detected — nothing to check
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
execFileSync(detector.cmd, detector.args, {
|
|
110
|
+
encoding: 'utf-8',
|
|
111
|
+
cwd,
|
|
112
|
+
timeout: 30000,
|
|
113
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
114
|
+
});
|
|
115
|
+
process.stderr.write(`[quality-gate] ${detector.name} check passed\n`);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
// Formatter found violations or failed to run — warn only
|
|
118
|
+
const output = err.stdout || err.stderr || '';
|
|
119
|
+
process.stderr.write(
|
|
120
|
+
`[quality-gate] ${detector.name} check reported issues:\n${output.slice(0, 500)}\n`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
process.exit(0);
|
|
125
|
+
} catch {
|
|
126
|
+
// Unexpected error — fail open
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
main();
|