ruvnet-brain 4.5.9 → 4.5.11
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/README.md +1 -1
- package/bin/install.mjs +48 -93
- package/bin/nightly-refresh.mjs +20 -34
- package/kb/download-retry.mjs +97 -0
- package/kb/forge-update.mjs +12 -10
- package/kb/verify-citation.mjs +10 -13
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/automatic-update.mjs +125 -0
- package/plugin/scripts/completion-claim-evidence.mjs +9 -4
- package/plugin/scripts/continuation-gate.mjs +34 -10
- package/plugin/scripts/continuation-objective.mjs +45 -10
- package/plugin/scripts/continuity-journal.mjs +11 -11
- package/plugin/scripts/ground-ruvnet.sh +3 -0
- package/plugin/scripts/grounding-code-projection.mjs +6 -1
- package/plugin/scripts/grounding-turn-evidence.mjs +2 -2
- package/plugin/scripts/grounding-turn-gate.mjs +8 -14
- package/plugin/scripts/grounding-turn-mark.mjs +106 -20
- package/plugin/scripts/hijack-ruvnet.sh +4 -4
- package/plugin/scripts/hook-input.mjs +1 -1
- package/plugin/scripts/host-update.mjs +16 -15
- package/plugin/scripts/lesson-presentation.mjs +2 -1
- package/plugin/scripts/nightly-scheduler.mjs +34 -7
- package/plugin/scripts/project-progression-hook.mjs +22 -10
- package/plugin/scripts/project-progression-producer.mjs +11 -1
- package/plugin/scripts/project-progression-store.mjs +3 -0
- package/plugin/scripts/project-transition-hook.mjs +7 -1
- package/plugin/scripts/ruvnet-gate1-pattern.mjs +33 -0
- package/plugin/scripts/session-snapshot-hook.mjs +4 -3
- package/plugin/scripts/session-start-core.mjs +3 -1
- package/plugin/scripts/turn-capture-privacy.mjs +202 -0
- package/plugin/scripts/turn-outcome-capture.mjs +28 -13
- package/plugin/scripts/user-settings.mjs +23 -1
- package/scripts/brain-grade-groundtruth.mjs +1 -0
- package/scripts/brain-score.mjs +11 -11
- package/scripts/claude-controlled-terminal.mjs +265 -0
- package/scripts/issue-watch.mjs +21 -10
- package/scripts/model-terminal-launchers.mjs +19 -9
- package/scripts/model-weekly-cycle.mjs +45 -9
- package/scripts/record-lesson.mjs +13 -7
- package/scripts/release-qualification-contract.mjs +35 -4
- package/scripts/user-model-hook-install.mjs +145 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// A controlled prompt boundary using native print/SDK controls, not the native terminal UI.
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
8
|
+
import { createInterface } from 'node:readline/promises';
|
|
9
|
+
import { pathToFileURL } from 'node:url';
|
|
10
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
11
|
+
import { decideNativeTurn, appendGatewayReceipt } from './model-routing-gateway.mjs';
|
|
12
|
+
import { subscriptionEnvironment, assertSubscriptionAuth, validateDispatchDecision } from './model-router-dispatch.mjs';
|
|
13
|
+
import { validateClaudeTerminalSettings } from './model-terminal-launchers.mjs';
|
|
14
|
+
|
|
15
|
+
const REFUSED = 'Controlled Claude turn refused; no fallback.';
|
|
16
|
+
const uuid = value => /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i.test(value || '');
|
|
17
|
+
const cleanText = value => String(value).replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, '');
|
|
18
|
+
const allocationEnv = /^(CLAUDE_CODE_EXTRA_BODY|CLAUDE_CODE_EFFORT_LEVEL|ANTHROPIC_DEFAULT_.*_MODEL|ANTHROPIC_MODEL|ANTHROPIC_SMALL_FAST_MODEL|CLAUDE_CODE_SUBAGENT_MODEL|CLAUDE_CODE_PLUGIN_(DIRS|CACHE_DIR|SEED_DIR)|CLAUDE_CODE_USE_COWORK_PLUGINS)$/;
|
|
19
|
+
|
|
20
|
+
/** Classic Brain hooks/MCP/skills remain loaded; native request-rewriting modules are refused. */
|
|
21
|
+
export function assertClaudeModuleBoundary(settings, { env = process.env, sessionId, read = fs.readFileSync } = {}) {
|
|
22
|
+
if (!settings || typeof settings !== 'object' || Object.keys(settings.env || {}).some(key => allocationEnv.test(key))) throw new Error(REFUSED);
|
|
23
|
+
const modules = value => value && typeof value === 'object' && Object.entries(value).some(([key, entry]) =>
|
|
24
|
+
key === 'modules' && (Array.isArray(entry) ? entry.length > 0 : Boolean(entry)) || modules(entry));
|
|
25
|
+
if (modules(settings.hooks)) throw new Error(REFUSED);
|
|
26
|
+
const config = env.CLAUDE_CONFIG_DIR || path.join(env.HOME || os.homedir(), '.claude');
|
|
27
|
+
const json = file => JSON.parse(read(file, 'utf8'));
|
|
28
|
+
const enabled = Object.entries(settings.enabledPlugins || {}).filter(([, value]) => value !== false).map(([key]) => key);
|
|
29
|
+
let registry;
|
|
30
|
+
if (enabled.length) registry = json(path.join(config, 'plugins', 'installed_plugins.json'));
|
|
31
|
+
for (const id of enabled) {
|
|
32
|
+
const installations = registry.plugins?.[id];
|
|
33
|
+
if (!Array.isArray(installations) || !installations.length) throw new Error(REFUSED);
|
|
34
|
+
const at = id.lastIndexOf('@'), name = id.slice(0, at), marketplace = id.slice(at + 1);
|
|
35
|
+
if (at < 1) throw new Error(REFUSED);
|
|
36
|
+
const known = json(path.join(config, 'plugins', 'known_marketplaces.json'));
|
|
37
|
+
const marketplaceRoot = known[marketplace]?.installLocation;
|
|
38
|
+
if (!path.isAbsolute(marketplaceRoot || '')) throw new Error(REFUSED);
|
|
39
|
+
const entry = json(path.join(marketplaceRoot, '.claude-plugin', 'marketplace.json')).plugins?.find(plugin => plugin.name === name);
|
|
40
|
+
if (!entry || modules(entry)) throw new Error(REFUSED);
|
|
41
|
+
// Refuse any registered module-bearing installation of an enabled ID; never guess scope precedence.
|
|
42
|
+
for (const installation of installations) {
|
|
43
|
+
const root = installation.installPath;
|
|
44
|
+
if (!path.isAbsolute(root || '')) throw new Error(REFUSED);
|
|
45
|
+
const manifestFiles = [path.join(root, '.claude-plugin', 'plugin.json'), path.join(root, 'plugin.json')];
|
|
46
|
+
// Native strict:false marketplace entries may supply the entire manifest (e.g. LSP plugins).
|
|
47
|
+
const manifests = manifestFiles.filter(file => fs.existsSync(file)).map(json);
|
|
48
|
+
if (!manifests.length && entry.strict === false) manifests.push(entry);
|
|
49
|
+
if (!manifests.length || manifests.some(modules)) throw new Error(REFUSED);
|
|
50
|
+
const hooks = [path.join(root, 'hooks', 'hooks.json')];
|
|
51
|
+
for (const relative of [...manifests.map(manifest => manifest.hooks), entry.hooks].flat().filter(value => typeof value === 'string')) {
|
|
52
|
+
const resolved = path.resolve(root, relative);
|
|
53
|
+
if (!resolved.startsWith(root + path.sep)) throw new Error(REFUSED);
|
|
54
|
+
hooks.push(resolved);
|
|
55
|
+
}
|
|
56
|
+
for (const file of hooks) if (fs.existsSync(file) && modules(json(file))) throw new Error(REFUSED);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const nonempty = folder => fs.existsSync(folder) && fs.readdirSync(folder).length > 0;
|
|
60
|
+
// Native 2.1.289 rft()/b(sessionId): <config-dir>/dev-mods/<session-id>.
|
|
61
|
+
if (nonempty(path.join(config, 'dev-mods', sessionId))) throw new Error(REFUSED);
|
|
62
|
+
const projects = path.join(config, 'projects');
|
|
63
|
+
if (fs.existsSync(projects)) for (const directory of fs.readdirSync(projects)) {
|
|
64
|
+
const transcript = path.join(projects, directory, `${sessionId}.jsonl`);
|
|
65
|
+
if (!fs.existsSync(transcript)) continue;
|
|
66
|
+
// A resumed transcript may restore a different dev-mods folder (native last-wins record).
|
|
67
|
+
for (const line of read(transcript, 'utf8').split('\n').filter(Boolean)) {
|
|
68
|
+
const record = JSON.parse(line);
|
|
69
|
+
if (record.type === 'dev-mods' && record.folder && nonempty(record.folder)) throw new Error(REFUSED);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Retire only the directly owned process; never infer descendant cleanup from its exit. */
|
|
75
|
+
export function retireControlledClaudeChild(child, { graceMs = 200, killMs = 200 } = {}) {
|
|
76
|
+
return new Promise(resolve => {
|
|
77
|
+
let finished = false, escalation, deadline;
|
|
78
|
+
const finish = () => {
|
|
79
|
+
if (finished) return;
|
|
80
|
+
finished = true; clearTimeout(escalation); clearTimeout(deadline);
|
|
81
|
+
child.removeListener('close', finish);
|
|
82
|
+
for (const stream of [child.stdin, child.stdout, child.stderr]) { stream?.removeAllListeners('data'); stream?.destroy(); }
|
|
83
|
+
child.unref?.(); resolve();
|
|
84
|
+
};
|
|
85
|
+
child.once('close', finish);
|
|
86
|
+
const kill = signal => { try { child.kill(signal); } catch { /* bounded cleanup still proceeds */ } };
|
|
87
|
+
escalation = setTimeout(() => kill('SIGKILL'), graceMs);
|
|
88
|
+
deadline = setTimeout(finish, graceMs + killMs);
|
|
89
|
+
kill('SIGTERM');
|
|
90
|
+
if (child.exitCode != null || child.signalCode != null) finish();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function controlledClaudeArguments(decision, sessionId, resume = false) {
|
|
95
|
+
if (decision?.harness !== 'claude-code' || decision.provider !== 'anthropic' || decision.subscriptionCovered !== true ||
|
|
96
|
+
!/^claude-[a-z0-9][a-z0-9.-]*$/.test(decision.model || '') ||
|
|
97
|
+
!['low', 'medium', 'high', 'xhigh', 'max'].includes(decision.effort) || !uuid(sessionId)) throw new Error(REFUSED);
|
|
98
|
+
return ['--print', '--input-format', 'stream-json', '--output-format', 'stream-json', '--verbose',
|
|
99
|
+
'--model', decision.model, '--effort', decision.effort, '--permission-mode', 'manual', '--permission-prompts', 'host',
|
|
100
|
+
'--permission-prompt-tool', 'stdio',
|
|
101
|
+
'--disallowedTools', 'Agent,Task',
|
|
102
|
+
resume ? '--resume' : '--session-id', sessionId];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** One native process per routed turn. Permission requests require an explicit host answer. */
|
|
106
|
+
export async function runControlledClaudeTurn({ binary, prompt, sessionId = crypto.randomUUID(), resume = false,
|
|
107
|
+
cwd = process.cwd(), env = process.env, decide = decideNativeTurn, verifyDecision = validateDispatchDecision,
|
|
108
|
+
checkSettings = validateClaudeTerminalSettings, checkAuth = assertSubscriptionAuth, spawnNative = spawn,
|
|
109
|
+
probe = execFileSync, approve = async () => false, output = () => {}, receipt = appendGatewayReceipt,
|
|
110
|
+
checkModules = assertClaudeModuleBoundary, signal, timeoutMs = 900000, handshakeMs = 10000 } = {}) {
|
|
111
|
+
if (!path.isAbsolute(binary || '') || typeof prompt !== 'string' || !prompt.trim() || prompt.length > 200000 ||
|
|
112
|
+
/^\s*\//.test(prompt) || !Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 900000 ||
|
|
113
|
+
!Number.isSafeInteger(handshakeMs) || handshakeMs <= 0 || handshakeMs > 30000 || signal?.aborted) throw new Error(REFUSED);
|
|
114
|
+
// Extra body / effort env overrides can bypass the native CLI's requested allocation.
|
|
115
|
+
if (Object.keys(env).some(key => allocationEnv.test(key))) throw new Error(REFUSED);
|
|
116
|
+
const clean = subscriptionEnvironment(env);
|
|
117
|
+
const managed = process.platform === 'darwin' ? '/Library/Application Support/ClaudeCode/managed-settings.json' : '/etc/claude-code/managed-settings.json';
|
|
118
|
+
if (fs.existsSync(managed)) throw new Error('Managed Claude settings require separate module-boundary qualification.');
|
|
119
|
+
checkSettings({ env: clean, cwd, home: env.HOME || os.homedir() });
|
|
120
|
+
checkAuth('claude-code', { env: clean, probe: (_name, args, options) => probe(binary, args, options) });
|
|
121
|
+
const decision = await decide(prompt, 'claude-code', { env: clean });
|
|
122
|
+
verifyDecision(decision);
|
|
123
|
+
const args = controlledClaudeArguments(decision, sessionId, resume);
|
|
124
|
+
if (signal?.aborted) throw new Error(REFUSED);
|
|
125
|
+
const child = spawnNative(binary, args, { cwd, env: clean, shell: false, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
let done = false, phase = 'initialize', buffer = '', bytes = 0, result, observed = false, permissions = false;
|
|
128
|
+
const decoder = new StringDecoder('utf8');
|
|
129
|
+
const ids = { initialize: crypto.randomUUID(), before: crypto.randomUUID(), after: crypto.randomUUID() };
|
|
130
|
+
let handshake;
|
|
131
|
+
const timer = setTimeout(() => fail(), timeoutMs);
|
|
132
|
+
const abort = () => fail();
|
|
133
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
134
|
+
const clear = () => { clearTimeout(timer); clearTimeout(handshake); signal?.removeEventListener('abort', abort); };
|
|
135
|
+
const fail = () => {
|
|
136
|
+
if (done) return;
|
|
137
|
+
done = true; clear();
|
|
138
|
+
void retireControlledClaudeChild(child).then(() => reject(new Error(REFUSED)));
|
|
139
|
+
};
|
|
140
|
+
const send = message => { if (!done) child.stdin.write(JSON.stringify(message) + '\n'); };
|
|
141
|
+
const control = which => {
|
|
142
|
+
phase = which; clearTimeout(handshake); handshake = setTimeout(fail, handshakeMs);
|
|
143
|
+
send({ type: 'control_request', request_id: ids[which], request: { subtype: which === 'initialize' ? 'initialize' : 'get_settings' } });
|
|
144
|
+
};
|
|
145
|
+
const settingsMatch = value => value?.applied?.model === decision.model && value.applied.effort === decision.effort && !value.errors?.length &&
|
|
146
|
+
Array.isArray(value.sources) && !value.sources.some(source => source.source === 'policySettings' && Object.keys(source.settings || {}).length);
|
|
147
|
+
const onMessage = async message => {
|
|
148
|
+
if (done) return;
|
|
149
|
+
if (message.type === 'control_response') {
|
|
150
|
+
const response = message.response;
|
|
151
|
+
if (response?.request_id !== ids[phase] || response.subtype !== 'success') return fail();
|
|
152
|
+
if (phase === 'initialize') return control('before');
|
|
153
|
+
if (!settingsMatch(response.response)) return fail();
|
|
154
|
+
if (phase === 'before') {
|
|
155
|
+
clearTimeout(handshake); phase = 'turn';
|
|
156
|
+
checkModules(response.response.effective, { env: clean, sessionId });
|
|
157
|
+
verifyDecision(decision); // Allocation may have changed while native startup was running.
|
|
158
|
+
receipt({ ts: new Date().toISOString(), harness: 'claude-code', status: 'native-settings-observed',
|
|
159
|
+
model: decision.model, effort: decision.effort, taskClass: decision.taskClass, modelObserved: false,
|
|
160
|
+
evidence: 'get_settings applied model and effort before prompt; execution identity pending' }, { env: clean });
|
|
161
|
+
send({ type: 'user', session_id: sessionId, message: { role: 'user', content: prompt } });
|
|
162
|
+
} else if (phase === 'after') { checkModules(response.response.effective, { env: clean, sessionId }); phase = 'exit'; clearTimeout(handshake); child.stdin.end(); }
|
|
163
|
+
else fail();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (message.type === 'control_request') {
|
|
167
|
+
if (phase !== 'turn' || permissions || message.request?.subtype !== 'can_use_tool' || typeof message.request_id !== 'string') return fail();
|
|
168
|
+
if (['Agent', 'Task'].includes(message.request.tool_name)) return fail();
|
|
169
|
+
permissions = true;
|
|
170
|
+
let allowed = false;
|
|
171
|
+
try { allowed = await approve(message.request) === true; } catch { allowed = false; }
|
|
172
|
+
permissions = false;
|
|
173
|
+
send({ type: 'control_response', response: { subtype: 'success', request_id: message.request_id,
|
|
174
|
+
response: allowed ? { behavior: 'allow', updatedInput: message.request.input } :
|
|
175
|
+
{ behavior: 'deny', message: 'The host did not approve this tool request.' } } });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (message.session_id && message.session_id !== sessionId) return fail();
|
|
179
|
+
if (message.type === 'assistant' && !message.parent_tool_use_id) {
|
|
180
|
+
if (phase !== 'turn' || message.message?.model !== decision.model) return fail();
|
|
181
|
+
observed = true;
|
|
182
|
+
for (const block of message.message.content || []) if (block.type === 'text') output(cleanText(block.text));
|
|
183
|
+
}
|
|
184
|
+
if (message.type === 'result') {
|
|
185
|
+
if (phase !== 'turn' || permissions || !observed || message.session_id !== sessionId || message.subtype !== 'success' ||
|
|
186
|
+
message.is_error || message.permission_denials?.length || message.errors?.length) return fail();
|
|
187
|
+
result = message; control('after');
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
child.stdout.on('data', chunk => {
|
|
191
|
+
if (done) return;
|
|
192
|
+
bytes += chunk.length; if (bytes > 16 * 1024 * 1024) return fail();
|
|
193
|
+
buffer += decoder.write(chunk);
|
|
194
|
+
let newline;
|
|
195
|
+
while ((newline = buffer.indexOf('\n')) !== -1 && !done) {
|
|
196
|
+
const line = buffer.slice(0, newline); buffer = buffer.slice(newline + 1);
|
|
197
|
+
try { void onMessage(JSON.parse(line)).catch(fail); } catch { fail(); }
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
child.stderr.on('data', () => {}); // Native diagnostics may contain credentials or prompt text.
|
|
201
|
+
child.stdin.on('error', fail); child.once('error', fail);
|
|
202
|
+
child.once('close', code => {
|
|
203
|
+
if (done) return;
|
|
204
|
+
if (code !== 0 || phase !== 'exit' || !result || buffer.trim() || decoder.end()) return fail();
|
|
205
|
+
try {
|
|
206
|
+
receipt({ ts: new Date().toISOString(), harness: 'claude-code', status: 'completed', model: decision.model,
|
|
207
|
+
effort: decision.effort, taskClass: decision.taskClass, modelObserved: true,
|
|
208
|
+
evidence: 'assistant model observed; get_settings applied effort matched before and after turn; per-request effort not exposed' }, { env: clean });
|
|
209
|
+
done = true; clear(); resolve({ sessionId, decision, modelObserved: true, effortSettingsObserved: true, perRequestEffortObserved: false });
|
|
210
|
+
} catch { fail(); }
|
|
211
|
+
});
|
|
212
|
+
control('initialize');
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Native tool approvals are presented by this host; --print supplies no terminal dialogs. */
|
|
217
|
+
export async function launchControlledClaudeTerminal({ binary, args = [], input = process.stdin, output = process.stdout,
|
|
218
|
+
diagnostics = process.stderr, env = process.env, cwd = process.cwd(), runTurn = runControlledClaudeTurn } = {}) {
|
|
219
|
+
let sessionId, resume = false, initialPrompt, ownerBypass = false;
|
|
220
|
+
const remaining = [...args];
|
|
221
|
+
const invalid = () => new Error('Controlled Claude accepts only --resume <session UUID>, --permission-mode bypassPermissions, and a literal initial prompt.');
|
|
222
|
+
while (remaining[0]?.startsWith('-')) {
|
|
223
|
+
if (remaining[0] === '--resume' && !resume && uuid(remaining[1])) {
|
|
224
|
+
sessionId = remaining[1]; resume = true;
|
|
225
|
+
} else if (remaining[0] === '--permission-mode' && !ownerBypass && remaining[1] === 'bypassPermissions') {
|
|
226
|
+
ownerBypass = true;
|
|
227
|
+
} else throw invalid();
|
|
228
|
+
remaining.splice(0, 2);
|
|
229
|
+
}
|
|
230
|
+
if (remaining.some(arg => typeof arg !== 'string' || arg.startsWith('-'))) throw invalid();
|
|
231
|
+
if (remaining.length) initialPrompt = remaining.join(' ');
|
|
232
|
+
if (!input.isTTY || !output.isTTY) throw new Error('Controlled Claude requires a person at a terminal for prompts and approvals.');
|
|
233
|
+
const terminal = createInterface({ input, output });
|
|
234
|
+
const controller = new AbortController();
|
|
235
|
+
const cancel = () => controller.abort();
|
|
236
|
+
terminal.on('SIGINT', cancel);
|
|
237
|
+
diagnostics.write('Controlled Claude: one reviewed native allocation per prompt; tool approvals are answered here. Native Agent/Task workers are disabled; use the managed dispatcher for independent child work. /exit closes.\n');
|
|
238
|
+
if (ownerBypass) diagnostics.write('Owner permission bypass is active: native tool requests are approved by this host automatically. Agent/Task remain refused; routing and subscription guards remain active.\n');
|
|
239
|
+
try {
|
|
240
|
+
while (!controller.signal.aborted) {
|
|
241
|
+
const prompt = initialPrompt ?? await terminal.question('Claude> '); initialPrompt = undefined;
|
|
242
|
+
if (prompt.trim() === '/exit') break;
|
|
243
|
+
if (!prompt.trim()) continue;
|
|
244
|
+
const turn = await runTurn({ binary, prompt, sessionId, resume, cwd, env, signal: controller.signal,
|
|
245
|
+
output: text => output.write(text + '\n'), approve: async request => {
|
|
246
|
+
if (['Agent', 'Task'].includes(request.tool_name)) return false;
|
|
247
|
+
const details = cleanText(JSON.stringify({ tool: request.tool_name, input: request.input, reason: request.decision_reason }));
|
|
248
|
+
output.write(`Native permission request (untrusted tool text):\n${details}\n`);
|
|
249
|
+
if (ownerBypass) return true;
|
|
250
|
+
return (await terminal.question('Approve this tool request? Type yes: ')).trim() === 'yes';
|
|
251
|
+
} });
|
|
252
|
+
sessionId = turn.sessionId; resume = true;
|
|
253
|
+
diagnostics.write(`Native session: ${sessionId} (resume with --resume ${sessionId})\n`);
|
|
254
|
+
}
|
|
255
|
+
} finally { terminal.close(); }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
259
|
+
const args = process.argv.slice(2);
|
|
260
|
+
if (args[0] !== '--real-binary' || !args[1] || args[2] !== '--') {
|
|
261
|
+
process.stderr.write('Expected --real-binary /absolute/native/claude -- [--resume UUID]\n'); process.exitCode = 1;
|
|
262
|
+
} else launchControlledClaudeTerminal({ binary: args[1], args: args.slice(3) }).catch(() => {
|
|
263
|
+
process.stderr.write(REFUSED + '\n'); process.exitCode = 1;
|
|
264
|
+
});
|
|
265
|
+
}
|
package/scripts/issue-watch.mjs
CHANGED
|
@@ -150,16 +150,19 @@ export async function run({ dryRun = false, now = Date.now(), repo = REPO } = {}
|
|
|
150
150
|
// lastAlertAt: state is only written when the push actually went out, so a failed push
|
|
151
151
|
// retries next run instead of burying the sighting.
|
|
152
152
|
const firstSighting = !state[key];
|
|
153
|
+
// Acknowledgment delivery is not page delivery. Retry each missing channel independently.
|
|
154
|
+
const needsNewPage = !state[key]?.newAlertAt;
|
|
155
|
+
const needsAck = !state[key]?.ackAt;
|
|
153
156
|
|
|
154
157
|
results.push({ number: issue.number, title: issue.title, ageHours, ownerComment, breach, dueForAlert, firstSighting, url });
|
|
155
158
|
|
|
156
|
-
if (
|
|
159
|
+
if (needsNewPage || needsAck) {
|
|
157
160
|
if (dryRun) {
|
|
158
161
|
alertsSent.push({ number: issue.number, sent: false, kind: 'new-issue', reason: 'dry-run' });
|
|
159
162
|
} else {
|
|
160
|
-
const topic = resolveTopic();
|
|
161
|
-
let sent =
|
|
162
|
-
if (topic) sent = await pushNtfy(topic, {
|
|
163
|
+
const topic = needsNewPage ? resolveTopic() : null;
|
|
164
|
+
let sent = !needsNewPage;
|
|
165
|
+
if (needsNewPage && topic) sent = await pushNtfy(topic, {
|
|
163
166
|
title: `New issue #${issue.number} (open ${fmtAge(ageHours)})`,
|
|
164
167
|
body: `${issue.title}\n${url}`,
|
|
165
168
|
priority: 'high', tags: 'new,eyes',
|
|
@@ -171,12 +174,20 @@ export async function run({ dryRun = false, now = Date.now(), repo = REPO } = {}
|
|
|
171
174
|
// not caring — the opposite of the point). Carries BOT_MARKER so judgeIssue() can never
|
|
172
175
|
// mistake it for the owner responding. Best-effort like ntfy: a comment failure must not
|
|
173
176
|
// break the watch; unacked issues simply retry next run (ackAt is delivery-derived).
|
|
174
|
-
let
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
let acked = !needsAck;
|
|
178
|
+
if (needsAck) {
|
|
179
|
+
const ackBody = `${BOT_MARKER} acknowledgment — received and opened. The next update here will be a fix, findings, or the maintainer in person.`;
|
|
180
|
+
const ack = spawnSync(GH_BIN, ['issue', 'comment', String(issue.number), '--repo', repo, '--body', ackBody], { encoding: 'utf8' });
|
|
181
|
+
acked = ack.status === 0;
|
|
182
|
+
}
|
|
183
|
+
if (sent || acked) state[key] = {
|
|
184
|
+
...(state[key] || {}), firstSeenAt: state[key]?.firstSeenAt || new Date(now).toISOString(),
|
|
185
|
+
...(needsNewPage && sent ? { newAlertAt: new Date(now).toISOString() } : {}),
|
|
186
|
+
...(needsAck && acked ? { ackAt: new Date(now).toISOString() } : {}), title: issue.title, url,
|
|
187
|
+
};
|
|
188
|
+
alertsSent.push({ number: issue.number, sent, acked, kind: 'new-issue',
|
|
189
|
+
pageAttempted: needsNewPage, ackAttempted: needsAck,
|
|
190
|
+
reason: needsNewPage && !topic ? 'no ntfy topic configured' : null });
|
|
180
191
|
}
|
|
181
192
|
}
|
|
182
193
|
|
|
@@ -111,7 +111,7 @@ export function installTerminalLaunchers({ home = os.homedir(), nodeBinary = pro
|
|
|
111
111
|
if ([runner, ...Object.keys(contents)].includes(binary)) throw new Error('Native terminal launcher recursion refused');
|
|
112
112
|
}
|
|
113
113
|
const receipt = { apply, launchers, configPath, config, shellSource, shellConflicts: shell.conflicts, backups: [],
|
|
114
|
-
claudeEnforcementScope: realClaude ? '
|
|
114
|
+
claudeEnforcementScope: realClaude ? 'Controlled native prompt boundary; model observations and effective effort settings checked for each completed turn' : null };
|
|
115
115
|
if (!apply) return receipt;
|
|
116
116
|
for (const file of [configPath, ...Object.keys(contents), ...(manageZsh ? [shellSource, zshrc] : [])]) {
|
|
117
117
|
const original = backup(file); if (original) receipt.backups.push(original);
|
|
@@ -226,18 +226,22 @@ export function validateClaudeTerminalArguments(args) {
|
|
|
226
226
|
return { options, prompts };
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
function claudeAdministrativeArguments(args) {
|
|
230
|
+
const administrativeArgs = args[0] === '--permission-mode' && args[1] === 'bypassPermissions' ? args.slice(2) : args;
|
|
231
|
+
const information = new Set(['--help', '-h', '--version', '-v']);
|
|
232
|
+
const ownerPermissionFlags = new Set(['--dangerously-skip-permissions', '--allow-dangerously-skip-permissions']);
|
|
233
|
+
return administrativeArgs.some(arg => information.has(arg)) &&
|
|
234
|
+
administrativeArgs.every(arg => information.has(arg) || ownerPermissionFlags.has(arg)) ||
|
|
235
|
+
['auth', 'mcp', 'plugin', 'plugins', 'update', 'upgrade', 'doctor', 'install'].includes(administrativeArgs[0]);
|
|
236
|
+
}
|
|
237
|
+
|
|
229
238
|
/** Native plugin startup only: an unloaded/crashed native worker may skip subsequent hooks. */
|
|
230
239
|
export async function runClaudeTerminal({ config, args = [], env = process.env, startupMs = 10000,
|
|
231
240
|
tempRoot = os.tmpdir(), cwd = process.cwd(), signalSource = process, diagnostics = process.stderr } = {}) {
|
|
232
241
|
if (env.RNB_TERMINAL_LAUNCH_ACTIVE) throw new Error('Native terminal launcher recursion refused');
|
|
233
242
|
if (!Number.isFinite(startupMs) || startupMs <= 0 || startupMs > 60000) throw new Error('Bounded native hook startup deadline required');
|
|
234
243
|
const binary = executable(config.realClaude), clean = subscriptionEnvironment(env);
|
|
235
|
-
|
|
236
|
-
const ownerPermissionFlags = new Set(['--dangerously-skip-permissions', '--allow-dangerously-skip-permissions']);
|
|
237
|
-
const administrative = args.some((arg) => information.has(arg)) &&
|
|
238
|
-
args.every((arg) => information.has(arg) || ownerPermissionFlags.has(arg)) ||
|
|
239
|
-
['auth', 'mcp', 'plugin', 'plugins', 'update', 'upgrade', 'doctor', 'install'].includes(args[0]);
|
|
240
|
-
if (administrative) {
|
|
244
|
+
if (claudeAdministrativeArguments(args)) {
|
|
241
245
|
const child = spawn(binary, args, { env: clean, cwd, stdio: 'inherit', shell: false });
|
|
242
246
|
const unforward = forwardSignals(child, signalSource);
|
|
243
247
|
try { return await exitOf(child); } finally { unforward(); }
|
|
@@ -280,8 +284,14 @@ export async function runClaudeTerminal({ config, args = [], env = process.env,
|
|
|
280
284
|
}
|
|
281
285
|
}
|
|
282
286
|
|
|
283
|
-
export async function runTerminalLauncher({ host, args, config, env = process.env, signalSource = process } = {}) {
|
|
284
|
-
if (host === 'claude')
|
|
287
|
+
export async function runTerminalLauncher({ host, args = [], config, env = process.env, signalSource = process } = {}) {
|
|
288
|
+
if (host === 'claude') {
|
|
289
|
+
if (claudeAdministrativeArguments(args)) return runClaudeTerminal({ config, args, env, signalSource });
|
|
290
|
+
if (env.RNB_TERMINAL_LAUNCH_ACTIVE) throw new Error('Native terminal launcher recursion refused');
|
|
291
|
+
const { launchControlledClaudeTerminal } = await import('./claude-controlled-terminal.mjs');
|
|
292
|
+
await launchControlledClaudeTerminal({ binary: executable(config.realClaude), args, env, cwd: process.cwd() });
|
|
293
|
+
return { code: 0, signal: null };
|
|
294
|
+
}
|
|
285
295
|
if (host !== 'codex') throw new Error('Unsupported terminal host');
|
|
286
296
|
const invocation = terminalInvocation({ host, args, config, env });
|
|
287
297
|
const child = spawn(invocation.command, invocation.args, { env: { ...subscriptionEnvironment(env), RNB_TERMINAL_LAUNCH_ACTIVE: '1' }, stdio: 'inherit', shell: false });
|
|
@@ -36,16 +36,50 @@ function writeOwned(dir, token, file, value) { const committed = transaction(dir
|
|
|
36
36
|
function release(dir, token) { transaction(dir, () => { if (json(dir, ownerFile, 4096)?.token === token) fs.unlinkSync(path.join(dir, ownerFile)); }); }
|
|
37
37
|
export function runCycleStage({ script, args, timeoutMs, env, spawnHost = spawn }) {
|
|
38
38
|
return new Promise((resolve, reject) => {
|
|
39
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 900000) throw new Error('Invalid weekly stage deadline');
|
|
40
|
+
const deadline = performance.now() + timeoutMs, retirementMs = Math.min(1000, timeoutMs / 10);
|
|
41
|
+
const workDeadline = deadline - retirementMs;
|
|
39
42
|
const child = spawnHost(process.execPath, [path.join(DIR, script), ...args], { env, shell: false, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
40
|
-
let stdout = '',
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
let stdout = '', outputBytes = 0, failure, settled = false, exitObserved = false;
|
|
44
|
+
let timer, killTimer, retirementTimer;
|
|
45
|
+
const kill = (signal) => { try { child.kill(signal); } catch { /* owned child retirement remains unverified */ } };
|
|
46
|
+
const finish = (error, value) => {
|
|
47
|
+
if (settled) return; settled = true;
|
|
48
|
+
clearTimeout(timer); clearTimeout(killTimer); clearTimeout(retirementTimer);
|
|
49
|
+
// Drop only our child's referenced handles, including pipes held open after its exit.
|
|
50
|
+
// Signals/exit cannot prove detached descendants retired.
|
|
51
|
+
for (const stream of [child.stdin, child.stdout, child.stderr]) { try { stream?.destroy?.(); } catch { /* best effort */ } }
|
|
52
|
+
try { child.unref?.(); } catch { /* best effort */ }
|
|
53
|
+
if (error) {
|
|
54
|
+
error.stageCleanup = { ownedChildPid: child.pid ?? null, childExitObserved: exitObserved,
|
|
55
|
+
ownedChildRetirement: exitObserved ? 'exit-observed' : 'unverified', descendantRetirement: 'unproven' };
|
|
56
|
+
reject(error);
|
|
57
|
+
} else resolve(value);
|
|
58
|
+
};
|
|
59
|
+
const fail = (reason) => {
|
|
60
|
+
if (settled || failure) return; failure = new Error(reason);
|
|
61
|
+
// Reserve retirement inside the caller's budget; do not wait indefinitely for exit/close.
|
|
62
|
+
const remaining = Math.max(0, Math.min(retirementMs, deadline - performance.now()));
|
|
63
|
+
killTimer = setTimeout(() => kill('SIGKILL'), remaining / 2);
|
|
64
|
+
retirementTimer = setTimeout(() => finish(failure), remaining);
|
|
65
|
+
kill('SIGTERM');
|
|
66
|
+
};
|
|
67
|
+
const consume = (chunk, capture) => {
|
|
68
|
+
if (settled || failure) return;
|
|
69
|
+
if (performance.now() >= workDeadline) return fail('Weekly cycle child exceeded bounded deadline or output limit');
|
|
70
|
+
outputBytes += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(String(chunk));
|
|
71
|
+
if (outputBytes > 262144) return fail('Weekly cycle child exceeded bounded deadline or output limit');
|
|
72
|
+
if (capture) stdout += chunk;
|
|
73
|
+
};
|
|
74
|
+
child.stderr.on('data', (chunk) => consume(chunk, false)); child.stdout.on('data', (chunk) => consume(chunk, true));
|
|
75
|
+
child.on('error', () => fail('Weekly cycle child host unavailable'));
|
|
76
|
+
child.once('exit', (code) => {
|
|
77
|
+
exitObserved = true; if (failure) return finish(failure); if (settled) return;
|
|
78
|
+
if (performance.now() >= workDeadline) { fail('Weekly cycle child exceeded bounded deadline or output limit'); return finish(failure); }
|
|
79
|
+
let result; try { result = JSON.parse(stdout.trim().split('\n').at(-1)); } catch { return finish(new Error('Weekly cycle child receipt missing')); }
|
|
80
|
+
finish(null, { code, result });
|
|
48
81
|
});
|
|
82
|
+
timer = setTimeout(() => fail('Weekly cycle child exceeded bounded deadline or output limit'), Math.max(0, workDeadline - performance.now()));
|
|
49
83
|
});
|
|
50
84
|
}
|
|
51
85
|
const INVENTORY_URL = 'https://openrouter.ai/api/v1/models';
|
|
@@ -172,7 +206,9 @@ export async function runWeeklyCycle({ routerDir = path.join(os.homedir(), '.cla
|
|
|
172
206
|
delete state.pendingSemanticReceipt; delete state.pendingSemanticReleaseIds;
|
|
173
207
|
state.lastQualification = qualified.result; save(STATE_FILE, state); record.status = 'complete';
|
|
174
208
|
save('weekly-cycle-last-attempt.json', record); return record;
|
|
175
|
-
} catch (error) { record.status = 'failed'; record.releaseStatus = 'unknown-or-pending'; record.reason = error.message.slice(0, 240);
|
|
209
|
+
} catch (error) { record.status = 'failed'; record.releaseStatus = 'unknown-or-pending'; record.reason = error.message.slice(0, 240);
|
|
210
|
+
if (error.stageCleanup) record.stageCleanup = error.stageCleanup;
|
|
211
|
+
try { save('weekly-cycle-last-attempt.json', record); } catch { /* superseded cannot write */ } return record;
|
|
176
212
|
} finally { release(routerDir, token); }
|
|
177
213
|
}
|
|
178
214
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
import { execFileSync } from 'node:child_process';
|
|
20
20
|
import fs from 'node:fs';
|
|
21
21
|
import path from 'node:path';
|
|
22
|
-
import {
|
|
22
|
+
import { randomUUID } from 'node:crypto';
|
|
23
|
+
import { resolveRuflo, rufloInvocation, RUFLO_MISSING } from '../plugin/scripts/ruflo-bin.mjs';
|
|
23
24
|
|
|
24
25
|
const arg = (name, def = '') => {
|
|
25
26
|
const i = process.argv.indexOf(`--${name}`);
|
|
@@ -60,21 +61,26 @@ const value = [
|
|
|
60
61
|
worked ? `WORKED: ${worked}` : null,
|
|
61
62
|
critique ? `CRITIQUE: ${critique}` : null,
|
|
62
63
|
`OUTCOME: ${outcome}`,
|
|
64
|
+
`RECORDING: ${randomUUID()}`, // binds the read-back to THIS canonical lesson write
|
|
63
65
|
].filter(Boolean).join(' ');
|
|
64
66
|
|
|
65
67
|
// Every `ruflo` invocation auto-starts a project background daemon unless this is set (verified
|
|
66
68
|
// live: ~/.npm-global/lib/node_modules/ruflo/node_modules/@claude-flow/cli/dist/src/services/
|
|
67
69
|
// daemon-autostart.js:85) — recording a lesson has no business leaving one running.
|
|
68
|
-
const ruflo = (args) =>
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
const ruflo = (args) => {
|
|
71
|
+
const invocation = rufloInvocation(RUFLO, args);
|
|
72
|
+
return execFileSync(invocation.executable, invocation.args, {
|
|
73
|
+
cwd: dir, encoding: 'utf8', timeout: 60000, shell: false,
|
|
74
|
+
env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' },
|
|
75
|
+
});
|
|
76
|
+
};
|
|
71
77
|
|
|
72
78
|
console.log(`\nRecording lesson into ${path.basename(dir)}/.swarm/memory.db (namespace: ${ns})`);
|
|
73
79
|
console.log(` key: ${key}`);
|
|
74
80
|
|
|
75
81
|
// 1a. STORE (native, signal namespace) — L1 content + L2 embedding
|
|
76
82
|
try {
|
|
77
|
-
ruflo(['memory', 'store', '-k', key, '-n', ns, '--value', value]);
|
|
83
|
+
ruflo(['memory', 'store', '-k', key, '-n', ns, '--value', value, '--path', db]);
|
|
78
84
|
} catch (e) {
|
|
79
85
|
console.error(' store FAILED:', String(e.stdout || e.message).split('\n')[0]);
|
|
80
86
|
process.exit(1);
|
|
@@ -91,7 +97,7 @@ try {
|
|
|
91
97
|
let stored = false;
|
|
92
98
|
try {
|
|
93
99
|
const back = ruflo(['memory', 'retrieve', '-k', key, '-n', ns, '--value-only', '--path', db]);
|
|
94
|
-
stored = String(back)
|
|
100
|
+
stored = String(back) === value;
|
|
95
101
|
} catch (e) {
|
|
96
102
|
console.error(' round-trip FAILED:', String(e.stdout || e.message).split('\n')[0]);
|
|
97
103
|
}
|
|
@@ -133,6 +139,6 @@ console.log(
|
|
|
133
139
|
// DERIVED, not asserted (F15): the closing line reports exactly what was verified, never more. The
|
|
134
140
|
// old line claimed "captured, refined, and recall-verified" even when distill failed and recall
|
|
135
141
|
// didn't return the key — asserted prose over an honest exit code.
|
|
136
|
-
const parts = ['captured', distillOk ? 'refined' : 'NOT refined (distill failed)', recalled ? 'recall-verified' : 'recall NOT verified'];
|
|
142
|
+
const parts = [stored ? 'captured' : 'capture NOT verified', distillOk ? 'refined' : 'NOT refined (distill failed)', recalled ? 'recall-verified' : 'recall NOT verified'];
|
|
137
143
|
console.log(`\nDone. Lesson is ${parts.join(', ')}.\n`);
|
|
138
144
|
process.exit(stored ? 0 : 1);
|
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
// Reviewed stabilization release boundaries. Legacy tests remain developer diagnostics.
|
|
2
2
|
export const RELEASE_REQUIREMENTS = Object.freeze({
|
|
3
3
|
"source": [
|
|
4
|
+
{
|
|
5
|
+
"id": "installer-release-argument-safety",
|
|
6
|
+
"reason": "Incomplete named releases refuse before network or settings writes; valid named releases and installer help retain their contracts",
|
|
7
|
+
"files": ["tests/unit/install-release-fallback.test.mjs"]
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": "honest-currency-lesson-and-metrics-evidence",
|
|
11
|
+
"reason": "Foreign KB roots remain isolated; panel currency derives from measured completion rather than checkout time; exact lesson readback binds this shell-free write; public metrics never fabricate performance or a competing product score",
|
|
12
|
+
"files": ["tests/unit/forge-currency-selected-root.test.mjs", "tests/unit/brain-score-producer.test.mjs", "tests/unit/brain-grade-groundtruth-timestamp.test.mjs", "tests/unit/record-lesson.test.mjs", "tests/unit/lesson-presentation-budget.test.mjs", "tests/unit/metrics-truth.test.mjs", "tests/unit/issue-watch-retry.test.mjs"]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "automatic-update-source-and-transport",
|
|
16
|
+
"reason": "Captured owner policy never falls back on read failures; installed-only automation avoids npx; transient GET and body failures retry without weakening trust; POSIX executable aliases retain interpreter identity",
|
|
17
|
+
"files": ["tests/unit/automatic-update.test.mjs", "tests/unit/download-retry.test.mjs"],
|
|
18
|
+
"platformFiles": {
|
|
19
|
+
"linux": ["tests/unit/automatic-update-node-alias.test.mjs"],
|
|
20
|
+
"macos": ["tests/unit/automatic-update-node-alias.test.mjs"]
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "customer-scope-and-session-ownership",
|
|
25
|
+
"reason": "Captured assistant commitments retain their actual session owner and honest noncompleted states; scoped grounding retains strict conservative evidence under contention; legacy guidance never suppresses a tool call",
|
|
26
|
+
"files": ["tests/unit/continuation-commitment-ownership.test.mjs"],
|
|
27
|
+
"platformFiles": {
|
|
28
|
+
"linux": ["tests/unit/grounding-scope.test.mjs", "tests/unit/hijack-no-defer.test.mjs"],
|
|
29
|
+
"macos": ["tests/unit/grounding-scope.test.mjs", "tests/unit/hijack-no-defer.test.mjs"]
|
|
30
|
+
}
|
|
31
|
+
},
|
|
4
32
|
{
|
|
5
33
|
"id": "bounded-advocacy-and-write-grounding",
|
|
6
34
|
"reason": "Explicit document meaning requests retain corroboration; literal searches stay silent; POSIX complete-write exemptions preserve executable and managed-store grounding",
|
|
@@ -83,7 +111,8 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
|
|
|
83
111
|
"tests/unit/forge-update-apply-rollback.test.mjs",
|
|
84
112
|
"tests/unit/forge-update-archive-digest.test.mjs",
|
|
85
113
|
"tests/unit/kb-copy-proof-legacy-sidecars.test.mjs",
|
|
86
|
-
"tests/unit/kb-copy-proof-unknown-content.test.mjs"
|
|
114
|
+
"tests/unit/kb-copy-proof-unknown-content.test.mjs",
|
|
115
|
+
"tests/unit/user-model-hook-install.test.mjs"
|
|
87
116
|
]
|
|
88
117
|
},
|
|
89
118
|
{
|
|
@@ -106,8 +135,8 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
|
|
|
106
135
|
"tests/unit/model-routing-launchers.test.mjs"
|
|
107
136
|
],
|
|
108
137
|
"platformFiles": {
|
|
109
|
-
"linux": ["tests/unit/model-terminal-gateway.test.mjs", "tests/unit/model-terminal-launchers.test.mjs"],
|
|
110
|
-
"macos": ["tests/unit/model-terminal-gateway.test.mjs", "tests/unit/model-terminal-launchers.test.mjs"],
|
|
138
|
+
"linux": ["tests/unit/model-terminal-gateway.test.mjs", "tests/unit/model-terminal-launchers.test.mjs", "tests/unit/claude-controlled-terminal.test.mjs"],
|
|
139
|
+
"macos": ["tests/unit/model-terminal-gateway.test.mjs", "tests/unit/model-terminal-launchers.test.mjs", "tests/unit/claude-controlled-terminal.test.mjs"],
|
|
111
140
|
"windows": ["tests/unit/windows-terminal-boundary.test.mjs"]
|
|
112
141
|
}
|
|
113
142
|
},
|
|
@@ -145,9 +174,11 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
|
|
|
145
174
|
},
|
|
146
175
|
{
|
|
147
176
|
"id": "canonical-memory-privacy",
|
|
148
|
-
"reason": "Canonical project isolation, persisted consent and truthful
|
|
177
|
+
"reason": "Canonical project isolation, persisted consent, excluded-resource privacy before recording, immutable delivery refusal and truthful terminal failure status",
|
|
149
178
|
"files": [
|
|
150
179
|
"tests/unit/turn-outcome-capture.test.mjs",
|
|
180
|
+
"tests/unit/turn-capture-content-privacy.test.mjs",
|
|
181
|
+
"tests/integration/project-progression-hook.test.mjs",
|
|
151
182
|
"tests/unit/turn-journal-platform.test.mjs",
|
|
152
183
|
"tests/unit/project-store-resolver.test.mjs"
|
|
153
184
|
]
|