@hecer/yoke 1.18.0 → 1.19.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/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +19 -0
- package/README.md +83 -910
- package/canon/manifest.yaml +1 -1
- package/dist/agents/process.js +42 -11
- package/dist/agents/providers.js +11 -2
- package/dist/agents/sol-pi-runtime.js +76 -0
- package/dist/code-intelligence/adapters/graphify.js +1 -1
- package/dist/code-intelligence/adapters/mcp.js +25 -4
- package/dist/code-intelligence/adapters/serena.js +1 -1
- package/dist/code-intelligence/coordinator.js +2 -1
- package/dist/code-intelligence/evidence.js +12 -1
- package/dist/code-intelligence/mcp-client.js +2 -2
- package/dist/dashboard/page.js +1 -1
- package/dist/dashboard/panels.js +25 -3
- package/dist/dashboard/server.js +62 -1
- package/dist/retrofit/config.js +28 -0
- package/docs/HARNESSES.md +1 -1
- package/docs/SOL-PI.md +41 -0
- package/docs/code-intelligence-stdio-fix-2026-09-27.md +8 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
package/canon/manifest.yaml
CHANGED
package/dist/agents/process.js
CHANGED
|
@@ -6,6 +6,7 @@ import { createBoundedOutput, createTelemetryAccumulator } from './process-strea
|
|
|
6
6
|
import { processIncarnation } from './process-incarnation.js';
|
|
7
7
|
import { prepareWindowsInvocation, resolveWindowsCommand } from './windows-launch.js';
|
|
8
8
|
import { createSupervision, supervisionLimits, assertPreviousProvidersStopped } from './supervision.js';
|
|
9
|
+
import { prepareSolPiTemporaryConfig } from './sol-pi-runtime.js';
|
|
9
10
|
function cancellationReason(signal) {
|
|
10
11
|
return typeof signal.reason === 'string' && signal.reason.length > 0
|
|
11
12
|
? signal.reason
|
|
@@ -26,25 +27,48 @@ export function startProviderProcess(agent, invocation, options = {}) {
|
|
|
26
27
|
let progress = () => { };
|
|
27
28
|
const limits = supervisionLimits(invocation.cwd);
|
|
28
29
|
const supervision = createSupervision(invocation.cwd, reason => failure(reason), () => progress(), options.attempt);
|
|
30
|
+
let restoreSolPiConfig = () => { };
|
|
29
31
|
let prepared;
|
|
30
32
|
try {
|
|
33
|
+
if (agent === 'pi')
|
|
34
|
+
restoreSolPiConfig = prepareSolPiTemporaryConfig(invocation.cwd);
|
|
31
35
|
assertPreviousProvidersStopped(invocation.cwd);
|
|
32
36
|
prepared = process.platform === 'win32' ? prepareWindowsInvocation(invocation) : { command: invocation.command, args: invocation.args, env: process.env };
|
|
33
37
|
}
|
|
34
38
|
catch (error) {
|
|
35
|
-
|
|
39
|
+
let message = error.message;
|
|
40
|
+
try {
|
|
41
|
+
restoreSolPiConfig();
|
|
42
|
+
}
|
|
43
|
+
catch (cleanupError) {
|
|
44
|
+
message = cleanupError.message;
|
|
45
|
+
}
|
|
36
46
|
supervision.stop(message);
|
|
37
47
|
return { pid: undefined, invocation, recordPath: '', cancel: () => false, completion: Promise.resolve({ kind: 'spawn-failed', error: message, invocation, pid: undefined, stdout: '', stderr: '', stdoutTruncated: false, stderrTruncated: false, telemetry: { usageAvailable: false } }) };
|
|
38
48
|
}
|
|
39
49
|
const spawnOptions = { ...prepared, cwd: invocation.cwd, shell: false, detached: process.platform !== 'win32' };
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
let child;
|
|
51
|
+
try {
|
|
52
|
+
child = spawn(spawnOptions.command, [...spawnOptions.args], {
|
|
53
|
+
cwd: spawnOptions.cwd,
|
|
54
|
+
shell: spawnOptions.shell,
|
|
55
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
56
|
+
detached: spawnOptions.detached,
|
|
57
|
+
env: prepared.env,
|
|
58
|
+
windowsHide: true,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
let message = error.message;
|
|
63
|
+
try {
|
|
64
|
+
restoreSolPiConfig();
|
|
65
|
+
}
|
|
66
|
+
catch (cleanupError) {
|
|
67
|
+
message = cleanupError.message;
|
|
68
|
+
}
|
|
69
|
+
supervision.stop(message);
|
|
70
|
+
return { pid: undefined, invocation, recordPath: '', cancel: () => false, completion: Promise.resolve({ kind: 'spawn-failed', error: message, invocation, pid: undefined, stdout: '', stderr: '', stdoutTruncated: false, stderrTruncated: false, telemetry: { usageAvailable: false } }) };
|
|
71
|
+
}
|
|
48
72
|
const targetDir = resolve(invocation.cwd);
|
|
49
73
|
const pid = child.pid;
|
|
50
74
|
const startedAt = pid === undefined ? `unverified:${new Date().toISOString()}` : processIncarnation(pid) ?? `unverified:${new Date().toISOString()}`;
|
|
@@ -130,11 +154,18 @@ export function startProviderProcess(agent, invocation, options = {}) {
|
|
|
130
154
|
return;
|
|
131
155
|
settled = true;
|
|
132
156
|
clearTimers();
|
|
133
|
-
|
|
157
|
+
let completedResult = result;
|
|
158
|
+
try {
|
|
159
|
+
restoreSolPiConfig();
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
completedResult = { ...evidence(), kind: 'spawn-failed', error: error.message };
|
|
163
|
+
}
|
|
164
|
+
supervision.stop(termination?.reason ?? (completedResult.kind === 'succeeded' ? 'provider-exited' : 'provider-failed'), !termination || terminationConfirmed);
|
|
134
165
|
options.signal?.removeEventListener('abort', onAbort);
|
|
135
166
|
if (!termination || terminationConfirmed)
|
|
136
167
|
removeRecord();
|
|
137
|
-
resolveCompletion(
|
|
168
|
+
resolveCompletion(completedResult);
|
|
138
169
|
};
|
|
139
170
|
const evidence = () => ({
|
|
140
171
|
invocation,
|
package/dist/agents/providers.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ModelSelectionSchema } from './contracts.js';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { loadSolPiProjectConfig } from './sol-pi-runtime.js';
|
|
4
|
+
const SOL_PI_EXTENSION = 'git:github.com/NVlabs/SoL-Pi@d7ecfc089944f0d04b80122a0a9a6ca0d786f3d0';
|
|
3
5
|
export { providerSpawnOptions, startProviderProcess, } from './process.js';
|
|
4
6
|
const argsFor = (agent, permissions) => {
|
|
5
7
|
if (agent === 'claude') {
|
|
@@ -76,6 +78,13 @@ export function buildProviderInvocation(agent, prompt, cwd, permissions = 'safe'
|
|
|
76
78
|
if (parsedSelection.variant && !['opencode', 'kilo', 'pi', 'hermes'].includes(agent))
|
|
77
79
|
throw new Error(`${agent} does not support a model variant selection`);
|
|
78
80
|
const args = argsFor(agent, permissions);
|
|
81
|
+
if (agent === 'pi' && loadSolPiProjectConfig(cwd)?.solpi?.enabled) {
|
|
82
|
+
const [major, minor] = process.version.slice(1).split('.').map(Number);
|
|
83
|
+
if (major < 22 || (major === 22 && minor < 19)) {
|
|
84
|
+
throw new Error(`SoL-Pi requires Node.js 22.19 or newer; current version is ${process.version}`);
|
|
85
|
+
}
|
|
86
|
+
args.push('--extension', SOL_PI_EXTENSION);
|
|
87
|
+
}
|
|
79
88
|
if (output.schemaFile !== undefined || output.jsonSchema !== undefined) {
|
|
80
89
|
if (agent === 'codex' && output.schemaFile && output.jsonSchema === undefined) {
|
|
81
90
|
if (/[\0\r\n]/u.test(output.schemaFile) || (process.platform === 'win32' && !/^[A-Za-z0-9_./:\\-]+$/u.test(output.schemaFile)))
|
|
@@ -119,7 +128,7 @@ export function buildProviderInvocation(agent, prompt, cwd, permissions = 'safe'
|
|
|
119
128
|
else if (agent === 'pi')
|
|
120
129
|
args.push('--thinking', parsedSelection.reasoningEffort);
|
|
121
130
|
else if (agent === 'hermes')
|
|
122
|
-
args.push('--reasoning
|
|
131
|
+
args.push('--reasoning', parsedSelection.reasoningEffort);
|
|
123
132
|
}
|
|
124
133
|
if (parsedSelection.variant) {
|
|
125
134
|
if ((agent === 'opencode' || agent === 'kilo') && !parsedSelection.reasoningEffort)
|
|
@@ -134,7 +143,7 @@ export function buildProviderInvocation(agent, prompt, cwd, permissions = 'safe'
|
|
|
134
143
|
if (parsedSelection.reasoningEffort && parsedSelection.reasoningEffort !== parsedSelection.variant)
|
|
135
144
|
throw new Error('Hermes reasoningEffort and variant selections must match');
|
|
136
145
|
if (!parsedSelection.reasoningEffort)
|
|
137
|
-
args.push('--reasoning
|
|
146
|
+
args.push('--reasoning', parsedSelection.variant);
|
|
138
147
|
}
|
|
139
148
|
}
|
|
140
149
|
if (agent === 'codex' && parsedSelection.nativeMultiAgent === false)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { lstatSync, mkdirSync, readFileSync, rmSync, rmdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { loadConfig, toSolPiNativeConfig } from '../retrofit/config.js';
|
|
4
|
+
export function loadSolPiProjectConfig(directory) {
|
|
5
|
+
let current = resolve(directory);
|
|
6
|
+
while (true) {
|
|
7
|
+
const config = loadConfig(current);
|
|
8
|
+
if (config)
|
|
9
|
+
return config;
|
|
10
|
+
const parent = dirname(current);
|
|
11
|
+
if (parent === current)
|
|
12
|
+
return null;
|
|
13
|
+
current = parent;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function lstatIfPresent(path) {
|
|
17
|
+
try {
|
|
18
|
+
return lstatSync(path);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error.code === 'ENOENT')
|
|
22
|
+
return undefined;
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function assertNoConfigSymlinks(paths) {
|
|
27
|
+
for (const path of paths) {
|
|
28
|
+
if (lstatIfPresent(path)?.isSymbolicLink())
|
|
29
|
+
throw new Error('SoL-Pi refuses symbolic-link config paths: ' + path);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function removeCreatedDirectories(directories) {
|
|
33
|
+
for (const directory of [...directories].reverse()) {
|
|
34
|
+
try {
|
|
35
|
+
rmdirSync(directory);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
const code = error.code;
|
|
39
|
+
if (code !== 'ENOENT' && code !== 'ENOTEMPTY')
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function prepareSolPiTemporaryConfig(workspace) {
|
|
45
|
+
const config = loadSolPiProjectConfig(workspace);
|
|
46
|
+
if (!config?.solpi?.enabled)
|
|
47
|
+
return () => { };
|
|
48
|
+
const piDirectory = resolve(workspace, '.pi');
|
|
49
|
+
const solPiConfigPath = join(piDirectory, 'sol-pi.json');
|
|
50
|
+
const configPaths = [piDirectory, solPiConfigPath];
|
|
51
|
+
assertNoConfigSymlinks(configPaths);
|
|
52
|
+
const existingConfig = lstatIfPresent(solPiConfigPath) ? readFileSync(solPiConfigPath) : undefined;
|
|
53
|
+
const createdDirectories = [];
|
|
54
|
+
const restore = () => {
|
|
55
|
+
assertNoConfigSymlinks(configPaths);
|
|
56
|
+
if (existingConfig === undefined)
|
|
57
|
+
rmSync(solPiConfigPath, { force: true });
|
|
58
|
+
else
|
|
59
|
+
writeFileSync(solPiConfigPath, existingConfig);
|
|
60
|
+
removeCreatedDirectories(createdDirectories);
|
|
61
|
+
};
|
|
62
|
+
try {
|
|
63
|
+
for (const directory of [piDirectory]) {
|
|
64
|
+
if (!lstatIfPresent(directory)) {
|
|
65
|
+
mkdirSync(directory);
|
|
66
|
+
createdDirectories.push(directory);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
writeFileSync(solPiConfigPath, JSON.stringify(toSolPiNativeConfig(config)), { flag: existingConfig === undefined ? 'wx' : 'w' });
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
restore();
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
return restore;
|
|
76
|
+
}
|
|
@@ -2,7 +2,7 @@ import { McpBackendAdapter } from './mcp.js';
|
|
|
2
2
|
export const GRAPHIFY_VERSION = '0.9.56';
|
|
3
3
|
export function createGraphifyAdapter(cwd, override) {
|
|
4
4
|
return new McpBackendAdapter({
|
|
5
|
-
name: 'graphify', version: GRAPHIFY_VERSION, cwd, command: override?.command ?? 'python', args: override?.args ?? ['-m', 'graphify.serve'], framing: '
|
|
5
|
+
name: 'graphify', version: GRAPHIFY_VERSION, cwd, command: override?.command ?? 'python', args: override?.args ?? ['-m', 'graphify.serve'], framing: 'line', semantic: false, documents: true,
|
|
6
6
|
aliases: { context: 'query_graph', symbol: 'get_node', neighbors: 'get_neighbors', trace: 'shortest_path', impact: 'get_pr_impact' },
|
|
7
7
|
});
|
|
8
8
|
}
|
|
@@ -2,16 +2,37 @@ import { McpStdioClient } from '../mcp-client.js';
|
|
|
2
2
|
function valueFromResult(value) {
|
|
3
3
|
if (!value || typeof value !== 'object')
|
|
4
4
|
return value;
|
|
5
|
-
if (value.
|
|
6
|
-
|
|
5
|
+
if (value.isError === true)
|
|
6
|
+
throw new Error('MCP backend tool returned an error');
|
|
7
|
+
if (value.structuredContent !== undefined) {
|
|
8
|
+
const structured = value.structuredContent;
|
|
9
|
+
if (structured && typeof structured === 'object' && Object.keys(structured).length === 1 && typeof structured.result === 'string') {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(structured.result);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return structured.result;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return structured;
|
|
18
|
+
}
|
|
7
19
|
const text = Array.isArray(value.content) ? value.content.find((item) => item?.type === 'text')?.text : undefined;
|
|
8
20
|
if (typeof text !== 'string')
|
|
9
21
|
return value;
|
|
10
22
|
try {
|
|
11
|
-
|
|
23
|
+
const parsed = JSON.parse(text);
|
|
24
|
+
if (parsed && typeof parsed === 'object' && Object.keys(parsed).length === 1 && typeof parsed.result === 'string') {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(parsed.result);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return parsed.result;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return parsed;
|
|
12
33
|
}
|
|
13
34
|
catch {
|
|
14
|
-
return
|
|
35
|
+
return text;
|
|
15
36
|
}
|
|
16
37
|
}
|
|
17
38
|
export class McpBackendAdapter {
|
|
@@ -2,7 +2,7 @@ import { McpBackendAdapter } from './mcp.js';
|
|
|
2
2
|
export const SERENA_VERSION = '1.7.1-dev';
|
|
3
3
|
export function createSerenaAdapter(cwd, override) {
|
|
4
4
|
return new McpBackendAdapter({
|
|
5
|
-
name: 'serena-lsp', version: SERENA_VERSION, cwd, command: override?.command ?? 'serena', args: override?.args ?? ['start-mcp-server', '--project-from-cwd', '--context', 'codex'], framing: '
|
|
5
|
+
name: 'serena-lsp', version: SERENA_VERSION, cwd, command: override?.command ?? 'serena', args: override?.args ?? ['start-mcp-server', '--project-from-cwd', '--context', 'codex'], framing: 'line', semantic: true, documents: false,
|
|
6
6
|
aliases: {
|
|
7
7
|
symbol: 'find_symbol', references: 'find_referencing_symbols', implementations: 'find_implementations', overview: 'get_symbols_overview', diagnostics: 'get_diagnostics_for_file',
|
|
8
8
|
rename: 'rename_symbol', replace_symbol_body: 'replace_symbol_body', insert_before_symbol: 'insert_before_symbol', insert_after_symbol: 'insert_after_symbol', safe_delete: 'safe_delete_symbol',
|
|
@@ -185,9 +185,10 @@ export class CodeIntelligenceCoordinator {
|
|
|
185
185
|
const name = input.symbol_id ?? input.query;
|
|
186
186
|
const reference = symbolArgs(name, input.path ?? '');
|
|
187
187
|
const graft = await this.call('graft', 'context', { query: name, limit: 10, ...(input.path ? { in: input.path } : {}) }, input.timeout_ms, state);
|
|
188
|
+
// Graft returns search prose, not structured symbol definitions. Preserve
|
|
189
|
+
// its provenance without inventing a symbol from the response heading.
|
|
188
190
|
if (graft !== null) {
|
|
189
191
|
const normalized = normalizeItems(graft, this.evidenceOptions(snapshot, 'graft'), 'symbol', 20);
|
|
190
|
-
symbols.push(...normalized.items.map(item => ({ symbol_id: `graft:${item.item_id}`, name: item.excerpt.split(/\s|\(|\{/u)[0] ?? name, path: item.path, kind: 'unknown', signature: item.excerpt.slice(0, 500), evidence_ids: item.evidence_ids })));
|
|
191
192
|
provenance.push(...normalized.provenance);
|
|
192
193
|
}
|
|
193
194
|
const symbol = await this.call('serena-lsp', 'symbol', { name_path_pattern: reference.name_path, depth: 0, relative_path: reference.relative_path, include_body: false, max_matches: 20 }, input.timeout_ms, state);
|
|
@@ -12,7 +12,7 @@ function textOf(raw) {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
function locate(text, root) {
|
|
15
|
-
const match = /(?:^|[\s("'`])((?:[A-Za-z0-9_.@-]+[\\/])*[A-Za-z0-9_.@-]+\.[A-Za-z0-9]+):(\d+)/u.exec(text);
|
|
15
|
+
const match = /(?:^|[\s("'`])((?:[A-Za-z0-9_.@-]+[\\/])*[A-Za-z0-9_.@-]+\.[A-Za-z0-9]+):L?(\d+)/u.exec(text);
|
|
16
16
|
if (!match)
|
|
17
17
|
return { path: null, line: null };
|
|
18
18
|
try {
|
|
@@ -44,6 +44,17 @@ export function normalizeItems(raw, options, kind = 'code', limit = 100) {
|
|
|
44
44
|
if (!excerpt)
|
|
45
45
|
continue;
|
|
46
46
|
const p = makeProvenance(options, excerpt, excerpt.toLowerCase().includes('ambiguous') ? 'ambiguous' : 'resolved');
|
|
47
|
+
if (candidate && typeof candidate === 'object' && typeof candidate.relative_path === 'string') {
|
|
48
|
+
try {
|
|
49
|
+
p.source_path = assertSafePath(options.root, candidate.relative_path);
|
|
50
|
+
p.content_hash = options.snapshot.files.find(file => file.path === p.source_path)?.hash ?? null;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
p.source_path = null;
|
|
54
|
+
p.content_hash = null;
|
|
55
|
+
p.resolution = 'unresolved';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
47
58
|
const id = evidenceId(p, excerpt);
|
|
48
59
|
provenance.push(p);
|
|
49
60
|
items.push({ item_id: `item-${hash(excerpt).slice(0, 24)}`, kind, path: p.source_path, excerpt, evidence_ids: [id], rank: items.length });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
/** Small, bounded stdio MCP client.
|
|
3
|
-
* Content-Length framing
|
|
2
|
+
/** Small, bounded stdio MCP client. Supports newline JSON-RPC used by the
|
|
3
|
+
* backend SDKs and optional Content-Length framing for legacy peers. */
|
|
4
4
|
export class McpStdioClient {
|
|
5
5
|
command;
|
|
6
6
|
args;
|
package/dist/dashboard/page.js
CHANGED
|
@@ -33,7 +33,7 @@ function validDate(value){if(!/^\\d{4}-\\d{2}-\\d{2}$/u.test(value))return false
|
|
|
33
33
|
function parseNavigationHash(hash){
|
|
34
34
|
const params=new URLSearchParams(String(hash||'').replace(/^#/u,''));const screen=params.get('screen')||'overview',project=params.get('project'),view=params.get('view')||'now',period=Number(params.get('period')||30),group=params.get('group')||'day',from=params.get('from')||'',to=params.get('to')||'',query=params.get('query')||'',status=params.get('status')||'all',sort=params.get('sort')||'attention';
|
|
35
35
|
const datesValid=(!from&&!to)||(validDate(from)&&validDate(to)&&from<=to&&Date.parse(to+'T00:00:00Z')-Date.parse(from+'T00:00:00Z')<=365*86400000);
|
|
36
|
-
if(!['overview','workspace','project'].includes(screen)||!['now','usage','results','history'].includes(view)||![1,7,30,90,365].includes(period)||!['day','week','month'].includes(group)||!['all','active','attention'].includes(status)||!['attention','activity','tokens','cost','acceptance','name'].includes(sort)||query.length>200||!datesValid||(screen==='project'&&!/^[a-f0-9]{32}$/u.test(project||'')))return {...defaultNavigation};
|
|
36
|
+
if(!['overview','workspace','project'].includes(screen)||!['now','usage','results','history','settings'].includes(view)||![1,7,30,90,365].includes(period)||!['day','week','month'].includes(group)||!['all','active','attention'].includes(status)||!['attention','activity','tokens','cost','acceptance','name'].includes(sort)||query.length>200||!datesValid||(screen==='project'&&!/^[a-f0-9]{32}$/u.test(project||'')))return {...defaultNavigation};
|
|
37
37
|
const state={screen,project:screen==='project'?project:null,view,period,group,from,to};if(params.has('query'))state.query=query;if(params.has('status'))state.status=status;if(params.has('sort'))state.sort=sort;return state;
|
|
38
38
|
}
|
|
39
39
|
function serializeNavigationHash(state){const params=new URLSearchParams({screen:state.screen||'overview',view:state.view||'now',period:String(state.period||30),group:state.group||'day'});if(state.project)params.set('project',state.project);if(state.from&&state.to){params.set('from',state.from);params.set('to',state.to)}if(state.query)params.set('query',state.query);if(state.status&&state.status!=='all')params.set('status',state.status);if(state.sort&&state.sort!=='attention')params.set('sort',state.sort);return '#'+params.toString()}
|
package/dist/dashboard/panels.js
CHANGED
|
@@ -125,25 +125,47 @@ function renderUsageHistory(current,previous,project={}){
|
|
|
125
125
|
}
|
|
126
126
|
function renderResults(p,a){const t=a.total;content.append(append(el('div',undefined,'metrics'),metric('Accepted in period',t.accepted,'Recorded acceptance events'),metric('Repair phases',t.repairs,'Routing escalations: '+t.escalations),metric('Tokens / acceptance',rate(t.tokensPerAccepted),'Reported input + output, including failed work')));const attempts=panel('Measured outcomes');attempts.append(el('p','Ended attempts: '+t.attempts+' · explicitly successful attempts: '+t.successfulAttempts+' · average summed attempt time per acceptance: '+duration(t.timePerAcceptedMs)));attempts.append(el('p','Worker termination alone is not counted as successful acceptance. Rates only describe recorded activity in the selected period.','muted'));attempts.append(table(['Task','Attempts','Accepted','Repairs','Escalations','Tokens / acceptance'],a.tasks.map(v=>[v.storyId,v.attempts,v.accepted,v.repairs,v.escalations,rate(v.tokensPerAccepted)])));content.append(attempts);const checks=panel('Latest saved acceptance evidence');if(p.check){checks.append(el('p',p.check.summary));for(const c of p.check.criteria){const item=row(c.text,c.id,c.status);item.firstChild.append(append(el('details'),el('summary','View evidence'),el('p',c.summary)));checks.append(item)}checks.append(el('p','Checked '+p.check.generatedAt+' · historical evidence; independent of the selected statistics period.','muted'))}else checks.append(el('p','No saved independent check.','empty'));content.append(checks);const goals=panel('Saved goal attempts');for(const attempt of p.goal?.attempts||[])goals.append(row(attempt.provider||'Agent',(attempt.summary||'')+' · '+duration(attempt.durationMs)+' · input '+tokenText(attempt.inputTokens)+' / output '+tokenText(attempt.outputTokens),attempt.success?'finished':'failed'));content.append(goals)}
|
|
127
127
|
function renderHistoryExplorer(project,history){const state=projectStatus(project),view=panel('History explorer');view.append(el('p','Recorded events and completed runs in the selected UTC window. The list is bounded to the latest 100 events.','muted'));const events=timelineEvents(history.events||[]);if(!events.length)view.append(el('p','No recorded history in this period.','empty'));else{const list=el('div',undefined,'timeline');for(const event of events)list.append(timelineItem(event));view.append(list)}const runs=panel('Run history');if(!(history.runs||[]).length)runs.append(el('p','No recorded runs in this period.','empty'));else runs.append(table(['Run','Started','Ended','Agent','Provider','Model','Tokens','Accepted','Outcome'],history.runs.map(run=>[dimensionLabel(run.runId),run.startedAt||'Not reported',run.endedAt||'Not reported',dimensionLabel(run.agent),dimensionLabel(run.provider),dimensionLabel(run.model),rankingText(run.total?.tokens?.total),rankingText(run.total?.outcomes?.accepted),rankingText(run.total?.outcomes?.successfulAttempts)])));content.append(renderCoverage(history,{stale:state.stale,unavailable:false}),runs,view)}
|
|
128
|
+
function solPiRatioError(value){const text=String(value);const ratio=Number(text);return text.trim()===''||!Number.isFinite(ratio)||ratio<0?'Enter a finite cache write/read ratio of 0 or more.':null}
|
|
129
|
+
async function saveSolPiSettings(projectId,settings,feedback){
|
|
130
|
+
const error=solPiRatioError(settings.cacheWriteReadRatio);if(error){feedback.className='settings-feedback notice';feedback.textContent=error;return}
|
|
131
|
+
feedback.className='settings-feedback muted';feedback.textContent='Saving settings…';
|
|
132
|
+
try{await api('/api/projects/'+projectId+'/settings',{method:'POST',headers:{'content-type':'application/json','x-yoke-token':sessionToken},body:JSON.stringify({...settings,cacheWriteReadRatio:Number(settings.cacheWriteReadRatio)})});feedback.className='settings-feedback muted';feedback.textContent='Settings saved.'}
|
|
133
|
+
catch(error){feedback.className='settings-feedback notice';feedback.textContent='Could not save settings: '+error.message}
|
|
134
|
+
}
|
|
135
|
+
function renderSolPiSettings(project,settings){
|
|
136
|
+
const view=panel('SoL-Pi settings');view.append(el('p','SoL-Pi is off by default. Change the master switch and any mechanism switches, then save.','muted'));
|
|
137
|
+
const controls={},toggles=[['enabled','Enable SoL-Pi (off by default)'],['actionFusion','Action fusion'],['observationPack','Observation packing'],['evidencePreservingReducer','Evidence-preserving reducer'],['onlineContextCompact','Online context compaction']];
|
|
138
|
+
for(const [key,label] of toggles){const input=el('input');input.type='checkbox';input.checked=settings[key]===true;input.setAttribute('role','switch');input.setAttribute('aria-label',label);controls[key]=input;view.append(append(el('label',undefined,'setting-toggle'),input,el('span',label)))}
|
|
139
|
+
const ratioLabel=el('label',undefined,'field'),ratio=el('input');ratio.type='number';ratio.min='0';ratio.step='any';ratio.value=String(settings.cacheWriteReadRatio);ratio.setAttribute('aria-label','Cache write/read ratio');ratioLabel.append(el('span','Cache write/read ratio','label'),ratio);view.append(ratioLabel);
|
|
140
|
+
view.append(el('p','The pinned SoL-Pi extension executes inside Pi. Review the pinned SoL-Pi extension before enabling it.','muted'));
|
|
141
|
+
view.append(el('p','SoL-Pi needs a compatible Pi runtime installed and configured; Yoke does not install or authenticate Pi. For headless runs, Pi requires explicit project trust before it loads project-local settings. Open Pi, run /trust, then restart. Yoke does not grant trust.','muted'));
|
|
142
|
+
view.append(el('p','The evidence-preserving reducer may make a model call using diagnostic logs; those logs can contain project data. Review the selected provider and model before enabling it.','muted'));
|
|
143
|
+
const feedback=el('p','Save your changes to apply them.','settings-feedback muted');feedback.setAttribute('role','status');feedback.setAttribute('aria-live','polite');view.append(feedback);
|
|
144
|
+
view.append(button('Save settings',()=>saveSolPiSettings(project.id,{enabled:controls.enabled.checked,actionFusion:controls.actionFusion.checked,observationPack:controls.observationPack.checked,evidencePreservingReducer:controls.evidencePreservingReducer.checked,onlineContextCompact:controls.onlineContextCompact.checked,cacheWriteReadRatio:ratio.value},feedback),true));
|
|
145
|
+
return view
|
|
146
|
+
}
|
|
128
147
|
async function showProject(id){
|
|
129
148
|
const request=beginRequest();selected=id;nav();
|
|
130
149
|
try{
|
|
131
150
|
const bounds=periodWindows(Date.now(),periodDays,customFrom,customTo),detailRequest=api('/api/projects/'+id,{},request.signal);
|
|
132
|
-
let project,current=null,previous=null,history=null;
|
|
151
|
+
let project,current=null,previous=null,history=null,settings=null;
|
|
133
152
|
if(projectView==='usage'){
|
|
134
153
|
[project,current,previous]=await Promise.all([detailRequest,api('/api/projects/'+id+'/analytics?'+queryPeriod(bounds.current),{},request.signal),api('/api/projects/'+id+'/analytics?'+queryPeriod(bounds.previous),{},request.signal)]);
|
|
135
154
|
}else if(projectView==='results'){
|
|
136
155
|
[project,current]=await Promise.all([detailRequest,api('/api/projects/'+id+'/analytics?'+queryPeriod(bounds.current),{},request.signal)]);
|
|
137
156
|
}else if(projectView==='history'){
|
|
138
157
|
[project,history]=await Promise.all([detailRequest,api('/api/projects/'+id+'/history?'+queryPeriod(bounds.current)+'&limit=100',{},request.signal)]);
|
|
158
|
+
}else if(projectView==='settings'){
|
|
159
|
+
[project,settings]=await Promise.all([detailRequest,api('/api/projects/'+id+'/settings',{},request.signal)]);
|
|
139
160
|
}else project=await detailRequest;
|
|
140
161
|
if(!currentRequest(request.version)||navigationState.screen!=='project'||selected!==id)return;
|
|
141
162
|
lastSuccessfulSync=new Date().toISOString();heading(project.name,project.goal?.objective||'Saved project work');content.append(el('p',project.root,'path'));for(const error of project.errors)content.append(el('p',error,'notice'));
|
|
142
|
-
const tabs=el('div',undefined,'actions');for(const [view,label] of [['now','Now'],['usage','Usage & time'],['results','Results'],['history','History']]){const control=button(label,()=>setNavigation({view}),projectView===view);control.setAttribute('aria-pressed',String(projectView===view));tabs.append(control)}
|
|
163
|
+
const tabs=el('div',undefined,'actions');for(const [view,label] of [['now','Now'],['usage','Usage & time'],['results','Results'],['history','History'],['settings','Settings']]){const control=button(label,()=>setNavigation({view}),projectView===view);control.setAttribute('aria-pressed',String(projectView===view));tabs.append(control)}
|
|
143
164
|
content.append(tabs);
|
|
144
165
|
if(projectView==='now'){
|
|
145
166
|
const tasks=project.stories||[],state=projectStatus(project);content.append(append(el('div',undefined,'metrics'),metric('Accepted tasks',tasks.filter(task=>task.passes).length+' / '+tasks.length,'Saved acceptance state'),metric('Remaining time',project.estimate?.available?duration(project.estimate.lowerMs)+' – '+duration(project.estimate.upperMs):'Unknown',project.estimate?.available?project.estimate.sampleCount+' samples · empirical range':'No reliable duration history'),metric('Project status',state.primary,state.goal&&state.loop&&state.goal!==state.loop?'Goal '+state.goal+' · loop '+(state.stale?'unconfirmed':state.loop):'Latest reported state')));renderNow(project);
|
|
146
|
-
}else
|
|
167
|
+
}else if(projectView==='settings')content.append(renderSolPiSettings(project,settings));
|
|
168
|
+
else{periodControls(id);if(projectView==='usage')renderUsageHistory(current,previous,projectStatus(project));else if(projectView==='history')renderHistoryExplorer(project,history);else renderResults(project,current)}
|
|
147
169
|
}catch(error){if(currentRequest(request.version)&&navigationState.screen==='project'&&selected===id&&!isAbort(error)){heading('Project unavailable','Offline or unavailable: '+error.message);content.append(renderCoverage({errors:[],coverage:'Historical analytics are unavailable for this project.'},{unavailable:true}))}}
|
|
148
170
|
}
|
|
149
171
|
if(typeof setInterval==='function')setInterval(()=>{if(selected&&projectView==='now'&&typeof document.hidden!=='undefined'&&!document.hidden&&!['INPUT','SELECT','TEXTAREA'].includes(document.activeElement?.tagName)&&!document.activeElement?.matches?.(':focus-visible'))showProject(selected)},5000);
|
package/dist/dashboard/server.js
CHANGED
|
@@ -4,6 +4,7 @@ import { lstatSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from '
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { parse } from 'yaml';
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
+
import { configPath, defaultConfig, loadConfig, resolveSolPiSettings, saveConfig, SolPiSettingsSchema } from '../retrofit/config.js';
|
|
7
8
|
import { listProjects } from './registry.js';
|
|
8
9
|
import { dashboardPage } from './page.js';
|
|
9
10
|
import { readEvents } from '../observability/events.js';
|
|
@@ -99,6 +100,28 @@ function writePauseSignal(root) {
|
|
|
99
100
|
throw error;
|
|
100
101
|
}
|
|
101
102
|
}
|
|
103
|
+
function assertSafeConfigTarget(root) {
|
|
104
|
+
const directory = join(root, '.yoke');
|
|
105
|
+
const file = configPath(root);
|
|
106
|
+
try {
|
|
107
|
+
const stat = lstatSync(directory);
|
|
108
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
109
|
+
throw new Error('Unsafe project settings target');
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (error.code !== 'ENOENT')
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const stat = lstatSync(file);
|
|
117
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
118
|
+
throw new Error('Unsafe project settings target');
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (error.code !== 'ENOENT')
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
102
125
|
function projectIsRunning(root) {
|
|
103
126
|
if (activeResumes.has(root))
|
|
104
127
|
return true;
|
|
@@ -213,13 +236,51 @@ export async function startDashboard(options = {}) {
|
|
|
213
236
|
send(200, workspaceAnalytics(listProjects(), period, sort));
|
|
214
237
|
return;
|
|
215
238
|
}
|
|
216
|
-
const match = /^\/api\/projects\/([a-f0-9]{32})(\/pause|\/resume|\/notes|\/changes|\/analytics|\/history|\/events)?$/u.exec(path);
|
|
239
|
+
const match = /^\/api\/projects\/([a-f0-9]{32})(\/pause|\/resume|\/notes|\/changes|\/settings|\/analytics|\/history|\/events)?$/u.exec(path);
|
|
217
240
|
if (match) {
|
|
218
241
|
const project = listProjects().find(project => project.id === match[1]);
|
|
219
242
|
if (!project) {
|
|
220
243
|
send(404, { error: 'Unknown project' });
|
|
221
244
|
return;
|
|
222
245
|
}
|
|
246
|
+
if (req.method === 'GET' && match[2] === '/settings') {
|
|
247
|
+
if (project.error) {
|
|
248
|
+
send(409, { error: project.error });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
assertSafeConfigTarget(project.root);
|
|
252
|
+
const config = loadConfig(project.root) ?? defaultConfig('0.0.0');
|
|
253
|
+
send(200, resolveSolPiSettings(config));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (req.method === 'POST' && match[2] === '/settings') {
|
|
257
|
+
let settings;
|
|
258
|
+
try {
|
|
259
|
+
settings = SolPiSettingsSchema.parse(await requestBody(req, {}));
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
send(error.message === 'Dashboard request body too large' ? 413 : 400, { error: error.message === 'Dashboard request body too large' ? error.message : 'Invalid project settings payload' });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (project.error) {
|
|
266
|
+
send(409, { error: project.error });
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
assertSafeConfigTarget(project.root);
|
|
271
|
+
const config = loadConfig(project.root) ?? defaultConfig('0.0.0');
|
|
272
|
+
saveConfig(project.root, { ...config, solpi: settings });
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
if (error.message === 'Unsafe project settings target') {
|
|
276
|
+
send(409, { error: error.message });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
send(200, settings);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
223
284
|
if (req.method === 'GET' && match[2] === '/analytics') {
|
|
224
285
|
if (project.error) {
|
|
225
286
|
send(409, { error: project.error });
|
package/dist/retrofit/config.js
CHANGED
|
@@ -32,6 +32,19 @@ const OutputPolicySchema = z.object({
|
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
});
|
|
35
|
+
const SolPiOptionsSchema = z.object({
|
|
36
|
+
actionFusion: z.boolean().default(false),
|
|
37
|
+
observationPack: z.boolean().default(false),
|
|
38
|
+
evidencePreservingReducer: z.boolean().default(false),
|
|
39
|
+
onlineContextCompact: z.boolean().default(false),
|
|
40
|
+
cacheWriteReadRatio: z.number().finite().nonnegative().default(12.5),
|
|
41
|
+
});
|
|
42
|
+
export const SolPiSettingsSchema = SolPiOptionsSchema.extend({ enabled: z.boolean().default(false) }).strict();
|
|
43
|
+
export const SolPiNativeConfigSchema = SolPiOptionsSchema.extend({
|
|
44
|
+
version: z.literal(1),
|
|
45
|
+
evidencePreservingReducerProvider: z.string().min(1).optional(),
|
|
46
|
+
evidencePreservingReducerModel: z.string().min(1).optional(),
|
|
47
|
+
}).strict();
|
|
35
48
|
const RoutingWorkerSchema = z.object({
|
|
36
49
|
id: z.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
37
50
|
agent: AgentSchema,
|
|
@@ -131,6 +144,7 @@ export const YokeConfigSchema = z.object({
|
|
|
131
144
|
smoke: SmokeSchema.optional(),
|
|
132
145
|
quality: ProjectQualityDefaultsSchema.optional(),
|
|
133
146
|
output: OutputPolicySchema.optional(),
|
|
147
|
+
solpi: SolPiSettingsSchema.optional(),
|
|
134
148
|
// Opt-in: upgrade yoke at loop START when a newer version is cached (never mid-run).
|
|
135
149
|
update: z.object({ auto: z.boolean() }).optional(),
|
|
136
150
|
});
|
|
@@ -143,6 +157,20 @@ export function resolveOutputPolicy(config) {
|
|
|
143
157
|
artifactThresholdBytes: config.output?.artifactThresholdBytes ?? DEFAULT_OUTPUT_POLICY.artifactThresholdBytes,
|
|
144
158
|
};
|
|
145
159
|
}
|
|
160
|
+
export function resolveSolPiSettings(config) {
|
|
161
|
+
return SolPiSettingsSchema.parse(config.solpi ?? {});
|
|
162
|
+
}
|
|
163
|
+
export function toSolPiNativeConfig(config) {
|
|
164
|
+
const settings = resolveSolPiSettings(config);
|
|
165
|
+
return SolPiNativeConfigSchema.parse({
|
|
166
|
+
version: 1,
|
|
167
|
+
actionFusion: settings.enabled && settings.actionFusion,
|
|
168
|
+
observationPack: settings.enabled && settings.observationPack,
|
|
169
|
+
evidencePreservingReducer: settings.enabled && settings.evidencePreservingReducer,
|
|
170
|
+
onlineContextCompact: settings.enabled && settings.onlineContextCompact,
|
|
171
|
+
cacheWriteReadRatio: settings.cacheWriteReadRatio,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
146
174
|
export function configPath(targetDir) {
|
|
147
175
|
return join(targetDir, '.yoke', 'config.yaml');
|
|
148
176
|
}
|
package/docs/HARNESSES.md
CHANGED
|
@@ -77,7 +77,7 @@ runner:
|
|
|
77
77
|
reasoningEffort: high
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
This becomes `--provider openrouter --model anthropic/claude-sonnet-4 --reasoning
|
|
80
|
+
This becomes `--provider openrouter --model anthropic/claude-sonnet-4 --reasoning high`. Hermes accepts `--reasoning`; configuring conflicting `variant` and `reasoningEffort` values is rejected.
|
|
81
81
|
|
|
82
82
|
The same fields are available on routing workers and quality critic/repair roles. Routing evidence is keyed by harness, provider, model, reasoning effort and variant, so a model profile does not inherit another profile's success history.
|
|
83
83
|
|
package/docs/SOL-PI.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# SoL-Pi with Yoke
|
|
2
|
+
|
|
3
|
+
Yoke can add NVIDIA's SoL-Pi extension to opted-in Pi invocations. The extension is off by default. This guide records the published evidence and the limits of Yoke's integration so you can decide whether to enable it.
|
|
4
|
+
|
|
5
|
+
## What the paper measured
|
|
6
|
+
|
|
7
|
+
The [SoL-Pi paper](https://arxiv.org/abs/2609.20519) reports benchmark runs on fixed tasks, models, harness configurations, and API prices. “Token traffic” below is recorded model-token traffic; scores are benchmark scores. The authors tested 51 publicly released tasks from EdgeBench, a subset of its 134 tasks.
|
|
8
|
+
|
|
9
|
+
On EdgeBench with GPT-5.6 Sol, the paper reports:
|
|
10
|
+
|
|
11
|
+
| Configuration | Recorded token traffic | API cost | Average score |
|
|
12
|
+
| --- | ---: | ---: | ---: |
|
|
13
|
+
| Pi baseline | 2.1538 billion | $1,339 | 44.833 |
|
|
14
|
+
| SoL-Pi Efficiency, all four mechanisms | 1.0990 billion (49.0% lower) | $894 (33.2% lower) | 42.003 (93.7% of Pi) |
|
|
15
|
+
| SoL-Pi Performance, ObservationPack alone | 2.0224 billion (6.1% lower) | $1,271 | 47.208 (5.3% above Pi) |
|
|
16
|
+
|
|
17
|
+
The complete efficiency configuration used fewer tokens and cost less in those runs, but scored lower than Pi. The separate performance configuration was the best-scoring single mechanism for GPT-5.6 Sol; it was not the full four-mechanism stack.
|
|
18
|
+
|
|
19
|
+
The paper also ran the complete stack with Opus 5, a backend not used to search for the mechanisms. It reports 44.7% less token traffic and 33.5% lower API cost than Pi ($1,158 versus $1,741), with an average score of 42.224 versus Pi's 44.756 (94.3% of Pi's score).
|
|
20
|
+
|
|
21
|
+
Results vary by benchmark. On 63 CPU-only Terminal-Bench 4 tasks, SoL-Pi solved 15 of 63 while Pi solved 18 of 63; total API cost was $211.12 versus $286.45 (26.3% lower), and cost per solved task was $14.07 versus $15.91 (11.6% lower). On six IMO 2026 problems, both systems passed three; SoL-Pi's reported cost per passed problem was $20.90 versus Pi's $25.32.
|
|
22
|
+
|
|
23
|
+
These are the paper's measurements, not measurements of Yoke's pinned extension build. They are not a project-level prediction or guarantee of savings. Your tasks, models, provider prices, cache billing, and enabled mechanisms can produce different cost and capability results. Yoke does not measure or promise a savings percentage for an individual project.
|
|
24
|
+
|
|
25
|
+
## Defaults and runtime
|
|
26
|
+
|
|
27
|
+
The `solpi.enabled` project setting defaults to `false`. Yoke adds the pinned SoL-Pi extension only to a Pi invocation when that setting is enabled. Each mechanism—`actionFusion`, `observationPack`, `evidencePreservingReducer`, and `onlineContextCompact`—also defaults to `false`; enabling the extension does not turn those mechanisms on. `cacheWriteReadRatio` defaults to `12.5` and is an input to the compaction decision, not a live price lookup or bill estimate.
|
|
28
|
+
|
|
29
|
+
Yoke pins SoL-Pi to [`d7ecfc089944f0d04b80122a0a9a6ca0d786f3d0`](https://github.com/NVlabs/SoL-Pi/tree/d7ecfc089944f0d04b80122a0a9a6ca0d786f3d0). The pinned upstream lists Node.js 22.19 or newer and `@earendil-works/pi-coding-agent@0.84.2` as its tested runtime baseline. Yoke checks the Node.js version but does not enforce the Pi version; other Pi releases are unverified. Yoke does not install Pi or set up provider authentication. Review the [pinned extension source](https://github.com/NVlabs/SoL-Pi/tree/d7ecfc089944f0d04b80122a0a9a6ca0d786f3d0) before enabling it.
|
|
30
|
+
|
|
31
|
+
## Trust, permissions, and data
|
|
32
|
+
|
|
33
|
+
Pi project trust and extension approval remain Pi's responsibility. Yoke does not grant trust or add an automatic approval flag. If Pi requires the project to be trusted before loading project-local settings or an extension, trust it through Pi yourself.
|
|
34
|
+
|
|
35
|
+
SoL-Pi runs inside the Pi process with that process's filesystem, process, network, and credential permissions. It is not a separate sandbox or permission boundary. Pi's selected tool permissions still apply: Yoke's safe Pi profile allows `read,bash,edit,write`, while its read-only profile allows `read,grep,find,ls`; the unsafe profile does not add an explicit tool allowlist. Action Fusion can run a model-requested validation command through Pi's shell tool. Review the extension at its pinned revision and choose Pi permissions appropriate for the project before enabling it.
|
|
36
|
+
|
|
37
|
+
When enabled, Evidence-Preserving Reducer may send eligible diagnostic-log content to its configured reducer model using Pi-managed authentication. Logs can contain project data. Its evidence checks validate the reduced receipt against the archived source; they do not make the source private or act as a complete redaction step. Leave the reducer off for logs that must stay local, and follow the data rules for the provider that receives the request. See the upstream [SoL-Pi security policy](https://github.com/NVlabs/SoL-Pi/blob/d7ecfc089944f0d04b80122a0a9a6ca0d786f3d0/SECURITY.md).
|
|
38
|
+
|
|
39
|
+
## Configuration ownership
|
|
40
|
+
|
|
41
|
+
The project's `.yoke/config.yaml` is the durable source for Yoke's SoL-Pi settings; the dashboard edits the same project settings. For an enabled Pi invocation, Yoke writes the corresponding temporary extension configuration to `.pi/sol-pi.json` in that invocation's workspace, the project-local path SoL-Pi reads. When the invocation ends, Yoke restores the file's previous contents or removes the temporary file and the `.pi` directory if Yoke created it. Pi only loads project-local extension configuration for a trusted project. Keep persistent SoL-Pi choices in Yoke's project configuration. Pi installation, project trust, and provider credentials remain owned by Pi and the user. See the pinned upstream [configuration contract](https://github.com/NVlabs/SoL-Pi/blob/d7ecfc089944f0d04b80122a0a9a6ca0d786f3d0/docs/configuration.md).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Code Intelligence stdio correction
|
|
2
|
+
|
|
3
|
+
Acceptance criteria:
|
|
4
|
+
- Serena and Graphify adapters initialize and call tools against newline-delimited JSON-RPC stdio servers.
|
|
5
|
+
- Existing Graft and client tests remain green; TypeScript builds.
|
|
6
|
+
- ReferDock uses explicit installed executable paths where Windows shims cannot be spawned.
|
|
7
|
+
- Real read-only probes report backend failures honestly; no cloud graph processing or package upgrades are required.
|
|
8
|
+
- Installed runtime changes retain backups and receive independent review before release.
|
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yoke",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0",
|
|
4
4
|
"description": "Cross-agent coding harness for eight supported CLIs: curated skill canon, mechanical safety gates, autonomous loop with proof artifacts. CLI: npm i -g @hecer/yoke",
|
|
5
5
|
"contextFileName": "GEMINI-EXTENSION.md"
|
|
6
6
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hecer/yoke",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0",
|
|
4
4
|
"description": "One harness, eight agents, zero trust in \"done\" — cross-agent coding harness for Claude Code, Codex CLI, Gemini CLI, Qwen Code, OpenCode, Kilo, Pi and Hermes: one skill canon, mechanical safety gates, an autonomous loop with screenshot/video proofs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|