@hecer/yoke 1.14.0 → 1.15.1
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 +36 -0
- package/README.md +15 -9
- package/canon/AGENTS.md +7 -0
- package/canon/manifest.yaml +1 -1
- package/canon/skills/executing-plans/SKILL.md +1 -1
- package/canon/skills/requesting-code-review/SKILL.md +2 -2
- package/canon/skills/requesting-code-review/code-reviewer.md +12 -0
- package/canon/skills/subagent-driven-development/SKILL.md +7 -3
- package/canon/skills/subagent-driven-development/code-quality-reviewer-prompt.md +7 -0
- package/canon/skills/subagent-driven-development/implementer-prompt.md +7 -0
- package/canon/skills/subagent-driven-development/spec-reviewer-prompt.md +7 -0
- package/canon/skills/systematic-debugging/SKILL.md +7 -11
- package/canon/skills/systematic-debugging/condition-based-waiting.md +7 -0
- package/canon/skills/systematic-debugging/defense-in-depth.md +7 -0
- package/canon/skills/systematic-debugging/root-cause-tracing.md +9 -0
- package/canon/skills/tdd/SKILL.md +1 -1
- package/canon/skills/tdd/testing-anti-patterns.md +9 -0
- package/dist/agents/pi-telemetry.js +53 -0
- package/dist/agents/process-streams.js +13 -0
- package/dist/agents/providers.js +4 -1
- package/dist/agents/supervision.js +2 -0
- package/dist/agents/telemetry.js +22 -25
- 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/estimation/schedule.js +40 -26
- 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 +2 -2
- package/dist/retrofit/planners/qwen.js +2 -2
- package/dist/retrofit/skill-actions.js +1 -1
- package/dist/retrofit/tools.js +9 -7
- package/dist/setup/command.js +3 -1
- package/docs/AGENT-HARDENING-2026-09-16.md +32 -0
- package/docs/CODE-INTELLIGENCE.md +36 -0
- package/docs/HARNESSES.md +6 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
function hash(value) { return createHash('sha256').update(JSON.stringify(value)).digest('hex'); }
|
|
5
|
+
function storeDir(root) { return join(root, '.yoke', 'code-intelligence', 'plans'); }
|
|
6
|
+
function planHashInput(plan) {
|
|
7
|
+
return { workspace_id: plan.workspace_id, snapshot_id: plan.snapshot_id, root: plan.root, sandbox_root: plan.sandbox_root, operations: plan.operations, validation_profile: plan.validation_profile, changedPaths: plan.changed_paths, validation: plan.validation, expiresAt: plan.expires_at, diffHash: plan.diff_hash, sandboxHash: plan.sandbox_hash };
|
|
8
|
+
}
|
|
9
|
+
export function createEditPlan(root, snapshot, sandboxRoot, operations, validation, validationProfile, changedPaths, diff, ttlMs = 30 * 60_000) {
|
|
10
|
+
const planId = `plan-${randomUUID()}`;
|
|
11
|
+
const diffUri = join(storeDir(root), `${planId}.diff`);
|
|
12
|
+
const expiresAt = new Date(Date.now() + ttlMs).toISOString();
|
|
13
|
+
const diffHash = hash(diff);
|
|
14
|
+
const sandboxHash = hash(changedPaths.map(path => [path, existsSync(join(sandboxRoot, path)) ? readFileSync(join(sandboxRoot, path)).toString('base64') : null]));
|
|
15
|
+
const base = { workspace_id: snapshot.workspace_id, snapshot_id: snapshot.snapshot_id, root, sandbox_root: sandboxRoot, operations, validation_profile: validationProfile, changedPaths, validation, expiresAt, diffHash, sandboxHash };
|
|
16
|
+
const planHash = hash(base);
|
|
17
|
+
const plan = {
|
|
18
|
+
plan_id: planId, plan_hash: planHash, diff_uri: diffUri, changed_paths: changedPaths, validation, approval_required: true, expires_at: expiresAt,
|
|
19
|
+
workspace_id: snapshot.workspace_id, snapshot_id: snapshot.snapshot_id, root, sandbox_root: sandboxRoot, operations, validation_profile: validationProfile, created_at: new Date().toISOString(), diff_hash: diffHash, sandbox_hash: sandboxHash,
|
|
20
|
+
};
|
|
21
|
+
mkdirSync(storeDir(root), { recursive: true });
|
|
22
|
+
writeFileSync(diffUri, diff, { mode: 0o600 });
|
|
23
|
+
writeFileSync(join(storeDir(root), `${planId}.json`), JSON.stringify(plan, null, 2) + '\n', { mode: 0o600 });
|
|
24
|
+
return plan;
|
|
25
|
+
}
|
|
26
|
+
export function loadEditPlan(root, planId) {
|
|
27
|
+
const file = join(storeDir(root), `${planId}.json`);
|
|
28
|
+
if (!existsSync(file))
|
|
29
|
+
throw new Error(`unknown edit plan: ${planId}`);
|
|
30
|
+
const plan = JSON.parse(readFileSync(file, 'utf8'));
|
|
31
|
+
if (plan.plan_hash !== hash(planHashInput(plan)))
|
|
32
|
+
throw new Error('edit plan integrity check failed');
|
|
33
|
+
return plan;
|
|
34
|
+
}
|
|
35
|
+
/** This is deliberately separate from the MCP apply tool. The loop/control layer
|
|
36
|
+
* calls it only after human/policy approval has been recorded. */
|
|
37
|
+
export function approveEditPlan(root, planId) {
|
|
38
|
+
const plan = loadEditPlan(root, planId);
|
|
39
|
+
mkdirSync(storeDir(root), { recursive: true });
|
|
40
|
+
writeFileSync(join(storeDir(root), `${planId}.approval.json`), JSON.stringify({ plan_id: planId, plan_hash: plan.plan_hash, approved_at: new Date().toISOString(), authority: 'yoke-control' }) + '\n', { mode: 0o600 });
|
|
41
|
+
}
|
|
42
|
+
export function readPlanApproval(root, plan) {
|
|
43
|
+
const file = join(storeDir(root), `${plan.plan_id}.approval.json`);
|
|
44
|
+
if (!existsSync(file))
|
|
45
|
+
return false;
|
|
46
|
+
try {
|
|
47
|
+
const approval = JSON.parse(readFileSync(file, 'utf8'));
|
|
48
|
+
return approval.plan_hash === plan.plan_hash && approval.authority === 'yoke-control';
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { assertSafePath } from './snapshots.js';
|
|
3
|
+
function hash(value) { return createHash('sha256').update(value).digest('hex'); }
|
|
4
|
+
function textOf(raw) {
|
|
5
|
+
if (typeof raw === 'string')
|
|
6
|
+
return raw;
|
|
7
|
+
try {
|
|
8
|
+
return JSON.stringify(raw) ?? '';
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return String(raw);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
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);
|
|
16
|
+
if (!match)
|
|
17
|
+
return { path: null, line: null };
|
|
18
|
+
try {
|
|
19
|
+
return { path: assertSafePath(root, match[1]), line: Number(match[2]) };
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return { path: null, line: null };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function makeProvenance(options, text, resolution = 'resolved') {
|
|
26
|
+
const location = locate(text, options.root);
|
|
27
|
+
const file = location.path ? options.snapshot.files.find(item => item.path === location.path) : undefined;
|
|
28
|
+
return {
|
|
29
|
+
backend: options.adapter.name, version: options.adapter.version, source_path: location.path, content_hash: file?.hash ?? null,
|
|
30
|
+
byte_range: null, origin: options.origin ?? (options.adapter.semantic ? 'lsp' : 'parser'), resolution, freshness: 'current',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function evidenceId(provenance, excerpt) {
|
|
34
|
+
return `evidence-${hash(JSON.stringify({ provenance, excerpt })).slice(0, 24)}`;
|
|
35
|
+
}
|
|
36
|
+
export function normalizeItems(raw, options, kind = 'code', limit = 100) {
|
|
37
|
+
const full = textOf(raw);
|
|
38
|
+
const chunks = Array.isArray(raw) ? raw : (raw && typeof raw === 'object' && Array.isArray(raw.results) ? raw.results : [raw]);
|
|
39
|
+
const provenance = [];
|
|
40
|
+
const items = [];
|
|
41
|
+
const maxChars = options.maxChars ?? 12000;
|
|
42
|
+
for (const candidate of chunks.slice(0, limit)) {
|
|
43
|
+
const excerpt = textOf(candidate).slice(0, maxChars);
|
|
44
|
+
if (!excerpt)
|
|
45
|
+
continue;
|
|
46
|
+
const p = makeProvenance(options, excerpt, excerpt.toLowerCase().includes('ambiguous') ? 'ambiguous' : 'resolved');
|
|
47
|
+
const id = evidenceId(p, excerpt);
|
|
48
|
+
provenance.push(p);
|
|
49
|
+
items.push({ item_id: `item-${hash(excerpt).slice(0, 24)}`, kind, path: p.source_path, excerpt, evidence_ids: [id], rank: items.length });
|
|
50
|
+
}
|
|
51
|
+
if (items.length === 0 && full) {
|
|
52
|
+
const excerpt = full.slice(0, maxChars);
|
|
53
|
+
const p = makeProvenance(options, excerpt, 'unresolved');
|
|
54
|
+
const id = evidenceId(p, excerpt);
|
|
55
|
+
provenance.push(p);
|
|
56
|
+
items.push({ item_id: `item-${hash(excerpt).slice(0, 24)}`, kind, path: p.source_path, excerpt, evidence_ids: [id], rank: 0 });
|
|
57
|
+
}
|
|
58
|
+
return { items, provenance };
|
|
59
|
+
}
|
|
60
|
+
export function normalizeSymbols(raw, options) {
|
|
61
|
+
const normalized = normalizeItems(raw, options, 'symbol');
|
|
62
|
+
const candidates = Array.isArray(raw) ? raw : (raw && typeof raw === 'object' && Array.isArray(raw.results) ? raw.results : []);
|
|
63
|
+
const symbols = normalized.items.map((item, index) => {
|
|
64
|
+
const candidate = candidates[index] && typeof candidates[index] === 'object' ? candidates[index] : {};
|
|
65
|
+
const namePath = String(candidate.name_path ?? candidate.namePath ?? candidate.name ?? item.excerpt.split(/\s|\(|\{/u)[0] ?? 'unknown');
|
|
66
|
+
const path = typeof candidate.relative_path === 'string' ? candidate.relative_path : item.path;
|
|
67
|
+
const stableId = `serena:${encodeURIComponent(JSON.stringify({ name_path: namePath, relative_path: path ?? '' }))}`;
|
|
68
|
+
return { symbol_id: stableId, name: String(candidate.name ?? namePath.split('/').at(-1) ?? namePath), path, kind: String(candidate.kind ?? 'unknown'), signature: item.excerpt.slice(0, 500), evidence_ids: item.evidence_ids };
|
|
69
|
+
});
|
|
70
|
+
return { symbols, references: [], diagnostics: [], provenance: normalized.provenance };
|
|
71
|
+
}
|
|
72
|
+
export function normalizeEdges(raw, options, relation) {
|
|
73
|
+
const normalized = normalizeItems(raw, options, 'code');
|
|
74
|
+
const nodes = normalized.items.map(item => ({ id: item.item_id, label: item.excerpt.slice(0, 160), path: item.path, evidence_ids: item.evidence_ids }));
|
|
75
|
+
const edges = nodes.slice(1).map((node, index) => ({ from: nodes[index].id, to: node.id, relation, evidence_ids: node.evidence_ids }));
|
|
76
|
+
return { nodes, edges, provenance: normalized.provenance };
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
/** Small, bounded stdio MCP client. It supports Graft's newline JSON-RPC and the
|
|
3
|
+
* Content-Length framing used by the Python MCP SDK (Graphify and Serena). */
|
|
4
|
+
export class McpStdioClient {
|
|
5
|
+
command;
|
|
6
|
+
args;
|
|
7
|
+
cwd;
|
|
8
|
+
framing;
|
|
9
|
+
maxBytes;
|
|
10
|
+
child;
|
|
11
|
+
buffer = Buffer.alloc(0);
|
|
12
|
+
pending = new Map();
|
|
13
|
+
nextId = 1;
|
|
14
|
+
stderrBytes = 0;
|
|
15
|
+
constructor(command, args, cwd, framing = 'content-length', maxBytes = 2_000_000) {
|
|
16
|
+
this.command = command;
|
|
17
|
+
this.args = args;
|
|
18
|
+
this.cwd = cwd;
|
|
19
|
+
this.framing = framing;
|
|
20
|
+
this.maxBytes = maxBytes;
|
|
21
|
+
}
|
|
22
|
+
async call(tool, arguments_, timeoutMs) {
|
|
23
|
+
await this.start(timeoutMs);
|
|
24
|
+
return this.request('tools/call', { name: tool, arguments: arguments_ }, timeoutMs);
|
|
25
|
+
}
|
|
26
|
+
async close() {
|
|
27
|
+
if (!this.child)
|
|
28
|
+
return;
|
|
29
|
+
const child = this.child;
|
|
30
|
+
this.child = undefined;
|
|
31
|
+
for (const pending of this.pending.values()) {
|
|
32
|
+
clearTimeout(pending.timer);
|
|
33
|
+
pending.reject(new Error('MCP process closed'));
|
|
34
|
+
}
|
|
35
|
+
this.pending.clear();
|
|
36
|
+
child.kill('SIGTERM');
|
|
37
|
+
await new Promise(resolve => {
|
|
38
|
+
const timer = setTimeout(() => { if (!child.killed)
|
|
39
|
+
child.kill('SIGKILL'); resolve(); }, 500);
|
|
40
|
+
child.once('close', () => { clearTimeout(timer); resolve(); });
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async start(timeoutMs) {
|
|
44
|
+
if (this.child)
|
|
45
|
+
return;
|
|
46
|
+
const child = spawn(this.command, this.args, { cwd: this.cwd, shell: false, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, NO_COLOR: '1' } });
|
|
47
|
+
this.child = child;
|
|
48
|
+
child.stdout.on('data', chunk => this.consume(Buffer.from(chunk)));
|
|
49
|
+
child.stderr.on('data', chunk => { this.stderrBytes += Buffer.byteLength(chunk); if (this.stderrBytes > this.maxBytes)
|
|
50
|
+
void this.close(); });
|
|
51
|
+
child.on('error', error => this.fail(new Error(`MCP backend failed to start: ${error.message}`)));
|
|
52
|
+
child.on('close', (code, signal) => { if (this.child === child)
|
|
53
|
+
this.child = undefined; this.fail(new Error(`MCP backend exited (${code ?? 'null'}${signal ? `, ${signal}` : ''})`)); });
|
|
54
|
+
await this.request('initialize', { protocolVersion: '2025-11-25', capabilities: {}, clientInfo: { name: 'yoke-code-intelligence', version: '0.1.0' } }, timeoutMs);
|
|
55
|
+
await this.notify('notifications/initialized', {});
|
|
56
|
+
}
|
|
57
|
+
notify(method, params) {
|
|
58
|
+
const payload = JSON.stringify({ jsonrpc: '2.0', method, params });
|
|
59
|
+
this.write(payload);
|
|
60
|
+
return Promise.resolve();
|
|
61
|
+
}
|
|
62
|
+
request(method, params, timeoutMs) {
|
|
63
|
+
if (!this.child)
|
|
64
|
+
return Promise.reject(new Error('MCP process is not running'));
|
|
65
|
+
const id = this.nextId++;
|
|
66
|
+
this.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }));
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const timer = setTimeout(() => { this.pending.delete(id); reject(new Error(`MCP request timed out: ${method}`)); void this.close(); }, timeoutMs);
|
|
69
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
write(payload) {
|
|
73
|
+
if (!this.child?.stdin.writable)
|
|
74
|
+
throw new Error('MCP stdin is closed');
|
|
75
|
+
if (this.framing === 'line')
|
|
76
|
+
this.child.stdin.write(`${payload}\n`);
|
|
77
|
+
else
|
|
78
|
+
this.child.stdin.write(`Content-Length: ${Buffer.byteLength(payload, 'utf8')}\r\n\r\n${payload}`);
|
|
79
|
+
}
|
|
80
|
+
consume(chunk) {
|
|
81
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
82
|
+
if (this.buffer.byteLength > this.maxBytes) {
|
|
83
|
+
void this.close();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
while (this.buffer.byteLength > 0) {
|
|
87
|
+
if (this.framing === 'line') {
|
|
88
|
+
const newline = this.buffer.indexOf(10);
|
|
89
|
+
if (newline < 0)
|
|
90
|
+
return;
|
|
91
|
+
const line = this.buffer.subarray(0, newline).toString('utf8').trim();
|
|
92
|
+
this.buffer = this.buffer.subarray(newline + 1);
|
|
93
|
+
if (line)
|
|
94
|
+
this.accept(line);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const headerEnd = this.buffer.indexOf('\r\n\r\n');
|
|
98
|
+
if (headerEnd < 0)
|
|
99
|
+
return;
|
|
100
|
+
const header = this.buffer.subarray(0, headerEnd).toString('ascii');
|
|
101
|
+
const match = /Content-Length:\s*(\d+)/i.exec(header);
|
|
102
|
+
if (!match) {
|
|
103
|
+
this.buffer = this.buffer.subarray(headerEnd + 4);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const length = Number(match[1]);
|
|
107
|
+
const start = headerEnd + 4;
|
|
108
|
+
if (this.buffer.byteLength < start + length)
|
|
109
|
+
return;
|
|
110
|
+
const body = this.buffer.subarray(start, start + length).toString('utf8');
|
|
111
|
+
this.buffer = this.buffer.subarray(start + length);
|
|
112
|
+
this.accept(body);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
accept(raw) {
|
|
116
|
+
try {
|
|
117
|
+
const message = JSON.parse(raw);
|
|
118
|
+
if (typeof message.id !== 'number')
|
|
119
|
+
return;
|
|
120
|
+
const pending = this.pending.get(message.id);
|
|
121
|
+
if (!pending)
|
|
122
|
+
return;
|
|
123
|
+
this.pending.delete(message.id);
|
|
124
|
+
clearTimeout(pending.timer);
|
|
125
|
+
if (message.error)
|
|
126
|
+
pending.reject(new Error(message.error.message ?? 'MCP request failed'));
|
|
127
|
+
else
|
|
128
|
+
pending.resolve(message.result);
|
|
129
|
+
}
|
|
130
|
+
catch { /* backend log/noise on stdout is ignored; protocol errors surface by timeout */ }
|
|
131
|
+
}
|
|
132
|
+
fail(error) {
|
|
133
|
+
for (const pending of this.pending.values()) {
|
|
134
|
+
clearTimeout(pending.timer);
|
|
135
|
+
pending.reject(error);
|
|
136
|
+
}
|
|
137
|
+
this.pending.clear();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -10,32 +10,46 @@ export function estimateSchedule(stories, maxConcurrency, history) {
|
|
|
10
10
|
if (!estimate)
|
|
11
11
|
return { available: false, reason: 'No measured duration history' };
|
|
12
12
|
const ranks = criticalPathRanks(stories);
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
13
|
+
const durations = new Map(stories.filter(story => !story.passes).map(story => [story.id,
|
|
14
|
+
estimateDurations(history.filter(item => item.storyId === story.id).map(item => item.ms)) ?? estimate,
|
|
15
|
+
]));
|
|
16
|
+
const simulate = (field) => {
|
|
17
|
+
const pending = stories.filter(story => !story.passes).sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0) || (ranks.get(b.id) ?? 0) - (ranks.get(a.id) ?? 0) || a.id.localeCompare(b.id));
|
|
18
|
+
const complete = new Set(stories.filter(story => story.passes).map(story => story.id));
|
|
19
|
+
const active = [];
|
|
20
|
+
const tasks = [];
|
|
21
|
+
let time = 0;
|
|
22
|
+
while (pending.length || active.length) {
|
|
23
|
+
for (let index = 0; index < pending.length && active.length < maxConcurrency;) {
|
|
24
|
+
const story = pending[index];
|
|
25
|
+
if ((story.needs ?? []).every(id => complete.has(id)) && (!story.area || !active.some(item => item.story.area === story.area)) && !active.some(item => writeScopesOverlap(story.writes, item.story.writes))) {
|
|
26
|
+
const end = time + durations.get(story.id)[field];
|
|
27
|
+
active.push({ story, end });
|
|
28
|
+
tasks.push({ storyId: story.id, startMs: time, endMs: end });
|
|
29
|
+
pending.splice(index, 1);
|
|
30
|
+
}
|
|
31
|
+
else
|
|
32
|
+
index++;
|
|
27
33
|
}
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
if (!active.length)
|
|
35
|
+
return undefined;
|
|
36
|
+
time = Math.min(...active.map(item => item.end));
|
|
37
|
+
for (let index = active.length - 1; index >= 0; index--)
|
|
38
|
+
if (active[index].end === time) {
|
|
39
|
+
complete.add(active[index].story.id);
|
|
40
|
+
active.splice(index, 1);
|
|
41
|
+
}
|
|
30
42
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
return { time, tasks };
|
|
44
|
+
};
|
|
45
|
+
const typical = simulate('typicalMs'), lower = simulate('lowerMs'), upper = simulate('upperMs');
|
|
46
|
+
if (!typical || !lower || !upper)
|
|
47
|
+
return { available: false, reason: 'Cyclic or missing dependencies' };
|
|
48
|
+
// Different durations can change a greedy schedule's ordering. Keep the
|
|
49
|
+
// empirical scenario envelope ordered; this is not a calibrated interval.
|
|
50
|
+
return { available: true, etaMs: typical.time,
|
|
51
|
+
lowerMs: Math.min(lower.time, typical.time, upper.time), upperMs: Math.max(lower.time, typical.time, upper.time),
|
|
52
|
+
sampleCount: estimate.sampleCount,
|
|
53
|
+
confidence: [...durations.values()].some(duration => duration.confidence === 'low') ? 'low' : estimate.confidence,
|
|
54
|
+
tasks: typical.tasks };
|
|
41
55
|
}
|
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 } } : {}),
|