harness-everything 0.9.0 → 0.11.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +52 -0
- package/ci/contract-integrity.test.js +203 -0
- package/ci/self-evolve-loop.test.js +311 -0
- package/ci/telemetry.test.js +193 -0
- package/contract-integrity/fixtures/consistent-trace.json +94 -0
- package/contract-integrity/fixtures/reconciled-trace.json +105 -0
- package/contract-integrity/fixtures/ticket-drift-trace.json +94 -0
- package/contract-integrity/schemas/trace.schema.json +77 -0
- package/contract-integrity/scripts/audit.js +536 -0
- package/docs/contract-integrity.md +109 -0
- package/docs/telemetry.md +62 -0
- package/hooks/hooks.json +43 -0
- package/hooks/scripts/contract-test.js +24 -0
- package/hooks/scripts/lib/learning-opportunity.js +148 -0
- package/hooks/scripts/lib/telemetry.js +377 -0
- package/hooks/scripts/rule-of-3-tracker.js +23 -0
- package/hooks/scripts/state-persist.js +5 -0
- package/hooks/scripts/telemetry-adapter.js +25 -0
- package/multi-agent-workspace/SKILL.md +1 -1
- package/multi-agent-workspace/scripts/index_memory.js +2 -0
- package/opencode-plugin/index.mjs +213 -1
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +7 -2
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/hooks.json +35 -0
- package/plugins/harness-everything/hooks/scripts/contract-test.js +24 -0
- package/plugins/harness-everything/hooks/scripts/lib/learning-opportunity.js +148 -0
- package/plugins/harness-everything/hooks/scripts/lib/telemetry.js +377 -0
- package/plugins/harness-everything/hooks/scripts/rule-of-3-tracker.js +23 -0
- package/plugins/harness-everything/hooks/scripts/state-persist.js +5 -0
- package/plugins/harness-everything/hooks/scripts/telemetry-adapter.js +25 -0
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/multi-agent-workspace/SKILL.md +1 -1
- package/plugins/harness-everything/skills/multi-agent-workspace/scripts/index_memory.js +2 -0
- package/plugins/harness-everything/skills/self-evolve/SKILL.md +14 -19
- package/plugins/harness-everything/skills/self-evolve/references/memory-resolution.md +32 -13
- package/plugins/harness-everything/skills/self-evolve/schemas/lesson-candidate.schema.json +101 -0
- package/plugins/harness-everything/skills/self-evolve/scripts/lesson-candidate.js +303 -0
- package/plugins/harness-everything/skills/self-evolve/scripts/self-regression.js +4 -1
- package/self-evolve/SKILL.md +14 -19
- package/self-evolve/references/memory-resolution.md +32 -13
- package/self-evolve/schemas/lesson-candidate.schema.json +101 -0
- package/self-evolve/scripts/lesson-candidate.js +303 -0
- package/self-evolve/scripts/self-regression.js +4 -1
- package/telemetry/schemas/event.schema.json +39 -0
- package/telemetry/scripts/benchmark.js +47 -0
- package/telemetry/scripts/report.js +133 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Harness Telemetry
|
|
2
|
+
|
|
3
|
+
Harness telemetry is a **local operational evidence layer**, not analytics upload and not a memory store. It normalizes host-native lifecycle evidence into JSONL so benchmark/report definitions can evolve without rewriting historical events.
|
|
4
|
+
|
|
5
|
+
## Privacy defaults
|
|
6
|
+
|
|
7
|
+
Events contain skill/runtime metadata only:
|
|
8
|
+
|
|
9
|
+
- skill name/version;
|
|
10
|
+
- host;
|
|
11
|
+
- locally hashed session/turn/agent IDs;
|
|
12
|
+
- random invocation ID;
|
|
13
|
+
- tool name (never tool arguments);
|
|
14
|
+
- status/retry count;
|
|
15
|
+
- separate timing fields;
|
|
16
|
+
- short reason codes.
|
|
17
|
+
|
|
18
|
+
The sink rejects content-bearing fields such as prompt, source/code contents, command/tool arguments, paths, transcript, messages, email/user identity. It never sends data over the network. Set `HARNESS_TELEMETRY=off` to disable local writes.
|
|
19
|
+
|
|
20
|
+
Raw events live under the workspace-keyed Harness state root:
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
~/.agents/harness-everything/workspaces/<workspace-key>/state/telemetry/events.jsonl
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
They are not project files and are not committed automatically.
|
|
27
|
+
|
|
28
|
+
## Timing semantics
|
|
29
|
+
|
|
30
|
+
Do not collapse these into one `duration_ms`:
|
|
31
|
+
|
|
32
|
+
- `skillLoadDurationMs`: host Skill pre-event to correlated Skill post-event. This is loading/framework latency.
|
|
33
|
+
- `activeWindowMs`: successful/observed skill load until the host turn/Stop boundary. This is a context exposure window, **not CPU/runtime**.
|
|
34
|
+
- `attributedToolDurationMs`: tool duration observed while a skill invocation is active. It is observational/non-exclusive: overlapping active skills may each receive the same tool observation.
|
|
35
|
+
|
|
36
|
+
When a host does not expose a reliable duration, the value remains `null`; Harness does not fabricate it.
|
|
37
|
+
|
|
38
|
+
## Host adapters
|
|
39
|
+
|
|
40
|
+
| Host | Adapter | Evidence boundary |
|
|
41
|
+
| --- | --- | --- |
|
|
42
|
+
| Claude Code | Narrow `Skill` Pre/Post hooks + Stop; existing `state-persist` observes host-reported tool duration | Mechanism covered in CI; live certification remains #82 |
|
|
43
|
+
| OpenCode | Existing single-process plugin records skill before/after, measures tool elapsed in-process, closes on `session.idle` | Mechanism covered in CI; retained live support remains bounded by #37/#82 |
|
|
44
|
+
| Codex | OpenAI package includes the same narrow Skill/Stop adapter | **Mechanism only** while plugin hook mounting/attribution is blocked by #122; no live claim |
|
|
45
|
+
|
|
46
|
+
Telemetry failure is always fail-open and cannot block a task.
|
|
47
|
+
|
|
48
|
+
## Reports
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
node telemetry/scripts/report.js --workspace <repo>
|
|
52
|
+
node telemetry/scripts/report.js --file <events.jsonl> --markdown
|
|
53
|
+
node telemetry/scripts/benchmark.js 250
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The report exposes invocation counts, completion statuses, P50/P95 for each timing vocabulary, and invalid-event counts. Raw JSONL remains the source of truth; SQLite/dashboard projections can be added later without changing the event evidence contract.
|
|
57
|
+
|
|
58
|
+
The overhead benchmark measures only local event normalization + append cost. Host adapter/process startup overhead should be measured separately in live host studies before using telemetry data for performance comparisons.
|
|
59
|
+
|
|
60
|
+
## Benchmark use
|
|
61
|
+
|
|
62
|
+
#71 may consume telemetry cost/execution evidence, but correctness pass rates stay separate. Infrastructure/telemetry failures must not become behavioral failures.
|
package/hooks/hooks.json
CHANGED
|
@@ -102,6 +102,17 @@
|
|
|
102
102
|
],
|
|
103
103
|
"description": "Snapshot repo state before a subagent burst starts",
|
|
104
104
|
"id": "harness:pre:subagent-scope-guard"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"matcher": "Skill",
|
|
108
|
+
"hooks": [
|
|
109
|
+
{
|
|
110
|
+
"type": "command",
|
|
111
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/scripts/telemetry-adapter.js\""
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
"description": "Record privacy-safe Harness skill load start telemetry",
|
|
115
|
+
"id": "harness:pre:skill-telemetry"
|
|
105
116
|
}
|
|
106
117
|
],
|
|
107
118
|
"Stop": [
|
|
@@ -134,6 +145,16 @@
|
|
|
134
145
|
],
|
|
135
146
|
"description": "Pre-delivery gate - bounce the end of a turn once per edit batch when edits were never verified",
|
|
136
147
|
"id": "harness:stop:stop-gate"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"hooks": [
|
|
151
|
+
{
|
|
152
|
+
"type": "command",
|
|
153
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/scripts/telemetry-adapter.js\""
|
|
154
|
+
}
|
|
155
|
+
],
|
|
156
|
+
"description": "Close Harness skill active-window telemetry at the turn boundary",
|
|
157
|
+
"id": "harness:stop:skill-telemetry"
|
|
137
158
|
}
|
|
138
159
|
],
|
|
139
160
|
"PostToolUse": [
|
|
@@ -202,6 +223,17 @@
|
|
|
202
223
|
],
|
|
203
224
|
"description": "Diff the whole repo after a subagent burst finishes - catches scope creep past what the task briefed",
|
|
204
225
|
"id": "harness:post:subagent-scope-guard"
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"matcher": "Skill",
|
|
229
|
+
"hooks": [
|
|
230
|
+
{
|
|
231
|
+
"type": "command",
|
|
232
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/scripts/telemetry-adapter.js\""
|
|
233
|
+
}
|
|
234
|
+
],
|
|
235
|
+
"description": "Record privacy-safe Harness skill load completion telemetry",
|
|
236
|
+
"id": "harness:post:skill-telemetry"
|
|
205
237
|
}
|
|
206
238
|
],
|
|
207
239
|
"PostToolUseFailure": [
|
|
@@ -226,6 +258,17 @@
|
|
|
226
258
|
],
|
|
227
259
|
"description": "Track failed tool error signatures for Rule of 3",
|
|
228
260
|
"id": "harness:post-failure:rule-of-3-tracker"
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"matcher": "Skill",
|
|
264
|
+
"hooks": [
|
|
265
|
+
{
|
|
266
|
+
"type": "command",
|
|
267
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/scripts/telemetry-adapter.js\""
|
|
268
|
+
}
|
|
269
|
+
],
|
|
270
|
+
"description": "Record failed Harness skill loading without prompt/tool arguments",
|
|
271
|
+
"id": "harness:post-failure:skill-telemetry"
|
|
229
272
|
}
|
|
230
273
|
]
|
|
231
274
|
}
|
|
@@ -23,6 +23,7 @@ const {
|
|
|
23
23
|
getWorkerId,
|
|
24
24
|
} = require('./lib/fable-contracts');
|
|
25
25
|
const { loadWorkflow, matchingRun, recordBudgetEvent } = require('./lib/workflow-runtime');
|
|
26
|
+
const { createLearningOpportunity } = require('./lib/learning-opportunity');
|
|
26
27
|
|
|
27
28
|
function resultState(payload) {
|
|
28
29
|
const toolResponse = payload.tool_response || payload.tool_result || {};
|
|
@@ -137,7 +138,30 @@ process.stdin.on('end', () => {
|
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
}
|
|
141
|
+
const previousStatus = entry.contract.status;
|
|
142
|
+
const previousVerifiedAt = entry.contract.verifiedAt || entry.contract.updatedAt || null;
|
|
140
143
|
const contract = updateRunContract(entry, payload, command, sessionId);
|
|
144
|
+
if (previousStatus === 'fail' && contract.status === 'pass') {
|
|
145
|
+
try {
|
|
146
|
+
createLearningOpportunity(payload, {
|
|
147
|
+
triggerType: 'verifier-fail-pass',
|
|
148
|
+
sourceEventIds: [
|
|
149
|
+
`verifier-fail:${contract.runId}:${contract.stageId}:${previousVerifiedAt || 'unknown'}`,
|
|
150
|
+
`verifier-pass:${contract.runId}:${contract.stageId}:${contract.verifiedAt}`,
|
|
151
|
+
],
|
|
152
|
+
evidence: {
|
|
153
|
+
planId: contract.planId,
|
|
154
|
+
runId: contract.runId,
|
|
155
|
+
stageId: contract.stageId,
|
|
156
|
+
previousStatus: 'fail',
|
|
157
|
+
currentStatus: 'pass',
|
|
158
|
+
verificationEvidenceRef: contract.verificationEvidence,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
} catch (_) {
|
|
162
|
+
// Learning capture is additive; verification disposition stays authoritative.
|
|
163
|
+
}
|
|
164
|
+
}
|
|
141
165
|
console.log(`[Contract Test] ${contract.planId}/${contract.runId}/${contract.stageId}: ${contract.status.toUpperCase()} ${contract.checkCommand}`);
|
|
142
166
|
if (contract.status === 'fail') {
|
|
143
167
|
if (workflowContext.workflow?.workflowId) {
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const {
|
|
7
|
+
getWorkspaceRoot,
|
|
8
|
+
getSessionId,
|
|
9
|
+
getSessionDir,
|
|
10
|
+
} = require('./harness-state');
|
|
11
|
+
|
|
12
|
+
const SCHEMA_VERSION = 1;
|
|
13
|
+
const TRIGGERS = new Set(['rule-of-3-recovery', 'verifier-fail-pass']);
|
|
14
|
+
|
|
15
|
+
function atomicWriteJson(filePath, value) {
|
|
16
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
17
|
+
const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
18
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
19
|
+
fs.renameSync(temporary, filePath);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readJson(filePath) {
|
|
23
|
+
try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch (_) { return null; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function safeText(value, max = 160) {
|
|
27
|
+
const text = String(value || '').replace(/[\r\n\0]+/g, ' ').trim();
|
|
28
|
+
return text ? text.slice(0, max) : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function safeId(value) {
|
|
32
|
+
const text = safeText(value, 180);
|
|
33
|
+
return text && /^[A-Za-z0-9._:@/-]+$/.test(text) ? text : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hash(value) {
|
|
37
|
+
return crypto.createHash('sha256').update(String(value), 'utf8').digest('hex');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function workflowIdentity(sessionDir) {
|
|
41
|
+
const workflow = readJson(path.join(sessionDir, 'workflow-run.json'));
|
|
42
|
+
return {
|
|
43
|
+
workflowId: safeId(workflow && workflow.workflowId),
|
|
44
|
+
runId: safeId(workflow && workflow.runId),
|
|
45
|
+
strategy: safeText(workflow && workflow.strategy, 80),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function candidateDirectory(sessionDir) {
|
|
50
|
+
return path.join(sessionDir, 'learning-candidates');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sourceIds(values) {
|
|
54
|
+
return [...new Set((values || []).map(value => safeId(value)).filter(Boolean))].slice(0, 16);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function minimalEvidence(triggerType, evidence = {}) {
|
|
58
|
+
if (triggerType === 'rule-of-3-recovery') {
|
|
59
|
+
return {
|
|
60
|
+
failureSignature: safeId(evidence.failureSignature),
|
|
61
|
+
failureCategory: safeText(evidence.failureCategory, 40),
|
|
62
|
+
zoomOutCycles: Number.isInteger(evidence.zoomOutCycles) ? evidence.zoomOutCycles : 1,
|
|
63
|
+
recovered: evidence.recovered === true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (triggerType === 'verifier-fail-pass') {
|
|
67
|
+
return {
|
|
68
|
+
planId: safeId(evidence.planId),
|
|
69
|
+
runId: safeId(evidence.runId),
|
|
70
|
+
stageId: safeId(evidence.stageId),
|
|
71
|
+
previousStatus: evidence.previousStatus === 'fail' ? 'fail' : null,
|
|
72
|
+
currentStatus: evidence.currentStatus === 'pass' ? 'pass' : null,
|
|
73
|
+
verificationEvidenceRef: safeText(evidence.verificationEvidenceRef, 240),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function createLearningOpportunity(payload, input = {}) {
|
|
80
|
+
const triggerType = String(input.triggerType || '');
|
|
81
|
+
if (!TRIGGERS.has(triggerType)) throw new Error(`unsupported learning trigger: ${triggerType}`);
|
|
82
|
+
|
|
83
|
+
const root = getWorkspaceRoot(payload);
|
|
84
|
+
const sessionId = getSessionId(payload);
|
|
85
|
+
const sessionDir = getSessionDir(root, sessionId, payload);
|
|
86
|
+
const identity = workflowIdentity(sessionDir);
|
|
87
|
+
const ids = sourceIds(input.sourceEventIds);
|
|
88
|
+
if (ids.length === 0) throw new Error('learning opportunity requires at least one source event id');
|
|
89
|
+
|
|
90
|
+
const evidence = minimalEvidence(triggerType, input.evidence);
|
|
91
|
+
const fingerprint = hash(JSON.stringify({
|
|
92
|
+
sessionId,
|
|
93
|
+
triggerType,
|
|
94
|
+
sourceEventIds: ids,
|
|
95
|
+
evidence,
|
|
96
|
+
}));
|
|
97
|
+
const candidateId = `lesson-${fingerprint.slice(0, 24)}`;
|
|
98
|
+
const file = path.join(candidateDirectory(sessionDir), `${candidateId}.json`);
|
|
99
|
+
const existing = readJson(file);
|
|
100
|
+
if (existing) return { created: false, file, candidate: existing };
|
|
101
|
+
|
|
102
|
+
const observedAt = new Date().toISOString();
|
|
103
|
+
const candidate = {
|
|
104
|
+
schemaVersion: SCHEMA_VERSION,
|
|
105
|
+
candidateId,
|
|
106
|
+
contentHash: null,
|
|
107
|
+
state: 'proposed',
|
|
108
|
+
trigger: {
|
|
109
|
+
type: triggerType,
|
|
110
|
+
reasonCodes: triggerType === 'rule-of-3-recovery'
|
|
111
|
+
? ['repeated-failure', 'zoom-out-completed', 'verified-subsequent-success']
|
|
112
|
+
: ['verifier-failed', 'later-verifier-pass'],
|
|
113
|
+
sourceEventIds: ids,
|
|
114
|
+
evidenceClass: triggerType === 'verifier-fail-pass' ? 'objective-transition' : 'runtime-recovery',
|
|
115
|
+
},
|
|
116
|
+
provenance: {
|
|
117
|
+
sessionId,
|
|
118
|
+
workflowId: identity.workflowId,
|
|
119
|
+
runId: identity.runId,
|
|
120
|
+
strategy: identity.strategy,
|
|
121
|
+
},
|
|
122
|
+
evidence,
|
|
123
|
+
generalizedRule: null,
|
|
124
|
+
scope: { task: null, requirement: null, role: null },
|
|
125
|
+
screening: null,
|
|
126
|
+
evaluation: null,
|
|
127
|
+
persistence: null,
|
|
128
|
+
outcome: null,
|
|
129
|
+
history: [
|
|
130
|
+
{ state: 'observed', observedAt, reasonCode: 'runtime-learning-opportunity' },
|
|
131
|
+
{ state: 'proposed', observedAt, reasonCode: 'objective-recovery-trigger' },
|
|
132
|
+
],
|
|
133
|
+
createdAt: observedAt,
|
|
134
|
+
updatedAt: observedAt,
|
|
135
|
+
};
|
|
136
|
+
atomicWriteJson(file, candidate);
|
|
137
|
+
return { created: true, file, candidate };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
SCHEMA_VERSION,
|
|
142
|
+
TRIGGERS,
|
|
143
|
+
atomicWriteJson,
|
|
144
|
+
candidateDirectory,
|
|
145
|
+
createLearningOpportunity,
|
|
146
|
+
hash,
|
|
147
|
+
readJson,
|
|
148
|
+
};
|