@rulemetric/hooks 0.16.0 → 0.17.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/package.json +1 -1
- package/scripts/_log.sh +1 -1
- package/scripts/scoped-live.mjs +28 -4
package/package.json
CHANGED
package/scripts/_log.sh
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Appends JSON lines to $TMPDIR/rulemetric/hook.log
|
|
4
4
|
# Never fails — all writes guarded with || true
|
|
5
5
|
|
|
6
|
-
if ! command -v jq >/dev/null 2>&1; then
|
|
6
|
+
if ! command -v jq >/dev/null 2>&1 || [ ! -x "$(command -v jq)" ]; then
|
|
7
7
|
_rulemetric_log_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
8
|
# npm normalises tarball file modes to 0644, so the bundled shim arrives
|
|
9
9
|
# NON-EXECUTABLE from a published install — `command -v jq` then never finds
|
package/scripts/scoped-live.mjs
CHANGED
|
@@ -52,8 +52,13 @@ export function observeEnvironment(root) {
|
|
|
52
52
|
for (const file of ['settings.json', 'CLAUDE.md']) context(join(homedir(), '.claude', file));
|
|
53
53
|
const userConfig = join(homedir(), '.claude.json');
|
|
54
54
|
context(userConfig);
|
|
55
|
-
|
|
55
|
+
const config = contextHashes[userConfig] ? JSON.parse(readFileSync(userConfig, 'utf8')) : null;
|
|
56
|
+
if (config) contextHashes[userConfig] = hash(JSON.stringify(stableClaudeConfig(config, resolve(root))));
|
|
57
|
+
const runtimeStateHash = config ? hash(JSON.stringify(Object.fromEntries(
|
|
58
|
+
['cachedDynamicConfigs', 'cachedGrowthBookFeatures', 'cachedStatsigGates', 'cachedExperimentFeatures', 'cachedExperimentData'].map(key => [key, config[key] ?? null]),
|
|
59
|
+
))) : null;
|
|
56
60
|
return {
|
|
61
|
+
runtimeStateHash,
|
|
57
62
|
harnessVersion: execFileSync('claude', ['--version'], { encoding: 'utf8', timeout: 5000 }).trim(),
|
|
58
63
|
repositoryRevision: execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8', timeout: 5000 }).trim(),
|
|
59
64
|
contextHashes: Object.fromEntries(Object.entries(contextHashes).sort(([a], [b]) => a.localeCompare(b))),
|
|
@@ -63,7 +68,7 @@ export function observeEnvironment(root) {
|
|
|
63
68
|
// .claude.json mixes session counters/history with configuration. Retain only
|
|
64
69
|
// configuration here; settings files are separately hashed in full above.
|
|
65
70
|
export function stableClaudeConfig(config, root) {
|
|
66
|
-
const keys = ['mcpServers', 'mcpContextUris', 'allowedTools', 'enabledMcpjsonServers', 'disabledMcpjsonServers', 'disabledMcpServers', 'hasClaudeMdExternalIncludesApproved', 'claudeInChromeDefaultEnabled'
|
|
71
|
+
const keys = ['mcpServers', 'mcpContextUris', 'allowedTools', 'enabledMcpjsonServers', 'disabledMcpjsonServers', 'disabledMcpServers', 'hasClaudeMdExternalIncludesApproved', 'claudeInChromeDefaultEnabled'];
|
|
67
72
|
const pick = object => Object.fromEntries(keys.map(key => [key, object?.[key] ?? null]));
|
|
68
73
|
return { global: pick(config), project: pick(config.projects?.[root]) };
|
|
69
74
|
}
|
|
@@ -110,14 +115,33 @@ export async function runScopedHook(mode, sessionId, root, model, transcriptPath
|
|
|
110
115
|
const response = await fetch(`${api}/api${path}`, { method: body ? 'POST' : 'GET',
|
|
111
116
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
112
117
|
...(body ? { body: JSON.stringify(body) } : {}), signal: AbortSignal.timeout(5000) });
|
|
113
|
-
if (!response.ok)
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
const detail = await response.text();
|
|
120
|
+
throw new Error(`Scoped ${mode} refused (${response.status}): ${detail.slice(0, 1000)}`);
|
|
121
|
+
}
|
|
114
122
|
return response.json();
|
|
115
123
|
};
|
|
116
124
|
const { confirmation } = await request(`/scoped-live-confirmations/${id}/contract`);
|
|
117
125
|
const protocol = confirmation.protocol;
|
|
118
126
|
const environment = protocol.verification ? observeEnvironment(root) : undefined;
|
|
119
127
|
if (mode === 'start') {
|
|
120
|
-
|
|
128
|
+
if (!model && protocol.research?.enrollment === 'prospective-launches') {
|
|
129
|
+
// SessionStart's model is optional. A requested model is not an observed
|
|
130
|
+
// outcome: retain the launch pin and verify actual transcript models later.
|
|
131
|
+
const requested = process.env.RULEMETRIC_SCOPED_REQUESTED_MODEL;
|
|
132
|
+
if (requested && requested === protocol.model) model = requested;
|
|
133
|
+
else {
|
|
134
|
+
// Compatibility for already-queued launches from workers without the pin env.
|
|
135
|
+
const jobs = await request('/launch-jobs?limit=100');
|
|
136
|
+
const launch = jobs.find(job => job.tool === 'claude_code' && ['claimed', 'running'].includes(job.status)
|
|
137
|
+
&& job.config?.projectPath === root && job.config?.scopedConfirmation?.id === id
|
|
138
|
+
&& job.config?.scopedConfirmation?.taskId === taskId && job.config?.model === protocol.model);
|
|
139
|
+
model = launch?.config.model;
|
|
140
|
+
}
|
|
141
|
+
if (!model) throw new Error('SessionStart omitted model and no matching claimed launch model is available');
|
|
142
|
+
process.stderr.write('[rulemetric] Enrollment uses the requested launch model; actual model remains subject to transcript verification.\n');
|
|
143
|
+
}
|
|
144
|
+
const configurationHash = environment ? hash(JSON.stringify({ harnessVersion: environment.harnessVersion, repositoryRevision: protocol.research?.enrollment === 'prospective-launches' ? protocol.verification.environment.repositoryRevision : environment.repositoryRevision, contextHashes: environment.contextHashes })) : process.env.RULEMETRIC_SCOPED_CONFIGURATION_HASH;
|
|
121
145
|
const rendered = await request(`/scoped-live-confirmations/${id}/hook-render`, { taskId, externalSessionId: sessionId, model, projectPath: root, configurationHash, environment, collectorVersion: 2 });
|
|
122
146
|
const content = Buffer.from(rendered.contentBase64, 'base64');
|
|
123
147
|
if (hash(content) !== rendered.deliveryHash) throw new Error('Scoped delivery hash mismatch');
|