@hecer/yoke 1.14.0 → 1.15.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/CHANGELOG.md +17 -0
- package/README.md +13 -9
- package/dist/cli.js +16 -2
- package/dist/code-intelligence/adapters/graft.js +11 -0
- package/dist/code-intelligence/adapters/graphify.js +8 -0
- package/dist/code-intelligence/adapters/index.js +5 -0
- package/dist/code-intelligence/adapters/mcp.js +37 -0
- package/dist/code-intelligence/adapters/serena.js +11 -0
- package/dist/code-intelligence/adapters/types.js +1 -0
- package/dist/code-intelligence/contracts.js +137 -0
- package/dist/code-intelligence/coordinator.js +370 -0
- package/dist/code-intelligence/edit-plans.js +53 -0
- package/dist/code-intelligence/evidence.js +77 -0
- package/dist/code-intelligence/index.js +5 -0
- package/dist/code-intelligence/internal-types.js +1 -0
- package/dist/code-intelligence/mcp-client.js +139 -0
- package/dist/code-intelligence/mcp-server.js +93 -0
- package/dist/code-intelligence/snapshots.js +117 -0
- package/dist/code-intelligence/transactions.js +60 -0
- package/dist/retrofit/command.js +3 -1
- package/dist/retrofit/config.js +10 -0
- package/dist/retrofit/gitignore.js +1 -0
- package/dist/retrofit/plan.js +3 -3
- package/dist/retrofit/planners/claude.js +3 -3
- package/dist/retrofit/planners/codex.js +5 -5
- package/dist/retrofit/planners/gemini.js +2 -2
- package/dist/retrofit/planners/kilo.js +2 -2
- package/dist/retrofit/planners/opencode.js +2 -2
- package/dist/retrofit/planners/pi.js +1 -1
- package/dist/retrofit/planners/qwen.js +2 -2
- package/dist/retrofit/tools.js +9 -7
- package/dist/setup/command.js +3 -1
- package/docs/CODE-INTELLIGENCE.md +36 -0
- package/package.json +1 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { CodeIntelligenceCoordinator } from './coordinator.js';
|
|
3
|
+
import { TOOL_DEFINITIONS } from './contracts.js';
|
|
4
|
+
import { loadConfig } from '../retrofit/config.js';
|
|
5
|
+
const INPUT_SCHEMAS = {
|
|
6
|
+
code_context: { type: 'object', required: ['workspace_id', 'query'], properties: { workspace_id: { type: 'string' }, query: { type: 'string' }, snapshot_id: { type: 'string' }, mode: { type: 'string', enum: ['orient', 'locate', 'explain'] }, paths: { type: 'array', items: { type: 'string' } }, include_docs: { type: 'boolean' }, token_budget: { type: 'integer' }, timeout_ms: { type: 'integer' } } },
|
|
7
|
+
code_symbol: { type: 'object', required: ['workspace_id', 'snapshot_id'], properties: { workspace_id: { type: 'string' }, snapshot_id: { type: 'string' }, symbol_id: { type: 'string' }, query: { type: 'string' }, path: { type: 'string' }, include: { type: 'array' } } },
|
|
8
|
+
code_trace: { type: 'object', required: ['workspace_id', 'snapshot_id', 'symbol_id', 'direction', 'relations'], properties: { workspace_id: { type: 'string' }, snapshot_id: { type: 'string' }, symbol_id: { type: 'string' }, direction: { type: 'string', enum: ['in', 'out'] }, relations: { type: 'array' }, max_depth: { type: 'integer' }, max_nodes: { type: 'integer' } } },
|
|
9
|
+
code_impact: { type: 'object', required: ['workspace_id', 'snapshot_id'], properties: { workspace_id: { type: 'string' }, snapshot_id: { type: 'string' }, targets: { type: 'array' }, plan_id: { type: 'string' }, include_docs: { type: 'boolean' }, require_semantic: { type: 'boolean' } } },
|
|
10
|
+
code_edit_preview: { type: 'object', required: ['workspace_id', 'snapshot_id', 'operations', 'validation_profile'], properties: { workspace_id: { type: 'string' }, snapshot_id: { type: 'string' }, operations: { type: 'array' }, validation_profile: { type: 'string', enum: ['default', 'extended'] } } },
|
|
11
|
+
code_edit_apply: { type: 'object', required: ['workspace_id', 'plan_id', 'expected_snapshot_id', 'idempotency_key'], properties: { workspace_id: { type: 'string' }, plan_id: { type: 'string' }, expected_snapshot_id: { type: 'string' }, idempotency_key: { type: 'string' } } },
|
|
12
|
+
};
|
|
13
|
+
function arg(name, args) { return args.find(value => value.startsWith(`--${name}=`))?.slice(name.length + 3); }
|
|
14
|
+
function summary(response) { return `status=${response.status} snapshot=${response.snapshot_id} warnings=${response.warnings.length} (full result in structuredContent)`; }
|
|
15
|
+
export async function runCodeIntelligenceServer(argv = process.argv.slice(2)) {
|
|
16
|
+
const workspace = arg('workspace', argv) ?? process.cwd();
|
|
17
|
+
const configured = loadConfig(workspace)?.codeIntelligence;
|
|
18
|
+
const mode = arg('mode', argv) ?? configured?.mode ?? 'active';
|
|
19
|
+
const coordinator = new CodeIntelligenceCoordinator(workspace, {
|
|
20
|
+
mode,
|
|
21
|
+
policy: { excludePatterns: configured?.policy?.excludePatterns, maxFileBytes: configured?.policy?.maxFileBytes },
|
|
22
|
+
limits: { maxBytes: configured?.limits?.maxBytes, maxBackends: configured?.limits?.maxBackends },
|
|
23
|
+
graft: configured?.graft, graphify: configured?.graphify, serena: configured?.serena,
|
|
24
|
+
});
|
|
25
|
+
let framing = 'line';
|
|
26
|
+
let buffer = Buffer.alloc(0);
|
|
27
|
+
const send = (message) => {
|
|
28
|
+
const payload = JSON.stringify(message);
|
|
29
|
+
if (framing === 'line')
|
|
30
|
+
process.stdout.write(`${payload}\n`);
|
|
31
|
+
else
|
|
32
|
+
process.stdout.write(`Content-Length: ${Buffer.byteLength(payload, 'utf8')}\r\n\r\n${payload}`);
|
|
33
|
+
};
|
|
34
|
+
const handle = async (message) => {
|
|
35
|
+
if (message.method === 'notifications/initialized' || message.method?.startsWith('notifications/'))
|
|
36
|
+
return;
|
|
37
|
+
if (message.method === 'initialize') {
|
|
38
|
+
send({ jsonrpc: '2.0', id: message.id, result: { protocolVersion: '2025-11-25', capabilities: { tools: {} }, serverInfo: { name: 'yoke-code-intelligence', version: '0.1.0' } } });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (message.method === 'tools/list') {
|
|
42
|
+
send({ jsonrpc: '2.0', id: message.id, result: { tools: TOOL_DEFINITIONS.map(tool => ({ ...tool, inputSchema: INPUT_SCHEMAS[tool.name] })) } });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (message.method === 'tools/call') {
|
|
46
|
+
const name = message.params?.name;
|
|
47
|
+
const response = await coordinator.dispatch(name, message.params?.arguments ?? {});
|
|
48
|
+
send({ jsonrpc: '2.0', id: message.id, result: { content: [{ type: 'text', text: summary(response) }], structuredContent: response, isError: response.status === 'error' || response.status === 'blocked' } });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (message.id !== undefined)
|
|
52
|
+
send({ jsonrpc: '2.0', id: message.id, error: { code: -32601, message: `method not found: ${message.method}` } });
|
|
53
|
+
};
|
|
54
|
+
process.stdin.on('data', chunk => {
|
|
55
|
+
buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
|
|
56
|
+
while (buffer.length) {
|
|
57
|
+
let raw;
|
|
58
|
+
if (/^Content-Length:/i.test(buffer.toString('ascii', 0, Math.min(buffer.length, 32)))) {
|
|
59
|
+
framing = 'content-length';
|
|
60
|
+
const end = buffer.indexOf('\r\n\r\n');
|
|
61
|
+
if (end < 0)
|
|
62
|
+
return;
|
|
63
|
+
const header = buffer.subarray(0, end).toString('ascii');
|
|
64
|
+
const match = /Content-Length:\s*(\d+)/i.exec(header);
|
|
65
|
+
if (!match) {
|
|
66
|
+
buffer = buffer.subarray(end + 4);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const length = Number(match[1]);
|
|
70
|
+
if (buffer.length < end + 4 + length)
|
|
71
|
+
return;
|
|
72
|
+
raw = buffer.subarray(end + 4, end + 4 + length).toString('utf8');
|
|
73
|
+
buffer = buffer.subarray(end + 4 + length);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const end = buffer.indexOf(10);
|
|
77
|
+
if (end < 0)
|
|
78
|
+
return;
|
|
79
|
+
raw = buffer.subarray(0, end).toString('utf8').trim();
|
|
80
|
+
buffer = buffer.subarray(end + 1);
|
|
81
|
+
}
|
|
82
|
+
if (!raw)
|
|
83
|
+
continue;
|
|
84
|
+
try {
|
|
85
|
+
void handle(JSON.parse(raw));
|
|
86
|
+
}
|
|
87
|
+
catch { /* malformed protocol input is ignored */ }
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
await new Promise(resolve => { const stop = () => { process.off('SIGTERM', stop); process.off('SIGINT', stop); void coordinator.close().finally(resolve); }; process.once('SIGTERM', stop); process.once('SIGINT', stop); process.stdin.once('end', stop); });
|
|
91
|
+
}
|
|
92
|
+
if (process.argv[1]?.endsWith('code-intelligence-server.js'))
|
|
93
|
+
void runCodeIntelligenceServer();
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, readdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
export const SnapshotFileSchema = z.object({ path: z.string().min(1), hash: z.string().length(64), size: z.number().int().nonnegative() });
|
|
7
|
+
export const SnapshotSchema = z.object({
|
|
8
|
+
snapshot_id: z.string().length(64), workspace_id: z.string().min(1), root: z.string().min(1), head: z.string().nullable(),
|
|
9
|
+
files: z.array(SnapshotFileSchema), policy_hash: z.string().length(64), created_at: z.string(),
|
|
10
|
+
});
|
|
11
|
+
function sha256(value) {
|
|
12
|
+
return createHash('sha256').update(value).digest('hex');
|
|
13
|
+
}
|
|
14
|
+
function inside(root, candidate) {
|
|
15
|
+
const rel = relative(root, candidate);
|
|
16
|
+
return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !rel.startsWith(sep));
|
|
17
|
+
}
|
|
18
|
+
function globToRegExp(pattern) {
|
|
19
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*').replace(/\?/g, '.');
|
|
20
|
+
return new RegExp(`^${escaped}$`);
|
|
21
|
+
}
|
|
22
|
+
function excluded(path, policy) {
|
|
23
|
+
const standard = [/^\.git(?:\/|$)/, /^\.yoke\/code-intelligence(?:\/|$)/, /(^|\/)\.env(?:\.|$)/, /\.(?:pem|key|p12|pfx)$/i];
|
|
24
|
+
if (standard.some(re => re.test(path)))
|
|
25
|
+
return true;
|
|
26
|
+
return (policy.excludePatterns ?? []).some(pattern => globToRegExp(pattern).test(path));
|
|
27
|
+
}
|
|
28
|
+
function safeFile(root, rel, policy) {
|
|
29
|
+
const normalized = rel.replaceAll('\\', '/');
|
|
30
|
+
if (!normalized || normalized.startsWith('/') || normalized.split('/').some(part => part === '..') || excluded(normalized, policy))
|
|
31
|
+
return null;
|
|
32
|
+
const absolute = resolve(root, normalized);
|
|
33
|
+
if (!inside(root, absolute) || !existsSync(absolute))
|
|
34
|
+
return null;
|
|
35
|
+
const stat = lstatSync(absolute);
|
|
36
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (policy.maxFileBytes && stat.size > policy.maxFileBytes))
|
|
37
|
+
return null;
|
|
38
|
+
return { path: normalized, bytes: readFileSync(absolute) };
|
|
39
|
+
}
|
|
40
|
+
function fallbackFiles(root, dir = root, output = []) {
|
|
41
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
42
|
+
if (entry.name === '.git' || entry.name === '.yoke' || entry.name === 'node_modules')
|
|
43
|
+
continue;
|
|
44
|
+
const absolute = join(dir, entry.name);
|
|
45
|
+
if (entry.isDirectory() && !entry.isSymbolicLink())
|
|
46
|
+
fallbackFiles(root, absolute, output);
|
|
47
|
+
else if (entry.isFile())
|
|
48
|
+
output.push(relative(root, absolute).replaceAll('\\', '/'));
|
|
49
|
+
}
|
|
50
|
+
return output;
|
|
51
|
+
}
|
|
52
|
+
function trackedAndUntracked(root) {
|
|
53
|
+
try {
|
|
54
|
+
const result = execFileSync('git', ['-C', root, 'ls-files', '-co', '--exclude-standard', '-z'], { encoding: 'buffer', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
55
|
+
return result.toString('utf8').split('\0').filter(Boolean);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return fallbackFiles(root);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function head(root) {
|
|
62
|
+
try {
|
|
63
|
+
return execFileSync('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || null;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function workspaceId(workspaceRoot) {
|
|
70
|
+
return `workspace-${sha256(realpathSync(resolve(workspaceRoot))).slice(0, 24)}`;
|
|
71
|
+
}
|
|
72
|
+
export function assertSafePath(workspaceRoot, candidate, policy = {}) {
|
|
73
|
+
const root = realpathSync(resolve(workspaceRoot));
|
|
74
|
+
const absolute = resolve(root, candidate);
|
|
75
|
+
const rel = relative(root, absolute).replaceAll('\\', '/');
|
|
76
|
+
if (!inside(root, absolute) || rel === '' || excluded(rel, policy))
|
|
77
|
+
throw new Error(`path is outside the permitted workspace: ${candidate}`);
|
|
78
|
+
try {
|
|
79
|
+
if (lstatSync(absolute).isSymbolicLink())
|
|
80
|
+
throw new Error(`symbolic links are not allowed: ${candidate}`);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error.code !== 'ENOENT')
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
return rel;
|
|
87
|
+
}
|
|
88
|
+
export function createSnapshot(workspaceRoot, policy = {}, workspace = workspaceId(workspaceRoot)) {
|
|
89
|
+
const root = realpathSync(resolve(workspaceRoot));
|
|
90
|
+
const files = trackedAndUntracked(root).map(path => safeFile(root, path, policy)).filter((value) => value !== null)
|
|
91
|
+
.sort((a, b) => a.path.localeCompare(b.path)).map(file => ({ path: file.path, hash: sha256(file.bytes), size: file.bytes.byteLength }));
|
|
92
|
+
const policyHash = sha256(JSON.stringify({ excludePatterns: policy.excludePatterns ?? [], maxFileBytes: policy.maxFileBytes ?? null }));
|
|
93
|
+
const identity = JSON.stringify({ workspace, root, head: head(root), files, policyHash });
|
|
94
|
+
const snapshot = SnapshotSchema.parse({ snapshot_id: sha256(identity), workspace_id: workspace, root, head: head(root), files, policy_hash: policyHash, created_at: new Date().toISOString() });
|
|
95
|
+
const dir = join(root, '.yoke', 'code-intelligence', 'snapshots');
|
|
96
|
+
mkdirSync(dir, { recursive: true });
|
|
97
|
+
writeFileSync(join(dir, `${snapshot.snapshot_id}.json`), JSON.stringify(snapshot, null, 2) + '\n', { mode: 0o600 });
|
|
98
|
+
return snapshot;
|
|
99
|
+
}
|
|
100
|
+
export function loadSnapshot(workspaceRoot, snapshotId) {
|
|
101
|
+
const root = realpathSync(resolve(workspaceRoot));
|
|
102
|
+
const file = join(root, '.yoke', 'code-intelligence', 'snapshots', `${snapshotId}.json`);
|
|
103
|
+
if (!existsSync(file))
|
|
104
|
+
throw new Error(`unknown snapshot: ${snapshotId}`);
|
|
105
|
+
const snapshot = SnapshotSchema.parse(JSON.parse(readFileSync(file, 'utf8')));
|
|
106
|
+
if (snapshot.root !== root)
|
|
107
|
+
throw new Error('snapshot belongs to another workspace');
|
|
108
|
+
return snapshot;
|
|
109
|
+
}
|
|
110
|
+
export function currentSnapshotMatches(workspaceRoot, snapshot, policy = {}) {
|
|
111
|
+
const current = createSnapshot(workspaceRoot, policy, snapshot.workspace_id);
|
|
112
|
+
return current.snapshot_id === snapshot.snapshot_id;
|
|
113
|
+
}
|
|
114
|
+
export function assertSnapshotCurrent(workspaceRoot, snapshot, policy = {}) {
|
|
115
|
+
if (!currentSnapshotMatches(workspaceRoot, snapshot, policy))
|
|
116
|
+
throw new Error('snapshot is stale; create a new snapshot before continuing');
|
|
117
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, openSync, closeSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { assertSnapshotCurrent } from './snapshots.js';
|
|
5
|
+
import { readPlanApproval } from './edit-plans.js';
|
|
6
|
+
function transactionDir(root) { return join(root, '.yoke', 'code-intelligence', 'transactions'); }
|
|
7
|
+
function hash(value) { return createHash('sha256').update(JSON.stringify(value)).digest('hex'); }
|
|
8
|
+
export function applyEditPlan(root, plan, snapshot, idempotencyKey) {
|
|
9
|
+
if (plan.expires_at <= new Date().toISOString())
|
|
10
|
+
throw new Error('edit plan has expired');
|
|
11
|
+
if (!readPlanApproval(root, plan))
|
|
12
|
+
throw new Error('edit plan requires Yoke control approval');
|
|
13
|
+
assertSnapshotCurrent(root, snapshot);
|
|
14
|
+
if (!existsSync(plan.diff_uri) || hash(readFileSync(plan.diff_uri, 'utf8')) !== plan.diff_hash)
|
|
15
|
+
throw new Error('edit plan diff has changed');
|
|
16
|
+
const sandboxHash = hash(plan.changed_paths.map(path => [path, existsSync(join(plan.sandbox_root, path)) ? readFileSync(join(plan.sandbox_root, path)).toString('base64') : null]));
|
|
17
|
+
if (sandboxHash !== plan.sandbox_hash)
|
|
18
|
+
throw new Error('edit plan sandbox has changed');
|
|
19
|
+
const dir = transactionDir(root);
|
|
20
|
+
mkdirSync(dir, { recursive: true });
|
|
21
|
+
const idem = join(dir, `${idempotencyKey}.json`);
|
|
22
|
+
if (existsSync(idem)) {
|
|
23
|
+
const prior = JSON.parse(readFileSync(idem, 'utf8'));
|
|
24
|
+
if (prior.plan_id !== plan.plan_id)
|
|
25
|
+
throw new Error('idempotency key is bound to another plan');
|
|
26
|
+
return prior;
|
|
27
|
+
}
|
|
28
|
+
const lock = join(dir, '.write.lock');
|
|
29
|
+
let handle;
|
|
30
|
+
try {
|
|
31
|
+
handle = openSync(lock, 'wx');
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new Error('another code-intelligence transaction is active');
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const transactionId = `tx-${randomUUID()}`;
|
|
38
|
+
const worktree = join(root, '.yoke', 'code-intelligence', 'worktrees', transactionId);
|
|
39
|
+
mkdirSync(worktree, { recursive: true });
|
|
40
|
+
for (const path of plan.changed_paths) {
|
|
41
|
+
const source = join(plan.sandbox_root, path);
|
|
42
|
+
const target = join(worktree, path);
|
|
43
|
+
if (!existsSync(source))
|
|
44
|
+
continue;
|
|
45
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
46
|
+
copyFileSync(source, target);
|
|
47
|
+
}
|
|
48
|
+
const result = { transaction_id: transactionId, plan_id: plan.plan_id, changed_paths: plan.changed_paths, worktree, new_snapshot_id: snapshot.snapshot_id, validation: plan.validation, committed: false, merged: false };
|
|
49
|
+
writeFileSync(idem, JSON.stringify(result, null, 2) + '\n', { mode: 0o600 });
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
if (handle !== undefined)
|
|
54
|
+
closeSync(handle);
|
|
55
|
+
try {
|
|
56
|
+
unlinkSync(lock);
|
|
57
|
+
}
|
|
58
|
+
catch { /* best effort */ }
|
|
59
|
+
}
|
|
60
|
+
}
|
package/dist/retrofit/command.js
CHANGED
|
@@ -17,7 +17,8 @@ export function runRetrofit(targetDir, opts) {
|
|
|
17
17
|
: (detection.agents.length > 0 ? detection.agents : [opts.host ?? detectHostAgent() ?? 'claude']);
|
|
18
18
|
const existing = loadConfig(targetDir);
|
|
19
19
|
const codeGraph = opts.codeGraph ?? existing?.codeGraph ?? 'graphify';
|
|
20
|
-
const
|
|
20
|
+
const codeIntelligence = opts.codeIntelligence ?? existing?.codeIntelligence?.mode ?? 'off';
|
|
21
|
+
const actions = planRetrofit(canonDir, targetDir, agents, codeGraph, codeIntelligence);
|
|
21
22
|
const backupDir = join(targetDir, '.yoke', 'backup', String(Date.now()));
|
|
22
23
|
const applied = applyActions(actions, targetDir, { backupDir });
|
|
23
24
|
// Ensure runtime artifacts (loop status/log, worktrees, backups) are gitignored
|
|
@@ -31,6 +32,7 @@ export function runRetrofit(targetDir, opts) {
|
|
|
31
32
|
agents: mergedAgents,
|
|
32
33
|
loop: { ...existing?.loop, enabled: opts.loop },
|
|
33
34
|
codeGraph,
|
|
35
|
+
codeIntelligence: { ...(existing?.codeIntelligence ?? {}), mode: codeIntelligence },
|
|
34
36
|
...(existing?.design
|
|
35
37
|
? { design: existing.design }
|
|
36
38
|
: detection.ui.detected ? { design: { mode: 'auto', max: 4 } } : {}),
|
package/dist/retrofit/config.js
CHANGED
|
@@ -6,7 +6,16 @@ import { AgentSchema, PermissionProfileSchema } from '../agents/contracts.js';
|
|
|
6
6
|
import { ProjectQualityDefaultsSchema } from '../quality/types.js';
|
|
7
7
|
import { DEFAULT_OUTPUT_POLICY } from '../output/types.js';
|
|
8
8
|
import { ToolActionSchema } from '../execution/actions.js';
|
|
9
|
+
import { CodeIntelligenceModeSchema } from '../code-intelligence/contracts.js';
|
|
10
|
+
export { CodeIntelligenceModeSchema };
|
|
9
11
|
const CodeGraphSchema = z.enum(['graphify', 'serena']);
|
|
12
|
+
const CodeIntelligenceProcessSchema = z.object({ command: z.string().min(1), args: z.array(z.string()).max(32).optional() });
|
|
13
|
+
const CodeIntelligenceSchema = z.object({
|
|
14
|
+
mode: CodeIntelligenceModeSchema.default('off'), workspaceId: z.string().min(1).optional(),
|
|
15
|
+
graft: CodeIntelligenceProcessSchema.optional(), graphify: CodeIntelligenceProcessSchema.optional(), serena: CodeIntelligenceProcessSchema.optional(),
|
|
16
|
+
policy: z.object({ allowNetwork: z.boolean().default(false), excludePatterns: z.array(z.string().min(1)).max(100).default([]), maxFileBytes: z.number().int().positive().max(20_000_000).optional() }).optional(),
|
|
17
|
+
limits: z.object({ tokenBudget: z.number().int().min(128).max(16000).default(2400), timeoutMs: z.number().int().min(100).max(600000).default(10000), maxBytes: z.number().int().positive().max(10_000_000).default(2_000_000), maxBackends: z.number().int().min(1).max(3).default(3) }).optional(),
|
|
18
|
+
});
|
|
10
19
|
const SmokeFlowSchema = z.object({ name: z.string().min(1), path: z.string().min(1), landmark: z.string().optional() });
|
|
11
20
|
const SmokeSchema = z.object({ baseUrl: z.string().min(1), flows: z.array(SmokeFlowSchema).min(1) });
|
|
12
21
|
const OutputPolicySchema = z.object({
|
|
@@ -118,6 +127,7 @@ export const YokeConfigSchema = z.object({
|
|
|
118
127
|
max: z.number().int().positive().default(4),
|
|
119
128
|
}).optional(),
|
|
120
129
|
codeGraph: CodeGraphSchema.optional(),
|
|
130
|
+
codeIntelligence: CodeIntelligenceSchema.optional(),
|
|
121
131
|
smoke: SmokeSchema.optional(),
|
|
122
132
|
quality: ProjectQualityDefaultsSchema.optional(),
|
|
123
133
|
output: OutputPolicySchema.optional(),
|
package/dist/retrofit/plan.js
CHANGED
|
@@ -10,7 +10,7 @@ export function planClaudeRetrofit(canonDir, targetDir) {
|
|
|
10
10
|
return planClaude(canonDir, targetDir);
|
|
11
11
|
}
|
|
12
12
|
export const PLANNERS = {
|
|
13
|
-
claude: (c, t, cg) => planClaude(c, t, undefined, cg),
|
|
13
|
+
claude: (c, t, cg, ci) => planClaude(c, t, undefined, cg, undefined, ci),
|
|
14
14
|
codex: planCodex,
|
|
15
15
|
gemini: planGemini,
|
|
16
16
|
qwen: planQwen,
|
|
@@ -18,7 +18,7 @@ export const PLANNERS = {
|
|
|
18
18
|
kilo: planKilo,
|
|
19
19
|
pi: planPi,
|
|
20
20
|
};
|
|
21
|
-
export function planRetrofit(canonDir, targetDir, agents, codeGraph = 'graphify') {
|
|
21
|
+
export function planRetrofit(canonDir, targetDir, agents, codeGraph = 'graphify', codeIntelligence = 'off') {
|
|
22
22
|
const seen = new Set();
|
|
23
23
|
const merged = [];
|
|
24
24
|
for (const action of baseContextActions(canonDir)) {
|
|
@@ -26,7 +26,7 @@ export function planRetrofit(canonDir, targetDir, agents, codeGraph = 'graphify'
|
|
|
26
26
|
merged.push(action);
|
|
27
27
|
}
|
|
28
28
|
for (const agent of agents) {
|
|
29
|
-
for (const action of PLANNERS[agent](canonDir, targetDir, codeGraph)) {
|
|
29
|
+
for (const action of PLANNERS[agent](canonDir, targetDir, codeGraph, codeIntelligence)) {
|
|
30
30
|
if (seen.has(action.target))
|
|
31
31
|
continue;
|
|
32
32
|
seen.add(action.target);
|
|
@@ -22,7 +22,7 @@ This project uses the Yoke harness. Baseline instructions:
|
|
|
22
22
|
${rtkNote ? `\n${rtkNote}\n` : ''}${composeNote ? `\n${composeNote}\n` : ''}
|
|
23
23
|
${PRESERVE_SCAFFOLD}
|
|
24
24
|
`;
|
|
25
|
-
export function planClaude(canonDir, targetDir, wslAvailable = hasWsl(), codeGraph = 'graphify', gstackDetected = detectGstack(targetDir)) {
|
|
25
|
+
export function planClaude(canonDir, targetDir, wslAvailable = hasWsl(), codeGraph = 'graphify', gstackDetected = detectGstack(targetDir), codeIntelligence = 'off') {
|
|
26
26
|
const manifest = loadManifest(join(canonDir, 'manifest.yaml'));
|
|
27
27
|
const actions = [];
|
|
28
28
|
for (const skill of manifest.skills) {
|
|
@@ -45,8 +45,8 @@ export function planClaude(canonDir, targetDir, wslAvailable = hasWsl(), codeGra
|
|
|
45
45
|
actions.push({
|
|
46
46
|
kind: 'write',
|
|
47
47
|
target: '.mcp.json',
|
|
48
|
-
content: JSON.stringify({ mcpServers: mcpServers(codeGraph) }, null, 2) + '\n',
|
|
49
|
-
reason: 'MCP servers (code-graph + playwright)',
|
|
48
|
+
content: JSON.stringify({ mcpServers: mcpServers(codeGraph, codeIntelligence, targetDir) }, null, 2) + '\n',
|
|
49
|
+
reason: codeIntelligence === 'off' ? 'MCP servers (code-graph + playwright)' : 'Yoke code-intelligence facade + playwright',
|
|
50
50
|
});
|
|
51
51
|
if (rtkHookable) {
|
|
52
52
|
actions.push({
|
|
@@ -3,8 +3,8 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { loadManifest } from '../../canon/manifest.js';
|
|
4
4
|
import { mcpServers, rtkInstruction } from '../tools.js';
|
|
5
5
|
import { skillPackageActions } from '../skill-actions.js';
|
|
6
|
-
function tomlMcp(codeGraph) {
|
|
7
|
-
const servers = mcpServers(codeGraph);
|
|
6
|
+
function tomlMcp(codeGraph, codeIntelligence, targetDir) {
|
|
7
|
+
const servers = mcpServers(codeGraph, codeIntelligence, targetDir);
|
|
8
8
|
// Codex reads MCP servers from ~/.codex/config.toml. This project-level file is a
|
|
9
9
|
// ready-to-merge snippet; users append these blocks to their global config.
|
|
10
10
|
return Object.entries(servers)
|
|
@@ -14,7 +14,7 @@ function tomlMcp(codeGraph) {
|
|
|
14
14
|
})
|
|
15
15
|
.join('\n');
|
|
16
16
|
}
|
|
17
|
-
export function planCodex(canonDir,
|
|
17
|
+
export function planCodex(canonDir, targetDir, codeGraph = 'graphify', codeIntelligence = 'off') {
|
|
18
18
|
const manifest = loadManifest(join(canonDir, 'manifest.yaml'));
|
|
19
19
|
const baseline = readFileSync(join(canonDir, 'AGENTS.md'), 'utf8');
|
|
20
20
|
const actions = manifest.skills.flatMap(skill => skillPackageActions(canonDir, skill, 'codex'));
|
|
@@ -32,8 +32,8 @@ export function planCodex(canonDir, _targetDir, codeGraph = 'graphify') {
|
|
|
32
32
|
}, {
|
|
33
33
|
kind: 'write',
|
|
34
34
|
target: '.codex/config.toml',
|
|
35
|
-
content: `# Yoke project configuration. Codex loads this in trusted repositories.\n\n[features]\nhooks = true\n\n${tomlMcp(codeGraph)}`,
|
|
36
|
-
reason: 'MCP servers (code-graph + playwright)',
|
|
35
|
+
content: `# Yoke project configuration. Codex loads this in trusted repositories.\n\n[features]\nhooks = true\n\n${tomlMcp(codeGraph, codeIntelligence, targetDir)}`,
|
|
36
|
+
reason: codeIntelligence === 'off' ? 'MCP servers (code-graph + playwright)' : 'Yoke code-intelligence facade + playwright',
|
|
37
37
|
}, {
|
|
38
38
|
kind: 'write',
|
|
39
39
|
target: '.codex/hooks.json',
|
|
@@ -8,7 +8,7 @@ import { skillPackageActions } from '../skill-actions.js';
|
|
|
8
8
|
function tomlString(s) {
|
|
9
9
|
return '"""\n' + s.replace(/\\/g, '\\\\').replace(/"""/g, '\\"\\"\\"') + '\n"""';
|
|
10
10
|
}
|
|
11
|
-
export function planGemini(canonDir,
|
|
11
|
+
export function planGemini(canonDir, targetDir, codeGraph = 'graphify', codeIntelligence = 'off') {
|
|
12
12
|
const manifest = loadManifest(join(canonDir, 'manifest.yaml'));
|
|
13
13
|
const actions = [];
|
|
14
14
|
// GEMINI.md: baseline + instruction for commands outside the rewrite hook.
|
|
@@ -56,7 +56,7 @@ export function planGemini(canonDir, _targetDir, codeGraph = 'graphify') {
|
|
|
56
56
|
target: '.gemini/settings.json',
|
|
57
57
|
merge: true,
|
|
58
58
|
content: JSON.stringify({
|
|
59
|
-
mcpServers: mcpServers(codeGraph),
|
|
59
|
+
mcpServers: mcpServers(codeGraph, codeIntelligence, targetDir),
|
|
60
60
|
context: { fileName: ['AGENTS.md', 'GEMINI.md'] },
|
|
61
61
|
hooks: { BeforeTool: [{ matcher: '^run_shell_command$', hooks: [{ name: 'yoke-rtk', type: 'command', command: 'node .gemini/hooks/gemini-rtk-hook.mjs' }] }] },
|
|
62
62
|
}, null, 2) + '\n',
|
|
@@ -15,7 +15,7 @@ permission:
|
|
|
15
15
|
|
|
16
16
|
Review the observed diff and test evidence. Do not modify files. Return only actionable findings grounded in evidence.
|
|
17
17
|
`;
|
|
18
|
-
export function planKilo(canonDir,
|
|
18
|
+
export function planKilo(canonDir, targetDir, codeGraph = 'graphify', codeIntelligence = 'off') {
|
|
19
19
|
const manifest = loadManifest(join(canonDir, 'manifest.yaml'));
|
|
20
20
|
const baseline = readFileSync(join(canonDir, 'AGENTS.md'), 'utf8');
|
|
21
21
|
const actions = manifest.skills.flatMap(skill => skillPackageActions(canonDir, skill, 'kilo'));
|
|
@@ -31,7 +31,7 @@ export function planKilo(canonDir, _targetDir, codeGraph = 'graphify') {
|
|
|
31
31
|
content: JSON.stringify({
|
|
32
32
|
$schema: 'https://app.kilo.ai/config.json',
|
|
33
33
|
instructions: ['AGENTS.md', '.yoke/context/*.md'],
|
|
34
|
-
mcp: openCodeMcpServers(codeGraph),
|
|
34
|
+
mcp: openCodeMcpServers(codeGraph, codeIntelligence, targetDir),
|
|
35
35
|
}, null, 2) + '\n',
|
|
36
36
|
reason: 'Kilo instructions + MCP servers',
|
|
37
37
|
}, {
|
|
@@ -15,7 +15,7 @@ tools:
|
|
|
15
15
|
|
|
16
16
|
Review the observed diff and test evidence. Do not modify files. Return only actionable findings grounded in evidence.
|
|
17
17
|
`;
|
|
18
|
-
export function planOpenCode(canonDir,
|
|
18
|
+
export function planOpenCode(canonDir, targetDir, codeGraph = 'graphify', codeIntelligence = 'off') {
|
|
19
19
|
const manifest = loadManifest(join(canonDir, 'manifest.yaml'));
|
|
20
20
|
const baseline = readFileSync(join(canonDir, 'AGENTS.md'), 'utf8');
|
|
21
21
|
const actions = manifest.skills.flatMap(skill => skillPackageActions(canonDir, skill, 'opencode'));
|
|
@@ -31,7 +31,7 @@ export function planOpenCode(canonDir, _targetDir, codeGraph = 'graphify') {
|
|
|
31
31
|
content: JSON.stringify({
|
|
32
32
|
$schema: 'https://opencode.ai/config.json',
|
|
33
33
|
instructions: ['AGENTS.md', '.yoke/context/*.md'],
|
|
34
|
-
mcp: openCodeMcpServers(codeGraph),
|
|
34
|
+
mcp: openCodeMcpServers(codeGraph, codeIntelligence, targetDir),
|
|
35
35
|
}, null, 2) + '\n',
|
|
36
36
|
reason: 'OpenCode instructions + MCP servers',
|
|
37
37
|
}, {
|
|
@@ -4,7 +4,7 @@ import { loadManifest } from '../../canon/manifest.js';
|
|
|
4
4
|
import { rtkInstruction } from '../tools.js';
|
|
5
5
|
import { PRESERVE_SCAFFOLD } from '../preserve.js';
|
|
6
6
|
import { skillPackageActions } from '../skill-actions.js';
|
|
7
|
-
export function planPi(canonDir, _targetDir, _codeGraph = 'graphify') {
|
|
7
|
+
export function planPi(canonDir, _targetDir, _codeGraph = 'graphify', _codeIntelligence = 'off') {
|
|
8
8
|
const manifest = loadManifest(join(canonDir, 'manifest.yaml'));
|
|
9
9
|
const baseline = readFileSync(join(canonDir, 'AGENTS.md'), 'utf8');
|
|
10
10
|
const actions = manifest.skills.flatMap(skill => skillPackageActions(canonDir, skill, 'pi'));
|
|
@@ -8,7 +8,7 @@ import { skillPackageActions } from '../skill-actions.js';
|
|
|
8
8
|
function tomlString(s) {
|
|
9
9
|
return '"""\n' + s.replace(/\\/g, '\\\\').replace(/"""/g, '\\"\\"\\"') + '\n"""';
|
|
10
10
|
}
|
|
11
|
-
export function planQwen(canonDir,
|
|
11
|
+
export function planQwen(canonDir, targetDir, codeGraph = 'graphify', codeIntelligence = 'off') {
|
|
12
12
|
const manifest = loadManifest(join(canonDir, 'manifest.yaml'));
|
|
13
13
|
const actions = [];
|
|
14
14
|
// QWEN.md: baseline + instruction for commands outside the rewrite hook.
|
|
@@ -56,7 +56,7 @@ export function planQwen(canonDir, _targetDir, codeGraph = 'graphify') {
|
|
|
56
56
|
target: '.qwen/settings.json',
|
|
57
57
|
merge: true,
|
|
58
58
|
content: JSON.stringify({
|
|
59
|
-
mcpServers: mcpServers(codeGraph),
|
|
59
|
+
mcpServers: mcpServers(codeGraph, codeIntelligence, targetDir),
|
|
60
60
|
context: { fileName: ['AGENTS.md', 'QWEN.md'] },
|
|
61
61
|
hooks: { PreToolUse: [{ matcher: '^run_shell_command$', hooks: [{ name: 'yoke-rtk', type: 'command', command: 'node .qwen/hooks/qwen-rtk-hook.mjs' }] }] },
|
|
62
62
|
}, null, 2) + '\n',
|
package/dist/retrofit/tools.js
CHANGED
|
@@ -8,15 +8,17 @@ const CODE_GRAPH_SERVERS = {
|
|
|
8
8
|
args: ['start-mcp-server', '--open-web-dashboard', 'false'],
|
|
9
9
|
},
|
|
10
10
|
};
|
|
11
|
-
export function mcpServers(codeGraph = 'graphify') {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
export function mcpServers(codeGraph = 'graphify', codeIntelligence = 'off', targetDir = '.') {
|
|
12
|
+
if (codeIntelligence !== 'off')
|
|
13
|
+
return {
|
|
14
|
+
'code-intelligence': { command: 'yoke', args: ['code-intelligence-server', `--workspace=${targetDir}`, `--mode=${codeIntelligence}`] },
|
|
15
|
+
playwright: { command: 'npx', args: ['@playwright/mcp@latest'] },
|
|
16
|
+
};
|
|
17
|
+
return { [codeGraph]: CODE_GRAPH_SERVERS[codeGraph], playwright: { command: 'npx', args: ['@playwright/mcp@latest'] } };
|
|
16
18
|
}
|
|
17
19
|
/** OpenCode-family CLIs use an object with a local command array, not Claude's mcpServers shape. */
|
|
18
|
-
export function openCodeMcpServers(codeGraph = 'graphify') {
|
|
19
|
-
return Object.fromEntries(Object.entries(mcpServers(codeGraph)).map(([name, server]) => [name, {
|
|
20
|
+
export function openCodeMcpServers(codeGraph = 'graphify', codeIntelligence = 'off', targetDir = '.') {
|
|
21
|
+
return Object.fromEntries(Object.entries(mcpServers(codeGraph, codeIntelligence, targetDir)).map(([name, server]) => [name, {
|
|
20
22
|
type: 'local',
|
|
21
23
|
command: [server.command, ...server.args],
|
|
22
24
|
enabled: true,
|
package/dist/setup/command.js
CHANGED
|
@@ -75,6 +75,7 @@ export async function runSetup(targetDir, opts = {}) {
|
|
|
75
75
|
? detected.agents
|
|
76
76
|
: modelProviders.length ? ['qwen'] : [host ?? 'claude'];
|
|
77
77
|
const defaultGraph = opts.codeGraph ?? existing?.codeGraph ?? 'graphify';
|
|
78
|
+
const defaultCodeIntelligence = opts.codeIntelligence ?? existing?.codeIntelligence?.mode ?? 'off';
|
|
78
79
|
const defaultLoop = opts.loop ?? existing?.loop.enabled ?? true;
|
|
79
80
|
const defaultRunner = opts.runner ?? existing?.runner?.agent ?? (host && defaultAgents.includes(host) ? host : defaultAgents[0] ?? host ?? 'claude');
|
|
80
81
|
const defaultPolicy = opts.decisionPolicy ?? existing?.loop.decisionPolicy ?? (existing?.loop.onAmbiguity === 'abort' ? 'critical' : 'auto');
|
|
@@ -90,6 +91,7 @@ export async function runSetup(targetDir, opts = {}) {
|
|
|
90
91
|
try {
|
|
91
92
|
let agents = defaultAgents;
|
|
92
93
|
let codeGraph = defaultGraph;
|
|
94
|
+
let codeIntelligence = defaultCodeIntelligence;
|
|
93
95
|
let loop = defaultLoop;
|
|
94
96
|
let runner = defaultRunner;
|
|
95
97
|
let decisionPolicy = defaultPolicy;
|
|
@@ -112,7 +114,7 @@ export async function runSetup(targetDir, opts = {}) {
|
|
|
112
114
|
agents = [...agents, 'qwen'];
|
|
113
115
|
if (!agents.includes(runner))
|
|
114
116
|
agents = [...agents, runner];
|
|
115
|
-
const code = runRetrofit(targetDir, { loop, agents, codeGraph, host });
|
|
117
|
+
const code = runRetrofit(targetDir, { loop, agents, codeGraph, codeIntelligence, host });
|
|
116
118
|
if (code !== 0)
|
|
117
119
|
return code;
|
|
118
120
|
applyActions(presetActions, targetDir, { backupDir: join(targetDir, '.yoke', 'backups', `model-presets-${Date.now()}`) });
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Yoke Code Intelligence
|
|
2
|
+
|
|
3
|
+
Yoke integrates Graphify, Graft and Serena as a federation. Their repositories are not merged into Yoke and their individual MCP tools are not published to agents. Yoke publishes one snapshot-bound facade with six tools:
|
|
4
|
+
|
|
5
|
+
`code_context`, `code_symbol`, `code_trace`, `code_impact`, `code_edit_preview`, and `code_edit_apply`.
|
|
6
|
+
|
|
7
|
+
## Enable it
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
yoke setup . --yes --code-intelligence=active
|
|
11
|
+
# or, for an existing project:
|
|
12
|
+
yoke retrofit . --code-intelligence=active
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The default is `off`, which preserves the previous `codeGraph` behavior. `shadow` enables a read-only canary and keeps all edits blocked. `active` enables read operations and snapshot-bound edit previews. The generated host configuration exposes only Yoke's facade, so an agent cannot accidentally combine duplicate backend tools.
|
|
16
|
+
|
|
17
|
+
## Backend roles and tested pins
|
|
18
|
+
|
|
19
|
+
| Backend | Role | Launch command | Pin checked during integration |
|
|
20
|
+
|---|---|---|---|
|
|
21
|
+
| [Graft](https://github.com/trailhq/Graft) | structural code search, repo map, call graph | `graft mcp` | `05760b07abc0e427f5af8ad378889ee402c5afc6` / 0.17.0 |
|
|
22
|
+
| [Graphify](https://github.com/Graphify-Labs/graphify) | architecture/document knowledge graph | `python -m graphify.serve` | `67f99bd0059dd1bac9e44382907ef9f10098b39f` / 0.9.56 |
|
|
23
|
+
| [Serena](https://github.com/oraios/serena) | LSP symbols, references, implementations and semantic edits | `serena start-mcp-server --project-from-cwd --context codex` | `701e7c843f46c6a649203a488cece1bf19f1df90` / 1.7.1-dev |
|
|
24
|
+
|
|
25
|
+
Install and pin these tools in the project or developer environment before enabling the facade. Yoke never installs `latest` during a task. Missing tools produce an explicit `partial` response with backend warnings and provenance; they are never interpreted as “no references”.
|
|
26
|
+
|
|
27
|
+
## Safety contract
|
|
28
|
+
|
|
29
|
+
- Every request is tied to a workspace and a content-addressed snapshot of committed and uncommitted, non-sensitive files.
|
|
30
|
+
- Paths are workspace-relative; traversal, symlinks, `.env`/key material and policy-excluded paths are rejected.
|
|
31
|
+
- Read calls are bounded by timeout and output budgets. Results carry backend, version, source path, content hash, resolution and freshness.
|
|
32
|
+
- Edits run in a disposable Yoke sandbox. Preview writes a durable plan and diff but requires an approval record created by the Yoke control layer.
|
|
33
|
+
- Apply rechecks the snapshot, plan expiry, plan hash, an exclusive transaction lock and idempotency. It produces a managed worktree and does not commit or merge; the existing Yoke loop and gates remain authoritative for integration.
|
|
34
|
+
- Network access is not needed by the facade. Backend installation and any backend-specific network behavior remain an explicit environment concern.
|
|
35
|
+
|
|
36
|
+
For a host that has no native MCP support (currently Pi), use the normal Yoke loop and bounded CLI/skill adapter. Yoke does not invent a second MCP transport for that host.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hecer/yoke",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"description": "One harness, seven agents, zero trust in \"done\" — cross-agent coding harness for Claude Code, Codex CLI, Gemini CLI, Qwen Code, OpenCode, Kilo and Pi: one skill canon, mechanical safety gates, an autonomous loop with screenshot/video proofs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|