ruvnet-brain 3.9.85-dev → 3.9.129-dev

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.
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash } from 'node:crypto';
4
+ import { spawn, spawnSync } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { probeSubscriptionHosts, subscriptionOnlyEnv } from './subscription-hosts.mjs';
7
+
8
+ const HOSTS = Object.freeze(['claude-code', 'codex']);
9
+ const HARD_PROBLEM = /\b(adr|architecture|architect|ddd|bounded context|aggregate|agentic[- ]?qe|holistic|security|production|migration|irreversible|threat model|experience)\b/i;
10
+
11
+ export function hardProblem(task) {
12
+ return HARD_PROBLEM.test(String(task));
13
+ }
14
+
15
+ export function chooseRoles(task) {
16
+ const firstByte = createHash('sha256').update(String(task)).digest()[0];
17
+ const scribe = HOSTS[firstByte % HOSTS.length];
18
+ return {
19
+ scribe,
20
+ verifier: HOSTS.find((host) => host !== scribe),
21
+ };
22
+ }
23
+
24
+ function hostKey(host) {
25
+ return host === 'claude-code' ? 'claude' : 'codex';
26
+ }
27
+
28
+ function promptFor(stage, payload) {
29
+ return [
30
+ 'You are one half of a subscription-only Claude Code and Codex deliberation.',
31
+ 'Do not request or use API keys. Work read-only. Return JSON only.',
32
+ `Stage: ${stage}`,
33
+ JSON.stringify(payload),
34
+ ].join('\n');
35
+ }
36
+
37
+ function parseCodexJsonl(stdout) {
38
+ const messages = String(stdout).trim().split('\n').flatMap((line) => {
39
+ try {
40
+ const value = JSON.parse(line);
41
+ return value.type === 'item.completed' && value.item?.type === 'agent_message'
42
+ ? [value.item.text]
43
+ : [];
44
+ } catch {
45
+ return [];
46
+ }
47
+ });
48
+ return messages.at(-1) ?? stdout;
49
+ }
50
+
51
+ function parseHostValue(host, stdout) {
52
+ const raw = host === 'claude-code'
53
+ ? (() => {
54
+ try {
55
+ const envelope = JSON.parse(stdout);
56
+ return envelope.result ?? envelope;
57
+ } catch {
58
+ return stdout;
59
+ }
60
+ })()
61
+ : parseCodexJsonl(stdout);
62
+ if (typeof raw !== 'string') return raw;
63
+ try {
64
+ return JSON.parse(raw);
65
+ } catch {
66
+ return { text: raw };
67
+ }
68
+ }
69
+
70
+ function spawnHost(binary, args, options) {
71
+ return new Promise((resolve) => {
72
+ const child = spawn(binary, args, options);
73
+ let stdout = '';
74
+ let stderr = '';
75
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
76
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
77
+ child.on('error', (error) => resolve({ status: null, stdout, stderr: error.message }));
78
+ child.on('close', (status) => resolve({ status, stdout, stderr }));
79
+ });
80
+ }
81
+
82
+ export async function runSubscriptionHost(host, stage, payload, { cwd = process.cwd() } = {}) {
83
+ const prompt = promptFor(stage, payload);
84
+ const env = subscriptionOnlyEnv();
85
+ const command = host === 'claude-code'
86
+ ? {
87
+ binary: 'claude',
88
+ args: [
89
+ '-p', '--output-format', 'json', '--permission-mode', 'plan',
90
+ '--tools', 'Read,Grep,Glob', '--no-session-persistence', '--effort', 'high', prompt,
91
+ ],
92
+ }
93
+ : {
94
+ binary: 'codex',
95
+ args: [
96
+ 'exec', '--ephemeral', '--sandbox', 'read-only', '--color', 'never', '--json',
97
+ '-m', 'gpt-5.6-sol', '-c', 'model_reasoning_effort="high"', prompt,
98
+ ],
99
+ };
100
+ const result = await spawnHost(command.binary, command.args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
101
+ if (result.status !== 0) {
102
+ return {
103
+ ok: false,
104
+ reason: /limit|quota|capacity|usage/i.test(result.stderr)
105
+ ? 'capacity-limited'
106
+ : 'host-failed',
107
+ };
108
+ }
109
+ return { ok: true, value: parseHostValue(host, result.stdout) };
110
+ }
111
+
112
+ export async function persistDeliberationReceipt(receipt, {
113
+ cwd = process.cwd(),
114
+ now = Date.now,
115
+ run = spawnSync,
116
+ } = {}) {
117
+ const recordedAt = now();
118
+ const value = {
119
+ protocol: receipt.protocol,
120
+ taskHash: receipt.taskHash,
121
+ hosts: receipt.hosts,
122
+ roles: receipt.roles,
123
+ accepted: receipt.accepted === true,
124
+ verifiedOutcome: receipt.accepted === true,
125
+ recordedAt: new Date(recordedAt).toISOString(),
126
+ };
127
+ const key = `dual-deliberation-${recordedAt}-${receipt.taskHash.slice(0, 12)}`;
128
+ const result = run('ruflo', [
129
+ 'memory', 'store',
130
+ '-k', key,
131
+ '--value', JSON.stringify(value),
132
+ '--namespace', 'ruvnet-brain',
133
+ '--path', '.swarm/memory.db',
134
+ '--no-upsert',
135
+ '--scan-content',
136
+ ], {
137
+ cwd,
138
+ encoding: 'utf8',
139
+ env: subscriptionOnlyEnv(),
140
+ timeout: 15_000,
141
+ });
142
+ return result?.status === 0;
143
+ }
144
+
145
+ function missingHosts(probes) {
146
+ return HOSTS.filter((host) => !probes[hostKey(host)]?.eligible);
147
+ }
148
+
149
+ async function singleHostDraft(task, host, context) {
150
+ const draft = await context.runHost(host, 'proposal', { task, cwd: context.cwd });
151
+ return {
152
+ status: draft.ok ? 'degraded' : 'unavailable',
153
+ dual: false,
154
+ missing: HOSTS.filter((candidate) => candidate !== host),
155
+ draft: draft.ok ? draft.value : undefined,
156
+ learningPersisted: false,
157
+ verifiedOutcome: false,
158
+ };
159
+ }
160
+
161
+ export async function deliberate(task, options = {}) {
162
+ const probes = options.probes ?? probeSubscriptionHosts();
163
+ const runHost = options.runHost ?? ((host, stage, payload) => (
164
+ runSubscriptionHost(host, stage, payload, { cwd: options.cwd })
165
+ ));
166
+ const cwd = options.cwd ?? process.cwd();
167
+ const persist = options.persist ?? ((receipt) => persistDeliberationReceipt(receipt, { cwd }));
168
+ const eligibleHosts = HOSTS.filter((host) => probes[hostKey(host)]?.eligible);
169
+
170
+ if (eligibleHosts.length === 0) {
171
+ return {
172
+ status: 'unavailable',
173
+ dual: false,
174
+ missing: missingHosts(probes),
175
+ learningPersisted: false,
176
+ verifiedOutcome: false,
177
+ };
178
+ }
179
+ if (eligibleHosts.length === 1) {
180
+ return singleHostDraft(task, eligibleHosts[0], { cwd, runHost });
181
+ }
182
+
183
+ const proposalResults = await Promise.all(HOSTS.map((host) => (
184
+ runHost(host, 'proposal', { task, cwd })
185
+ )));
186
+ const successfulProposals = HOSTS.flatMap((host, index) => (
187
+ proposalResults[index].ok ? [{ host, value: proposalResults[index].value }] : []
188
+ ));
189
+ if (successfulProposals.length < 2) {
190
+ const host = successfulProposals[0]?.host;
191
+ if (!host) {
192
+ return {
193
+ status: 'unavailable',
194
+ dual: false,
195
+ missing: HOSTS,
196
+ learningPersisted: false,
197
+ verifiedOutcome: false,
198
+ };
199
+ }
200
+ return {
201
+ status: 'degraded',
202
+ dual: false,
203
+ missing: HOSTS.filter((candidate) => candidate !== host),
204
+ draft: successfulProposals[0].value,
205
+ learningPersisted: false,
206
+ verifiedOutcome: false,
207
+ };
208
+ }
209
+
210
+ const proposals = Object.fromEntries(successfulProposals.map(({ host, value }) => [host, value]));
211
+ const critiques = Object.fromEntries(await Promise.all(HOSTS.map(async (host) => {
212
+ const other = HOSTS.find((candidate) => candidate !== host);
213
+ const result = await runHost(host, 'critique', { task, proposal: proposals[other] });
214
+ return [host, result.ok ? result.value : { unavailable: true }];
215
+ })));
216
+ const roles = chooseRoles(task);
217
+ const synthesis = await runHost(roles.scribe, 'synthesis', { task, proposals, critiques });
218
+ if (!synthesis.ok) {
219
+ return {
220
+ status: 'unresolved',
221
+ dual: true,
222
+ roles,
223
+ verifiedOutcome: false,
224
+ learningPersisted: false,
225
+ };
226
+ }
227
+
228
+ let artifact = synthesis.value;
229
+ let verification = await runHost(roles.verifier, 'verify', { task, artifact });
230
+ if (verification.ok && verification.value?.verdict === 'changes') {
231
+ const revision = await runHost(roles.scribe, 'revise', {
232
+ task,
233
+ artifact,
234
+ corrections: verification.value.corrections ?? [],
235
+ });
236
+ if (revision.ok) {
237
+ artifact = revision.value;
238
+ verification = await runHost(roles.verifier, 'reverify', { task, artifact });
239
+ }
240
+ }
241
+
242
+ const accepted = verification.ok && verification.value?.verdict === 'accept';
243
+ const receipt = {
244
+ protocol: 'dual-host-deliberation-v1',
245
+ taskHash: createHash('sha256').update(String(task)).digest('hex'),
246
+ hosts: HOSTS,
247
+ roles,
248
+ accepted,
249
+ };
250
+ const learningPersisted = accepted ? Boolean(await persist(receipt)) : false;
251
+ return {
252
+ status: accepted ? 'accepted' : 'unresolved',
253
+ dual: true,
254
+ roles,
255
+ artifact,
256
+ verification: verification.ok ? verification.value : undefined,
257
+ verifiedOutcome: accepted,
258
+ learningPersisted,
259
+ };
260
+ }
261
+
262
+ export async function main(argv = process.argv.slice(2), {
263
+ deliberateFn = deliberate,
264
+ stdout = process.stdout,
265
+ stderr = process.stderr,
266
+ } = {}) {
267
+ const task = argv.join(' ').trim();
268
+ if (!task) {
269
+ stderr.write('Usage: dual-host-deliberation.mjs "<hard problem>"\n');
270
+ return 64;
271
+ }
272
+ try {
273
+ const result = await deliberateFn(task);
274
+ stdout.write(`${JSON.stringify(result, null, 2)}\n`);
275
+ return result.status === 'accepted' ? 0 : result.status === 'unavailable' ? 3 : 2;
276
+ } catch (error) {
277
+ stderr.write(`Dual-host deliberation failed: ${error.message}\n`);
278
+ return 1;
279
+ }
280
+ }
281
+
282
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
283
+ process.exitCode = await main();
284
+ }
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { fileURLToPath } from 'node:url';
4
+ import { hardProblem } from './dual-host-deliberation.mjs';
5
+ import { probeSubscriptionHosts } from './subscription-hosts.mjs';
6
+
7
+ const HOSTS = Object.freeze([
8
+ ['claude', 'claude-code'],
9
+ ['codex', 'codex'],
10
+ ]);
11
+
12
+ export function recommendDualHost(task, options = {}) {
13
+ if (!hardProblem(task)) {
14
+ return {
15
+ action: 'single-host',
16
+ hardProblem: false,
17
+ hosts: [],
18
+ missing: [],
19
+ billing: 'subscription-only',
20
+ apiKeyFallback: false,
21
+ };
22
+ }
23
+
24
+ const probes = options.probes ?? probeSubscriptionHosts();
25
+ const hosts = HOSTS
26
+ .filter(([key]) => probes[key]?.eligible)
27
+ .map(([, host]) => host);
28
+ const missing = HOSTS
29
+ .filter(([key]) => !probes[key]?.eligible)
30
+ .map(([, host]) => host);
31
+
32
+ return {
33
+ action: missing.length === 0 ? 'duel' : 'login-required',
34
+ hardProblem: true,
35
+ hosts,
36
+ missing,
37
+ billing: 'subscription-only',
38
+ apiKeyFallback: false,
39
+ };
40
+ }
41
+
42
+ export function main(argv = process.argv.slice(2), {
43
+ probes,
44
+ stdout = process.stdout,
45
+ stderr = process.stderr,
46
+ } = {}) {
47
+ const task = argv.join(' ').trim();
48
+ if (!task) {
49
+ stderr.write('Usage: dual-host-suggest.mjs "<task>"\n');
50
+ return 64;
51
+ }
52
+ stdout.write(`${JSON.stringify(recommendDualHost(task, { probes }), null, 2)}\n`);
53
+ return 0;
54
+ }
55
+
56
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
57
+ process.exitCode = main();
58
+ }