chore(cleanup): R-b R1 — Voyage's own stats, events and OTel export removed
Removes Voyage's measurement, statistics and export apparatus (decision 189; condition 175 met in step 1) and the R-a leftovers: lib/stats/, lib/exporters/, the otel-export and post-bash-stats hooks, yardstick, end-state and comparison gates, synthesis-measure, gold-scorer, docs/observability.md, examples/observability/, docs/T1-cc26-delegated-orchestration.md and their tests and fixtures. The six command stats phases and the /trekexecute main-merge event blocks go; intent-approval --stamp no longer emits brief-approved. README and docs point to Claude Code's own OpenTelemetry. Files removed: 52. Files edited: 37. npm test: 2930 tests (2928 pass / 0 fail / 2 skipped) before, 2600 tests (2598 pass / 0 fail / 2 skipped) after. removed-surfaces group R1 green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
92010d20de
commit
21480e8627
89 changed files with 94 additions and 10290 deletions
|
|
@ -54,16 +54,6 @@
|
|||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node",
|
||||
"args": ["${CLAUDE_PLUGIN_ROOT}/hooks/scripts/post-bash-stats.mjs"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Bash|ExitPlanMode",
|
||||
"hooks": [
|
||||
|
|
@ -108,15 +98,6 @@
|
|||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node",
|
||||
"args": ["${CLAUDE_PLUGIN_ROOT}/hooks/scripts/otel-export.mjs"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,188 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
// otel-export.mjs — Stop-event hook (CC v2.1.105+)
|
||||
//
|
||||
// Reads ${CLAUDE_PLUGIN_DATA}/trek*-stats.jsonl, applies field-allowlist,
|
||||
// and exports either Prometheus textfile (default off) or OTLP/HTTP.
|
||||
//
|
||||
// Strict opt-in via VOYAGE_EXPORT_MODE env-var:
|
||||
// off (default) — silent exit, no work done
|
||||
// textfile — write voyage.prom to VOYAGE_TEXTFILE_DIR or CLAUDE_PLUGIN_DATA
|
||||
// otlp — POST OTLP/JSON to VOYAGE_OTEL_ENDPOINT (https required for non-private)
|
||||
//
|
||||
// Hard invariants:
|
||||
// - Outer try/catch with process.exit(0). Stats failures MUST NOT block Stop.
|
||||
// - Tail-latency NFR: textfile <5ms p99, otlp <1500ms (AbortController).
|
||||
// - Allowlist redaction MUST happen before any export (CWE-212).
|
||||
// - Path / endpoint validation MUST happen before any I/O (CWE-22, CWE-918).
|
||||
// - All stderr prefixed with [voyage].
|
||||
// - EXDEV mitigation: tmp file in same dir as target (do NOT use atomicWriteJson).
|
||||
|
||||
import { stdin } from 'node:process';
|
||||
import { readFileSync, existsSync, writeFileSync, renameSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { transformToPrometheus } from '../../lib/exporters/textfile-format.mjs';
|
||||
import { transformToOtlpJson } from '../../lib/exporters/otlp-format.mjs';
|
||||
import { validateTextfilePath } from '../../lib/exporters/path-validator.mjs';
|
||||
import { validateOtlpEndpoint } from '../../lib/exporters/endpoint-validator.mjs';
|
||||
import { applyFieldAllowlist } from '../../lib/exporters/field-allowlist.mjs';
|
||||
import { captureTokenUsage } from '../../lib/stats/token-usage.mjs';
|
||||
|
||||
async function readStdin() {
|
||||
let data = '';
|
||||
for await (const chunk of stdin) data += chunk;
|
||||
return data;
|
||||
}
|
||||
|
||||
const VALID_MODES = new Set(['off', 'textfile', 'otlp']);
|
||||
const TEXTFILE_NAME = 'voyage.prom';
|
||||
const TEXTFILE_TMP_NAME = '.voyage-prom.tmp';
|
||||
const OTLP_TIMEOUT_MS = 1500;
|
||||
|
||||
// Map JSONL filename → schema_id for allowlist + exporter grouping
|
||||
const STATS_FILES = [
|
||||
{ file: 'trekbrief-stats.jsonl', schema: 'trekbrief' },
|
||||
{ file: 'trekresearch-stats.jsonl', schema: 'trekresearch' },
|
||||
{ file: 'trekplan-stats.jsonl', schema: 'trekplan' },
|
||||
{ file: 'trekexecute-stats.jsonl', schema: 'trekexecute' },
|
||||
{ file: 'trekreview-stats.jsonl', schema: 'trekreview' },
|
||||
{ file: 'trekcontinue-stats.jsonl', schema: 'trekcontinue' },
|
||||
{ file: 'token-usage-stats.jsonl', schema: 'token-usage' },
|
||||
];
|
||||
|
||||
function loadAndAllowlist(dataDir) {
|
||||
const out = [];
|
||||
for (const { file, schema } of STATS_FILES) {
|
||||
const path = join(dataDir, file);
|
||||
if (!existsSync(path)) continue;
|
||||
let text;
|
||||
try { text = readFileSync(path, 'utf-8'); }
|
||||
catch { continue; }
|
||||
const lines = text.split('\n').filter(l => l.trim());
|
||||
for (const line of lines) {
|
||||
let record;
|
||||
try { record = JSON.parse(line); }
|
||||
catch { continue; }
|
||||
|
||||
let actualSchema = schema;
|
||||
if (schema === 'trekexecute') {
|
||||
if ('event' in record) actualSchema = 'event-emit';
|
||||
else if ('command_excerpt' in record || 'session_id' in record) actualSchema = 'post-bash-stats';
|
||||
}
|
||||
|
||||
out.push(applyFieldAllowlist(record, actualSchema));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function exportTextfile(records, env) {
|
||||
const targetDir = env.VOYAGE_TEXTFILE_DIR || env.CLAUDE_PLUGIN_DATA;
|
||||
if (!targetDir) {
|
||||
process.stderr.write('[voyage] otel-export: textfile mode requires VOYAGE_TEXTFILE_DIR or CLAUDE_PLUGIN_DATA\n');
|
||||
return;
|
||||
}
|
||||
|
||||
const targetPath = join(targetDir, TEXTFILE_NAME);
|
||||
const allowedRoots = [];
|
||||
if (env.VOYAGE_TEXTFILE_DIR) allowedRoots.push(env.VOYAGE_TEXTFILE_DIR);
|
||||
if (env.CLAUDE_PLUGIN_DATA) allowedRoots.push(env.CLAUDE_PLUGIN_DATA);
|
||||
const pathCheck = validateTextfilePath(targetPath, { allowedRoots });
|
||||
if (!pathCheck.valid) {
|
||||
process.stderr.write(`[voyage] otel-export: invalid textfile path: ${pathCheck.errors[0].message}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
const text = transformToPrometheus(records);
|
||||
const tmpPath = join(dirname(targetPath), TEXTFILE_TMP_NAME);
|
||||
try {
|
||||
writeFileSync(tmpPath, text);
|
||||
renameSync(tmpPath, targetPath);
|
||||
} catch (e) {
|
||||
process.stderr.write(`[voyage] otel-export: textfile write failed: ${e.message}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
async function exportOtlp(records, env) {
|
||||
const url = env.VOYAGE_OTEL_ENDPOINT;
|
||||
if (!url) {
|
||||
process.stderr.write('[voyage] otel-export: otlp mode requires VOYAGE_OTEL_ENDPOINT\n');
|
||||
return;
|
||||
}
|
||||
const epCheck = validateOtlpEndpoint(url, { env });
|
||||
if (!epCheck.valid) {
|
||||
process.stderr.write(`[voyage] otel-export: invalid OTLP endpoint: ${epCheck.errors[0].message}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = transformToOtlpJson(records);
|
||||
const body = JSON.stringify(payload);
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), OTLP_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const res = await fetch(epCheck.parsed.url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'User-Agent': 'voyage/4.1.0' },
|
||||
body,
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`[voyage] otel-export: OTLP endpoint returned ${res.status}\n`);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.name === 'AbortError') {
|
||||
process.stderr.write(`[voyage] otel-export: OTLP request timed out after ${OTLP_TIMEOUT_MS}ms\n`);
|
||||
} else {
|
||||
process.stderr.write(`[voyage] otel-export: OTLP send failed: ${e.message}\n`);
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const env = process.env;
|
||||
|
||||
// SKAL-2: opt-in main-context token/cost capture (default off → zero added
|
||||
// latency). Rides this existing Stop hook rather than a new hook script —
|
||||
// avoids a second-Stop-hook race and keeps the README hook-count pin intact.
|
||||
// Runs BEFORE the export-mode gate so capture is independent of export.
|
||||
// Fail-open: any error is swallowed; capture must never block Stop.
|
||||
if (env.VOYAGE_TOKEN_METER) {
|
||||
try {
|
||||
const raw = await readStdin();
|
||||
if (raw.trim()) {
|
||||
const payload = JSON.parse(raw);
|
||||
captureTokenUsage({
|
||||
transcriptPath: payload.transcript_path,
|
||||
sessionId: payload.session_id,
|
||||
dataDir: env.CLAUDE_PLUGIN_DATA,
|
||||
});
|
||||
}
|
||||
} catch { /* fail-open: never block Stop, never throw */ }
|
||||
}
|
||||
|
||||
const mode = (env.VOYAGE_EXPORT_MODE || 'off').toLowerCase();
|
||||
|
||||
if (mode === 'off') return;
|
||||
if (!VALID_MODES.has(mode)) {
|
||||
process.stderr.write(`[voyage] otel-export: unknown VOYAGE_EXPORT_MODE="${mode}", expected one of [${[...VALID_MODES].join(', ')}]\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
const dataDir = env.CLAUDE_PLUGIN_DATA;
|
||||
if (!dataDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
const records = loadAndAllowlist(dataDir);
|
||||
if (records.length === 0) return;
|
||||
|
||||
if (mode === 'textfile') await exportTextfile(records, env);
|
||||
else if (mode === 'otlp') await exportOtlp(records, env);
|
||||
} catch (e) {
|
||||
try { process.stderr.write(`[voyage] otel-export: unexpected error: ${e.message}\n`); } catch {}
|
||||
}
|
||||
process.exit(0);
|
||||
})();
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
// post-bash-stats.mjs — PostToolUse hook (CC v2.1.97+)
|
||||
//
|
||||
// Captures duration_ms from PostToolUse payload for Bash tool calls and
|
||||
// appends a structured stats line to ${CLAUDE_PLUGIN_DATA}/trekexecute-stats.jsonl
|
||||
// when the running session is an trekexecute session.
|
||||
//
|
||||
// Detection: only fires when the tool input matches the verify/checkpoint
|
||||
// pattern of an trekexecute step (i.e., the command was issued from inside
|
||||
// /trekexecute). We err on the side of "log everything in plugin
|
||||
// scope" — duration data is cheap and the alternative is missing real
|
||||
// per-step timings.
|
||||
//
|
||||
// Fail-open invariant: any error → exit 0, no output, no log line.
|
||||
|
||||
import { stdin } from 'node:process';
|
||||
import { appendFileSync, mkdirSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
async function readStdin() {
|
||||
let data = '';
|
||||
for await (const chunk of stdin) data += chunk;
|
||||
return data;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const raw = await readStdin();
|
||||
if (!raw.trim()) return;
|
||||
const payload = JSON.parse(raw);
|
||||
|
||||
if (payload.tool_name !== 'Bash') return;
|
||||
const duration = payload.duration_ms;
|
||||
if (typeof duration !== 'number') return;
|
||||
|
||||
const dataDir = process.env.CLAUDE_PLUGIN_DATA;
|
||||
if (!dataDir) return;
|
||||
|
||||
const cmd = payload.tool_input?.command || '';
|
||||
if (!cmd) return;
|
||||
|
||||
const line = JSON.stringify({
|
||||
ts: new Date().toISOString(),
|
||||
session_id: payload.session_id || null,
|
||||
command_excerpt: cmd.slice(0, 120),
|
||||
duration_ms: duration,
|
||||
success: payload.tool_response?.success !== false,
|
||||
});
|
||||
|
||||
const target = join(dataDir, 'trekexecute-stats.jsonl');
|
||||
try {
|
||||
mkdirSync(dirname(target), { recursive: true });
|
||||
} catch {}
|
||||
appendFileSync(target, line + '\n');
|
||||
} catch {
|
||||
// fail open
|
||||
}
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue