@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
package/dist/cli.js
CHANGED
|
@@ -22,6 +22,7 @@ import { runUpgrade } from './update/upgrade.js';
|
|
|
22
22
|
import { AGENT_LIST, isSupportedAgent, SUPPORTED_AGENTS } from './agents/catalog.js';
|
|
23
23
|
import { printAudit, runAudit } from './audit/command.js';
|
|
24
24
|
import { runSetup } from './setup/command.js';
|
|
25
|
+
import { runCodeIntelligenceServer } from './code-intelligence/mcp-server.js';
|
|
25
26
|
import { pendingChanges, queueChange } from './change/inbox.js';
|
|
26
27
|
import { answerPendingDecision, answeredDecisionResumeIsValid, clearDecisionResume, decisionProcessingExists, decisionResumeMatchesCurrent, finalizeCommittedDecisionResume, formatPendingDecision, readDecisionResume, readPendingDecision, writeDecisionResume, } from './loop/decision.js';
|
|
27
28
|
export { runRetrofit } from './retrofit/command.js';
|
|
@@ -104,6 +105,8 @@ export function parseQualityFlags(args) {
|
|
|
104
105
|
export function main(argv) {
|
|
105
106
|
const [cmd, ...rest] = argv;
|
|
106
107
|
switch (cmd) {
|
|
108
|
+
case 'code-intelligence-server':
|
|
109
|
+
return runCodeIntelligenceServer(rest).then(() => 0).catch(error => { console.error(`Code intelligence server: ${error.message}`); return 2; });
|
|
107
110
|
case 'setup': {
|
|
108
111
|
const targetDir = rest.find(a => !a.startsWith('-')) ?? '.';
|
|
109
112
|
const valid = [...SUPPORTED_AGENTS];
|
|
@@ -130,6 +133,11 @@ export function main(argv) {
|
|
|
130
133
|
console.error(`Invalid --code-graph value: ${graphArg}`);
|
|
131
134
|
return 1;
|
|
132
135
|
}
|
|
136
|
+
const intelligenceArg = rest.find(a => a.startsWith('--code-intelligence='))?.slice('--code-intelligence='.length);
|
|
137
|
+
if (intelligenceArg && !['off', 'shadow', 'active'].includes(intelligenceArg)) {
|
|
138
|
+
console.error(`Invalid --code-intelligence value: ${intelligenceArg}`);
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
133
141
|
const policyArg = rest.find(a => a.startsWith('--decision-policy='))?.slice('--decision-policy='.length);
|
|
134
142
|
if (policyArg && policyArg !== 'auto' && policyArg !== 'critical') {
|
|
135
143
|
console.error(`Invalid --decision-policy value: ${policyArg}`);
|
|
@@ -152,6 +160,7 @@ export function main(argv) {
|
|
|
152
160
|
modelProviders: modelProviders,
|
|
153
161
|
host: hostArg, agents, runner: runnerArg,
|
|
154
162
|
codeGraph: graphArg,
|
|
163
|
+
codeIntelligence: intelligenceArg,
|
|
155
164
|
loop, routing, decisionPolicy: policyArg,
|
|
156
165
|
routingStrategy: routingStrategy,
|
|
157
166
|
routingPreset: rest.includes('--routing-preset'),
|
|
@@ -291,7 +300,12 @@ export function main(argv) {
|
|
|
291
300
|
console.error(`Invalid --code-graph value: ${cgArg} (expected graphify|serena)`);
|
|
292
301
|
return 1;
|
|
293
302
|
}
|
|
294
|
-
|
|
303
|
+
const ciArg = rest.find(a => a.startsWith('--code-intelligence='))?.slice('--code-intelligence='.length);
|
|
304
|
+
if (ciArg && !['off', 'shadow', 'active'].includes(ciArg)) {
|
|
305
|
+
console.error(`Invalid --code-intelligence value: ${ciArg} (expected off|shadow|active)`);
|
|
306
|
+
return 1;
|
|
307
|
+
}
|
|
308
|
+
return runRetrofit(targetDir, { loop, agents, codeGraph, codeIntelligence: ciArg });
|
|
295
309
|
}
|
|
296
310
|
case 'change': {
|
|
297
311
|
const sub = rest[0];
|
|
@@ -657,7 +671,7 @@ export function main(argv) {
|
|
|
657
671
|
return runUpgrade();
|
|
658
672
|
default:
|
|
659
673
|
console.log('Project workflows: yoke check [dir] [--json|--protect] | goal set|run|resume|pause|status|handoff|budget [dir] | projects add|list|remove | dashboard [dir] [--port=N]');
|
|
660
|
-
console.log(`usage: yoke <setup [dir] | new <dir> [--idea="..."] | validate [canonDir] | retrofit [targetDir] [--agent=${AGENT_LIST}|all] [--code-graph=graphify|serena] [--loop] | change <add|status> [dir] | prd <draft|check|assess> [dir] | loop <on|off|status|decision|answer|resume|run|cleanup> | context <init|status> | review [dir]
|
|
674
|
+
console.log(`usage: yoke <setup [dir] | new <dir> [--idea="..."] | validate [canonDir] | retrofit [targetDir] [--agent=${AGENT_LIST}|all] [--code-graph=graphify|serena] [--code-intelligence=off|shadow|active] [--loop] | change <add|status> [dir] | code-intelligence-server --workspace=<dir> --mode=<mode> | prd <draft|check|assess> [dir] | loop <on|off|status|decision|answer|resume|run|cleanup> | context <init|status> | review [dir] | design-scan [dir] | flow-smoke [dir] | upgrade>`);
|
|
661
675
|
return cmd ? 1 : 0;
|
|
662
676
|
}
|
|
663
677
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { McpBackendAdapter } from './mcp.js';
|
|
2
|
+
export const GRAFT_VERSION = '0.17.0';
|
|
3
|
+
export function createGraftAdapter(cwd, override) {
|
|
4
|
+
return new McpBackendAdapter({
|
|
5
|
+
name: 'graft', version: GRAFT_VERSION, cwd, command: override?.command ?? 'graft', args: override?.args ?? ['mcp'], framing: 'line', semantic: false, documents: false,
|
|
6
|
+
aliases: { context: 'graft_find_code', trace: 'graft_trace_calls', file: 'graft_file_api', freshness: 'graft_check_freshness', repo_map: 'graft_repo_map' },
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
export function graftCall(kind, args) {
|
|
10
|
+
return { tool: kind, arguments: args };
|
|
11
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { McpBackendAdapter } from './mcp.js';
|
|
2
|
+
export const GRAPHIFY_VERSION = '0.9.56';
|
|
3
|
+
export function createGraphifyAdapter(cwd, override) {
|
|
4
|
+
return new McpBackendAdapter({
|
|
5
|
+
name: 'graphify', version: GRAPHIFY_VERSION, cwd, command: override?.command ?? 'python', args: override?.args ?? ['-m', 'graphify.serve'], framing: 'content-length', semantic: false, documents: true,
|
|
6
|
+
aliases: { context: 'query_graph', symbol: 'get_node', neighbors: 'get_neighbors', trace: 'shortest_path', impact: 'get_pr_impact' },
|
|
7
|
+
});
|
|
8
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { McpStdioClient } from '../mcp-client.js';
|
|
2
|
+
function valueFromResult(value) {
|
|
3
|
+
if (!value || typeof value !== 'object')
|
|
4
|
+
return value;
|
|
5
|
+
if (value.structuredContent !== undefined)
|
|
6
|
+
return value.structuredContent;
|
|
7
|
+
const text = Array.isArray(value.content) ? value.content.find((item) => item?.type === 'text')?.text : undefined;
|
|
8
|
+
if (typeof text !== 'string')
|
|
9
|
+
return value;
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(text);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return { text };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class McpBackendAdapter {
|
|
18
|
+
name;
|
|
19
|
+
version;
|
|
20
|
+
semantic;
|
|
21
|
+
documents;
|
|
22
|
+
client;
|
|
23
|
+
aliases;
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.name = options.name;
|
|
26
|
+
this.version = options.version;
|
|
27
|
+
this.semantic = options.semantic;
|
|
28
|
+
this.documents = options.documents;
|
|
29
|
+
this.aliases = options.aliases;
|
|
30
|
+
this.client = new McpStdioClient(options.command, options.args, options.cwd, options.framing);
|
|
31
|
+
}
|
|
32
|
+
async call(request, timeoutMs) {
|
|
33
|
+
const tool = this.aliases[request.tool] ?? request.tool;
|
|
34
|
+
return valueFromResult(await this.client.call(tool, request.arguments, timeoutMs));
|
|
35
|
+
}
|
|
36
|
+
close() { return this.client.close(); }
|
|
37
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { McpBackendAdapter } from './mcp.js';
|
|
2
|
+
export const SERENA_VERSION = '1.7.1-dev';
|
|
3
|
+
export function createSerenaAdapter(cwd, override) {
|
|
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: 'content-length', semantic: true, documents: false,
|
|
6
|
+
aliases: {
|
|
7
|
+
symbol: 'find_symbol', references: 'find_referencing_symbols', implementations: 'find_implementations', overview: 'get_symbols_overview', diagnostics: 'get_diagnostics_for_file',
|
|
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',
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const CodeIntelligenceModeSchema = z.enum(['off', 'shadow', 'active']);
|
|
3
|
+
export const BackendNameSchema = z.enum(['graft', 'graphify', 'serena-lsp', 'serena-jetbrains', 'yoke']);
|
|
4
|
+
export const StatusSchema = z.enum(['success', 'partial', 'blocked', 'error']);
|
|
5
|
+
export const ResolutionSchema = z.enum(['resolved', 'unresolved', 'ambiguous', 'not_applicable']);
|
|
6
|
+
export const FreshnessSchema = z.enum(['current', 'stale', 'unknown']);
|
|
7
|
+
export const ProvenanceSchema = z.object({
|
|
8
|
+
backend: BackendNameSchema,
|
|
9
|
+
version: z.string().min(1),
|
|
10
|
+
source_path: z.string().nullable(),
|
|
11
|
+
content_hash: z.string().nullable(),
|
|
12
|
+
byte_range: z.object({ start: z.number().int().nonnegative(), end: z.number().int().nonnegative() }).nullable(),
|
|
13
|
+
origin: z.enum(['parser', 'resolver', 'lsp', 'ide', 'llm', 'manual']),
|
|
14
|
+
resolution: ResolutionSchema,
|
|
15
|
+
freshness: FreshnessSchema,
|
|
16
|
+
});
|
|
17
|
+
export const CoverageSchema = z.object({
|
|
18
|
+
backends_requested: z.array(BackendNameSchema),
|
|
19
|
+
backends_used: z.array(BackendNameSchema),
|
|
20
|
+
backends_missing: z.array(BackendNameSchema),
|
|
21
|
+
structural: z.enum(['complete', 'partial', 'unavailable']),
|
|
22
|
+
semantic: z.enum(['complete', 'partial', 'unavailable']),
|
|
23
|
+
documents: z.enum(['complete', 'partial', 'unavailable']),
|
|
24
|
+
});
|
|
25
|
+
export const MetricsSchema = z.object({
|
|
26
|
+
latency_ms: z.number().nonnegative(),
|
|
27
|
+
result_tokens: z.number().int().nonnegative(),
|
|
28
|
+
token_count_kind: z.enum(['exact', 'estimated']),
|
|
29
|
+
returned_bytes: z.number().int().nonnegative(),
|
|
30
|
+
});
|
|
31
|
+
export const ErrorSchema = z.object({
|
|
32
|
+
code: z.enum([
|
|
33
|
+
'INVALID_ARGUMENT', 'WORKSPACE_DENIED', 'CAPABILITY_UNAVAILABLE', 'SNAPSHOT_STALE',
|
|
34
|
+
'AMBIGUOUS_SYMBOL', 'BACKEND_TIMEOUT', 'BACKEND_UNAVAILABLE', 'BUDGET_EXCEEDED',
|
|
35
|
+
'APPROVAL_REQUIRED', 'PLAN_EXPIRED', 'SCOPE_VIOLATION', 'VALIDATION_FAILED',
|
|
36
|
+
'WRITE_CONFLICT', 'IDEMPOTENCY_CONFLICT', 'INTERNAL_ERROR',
|
|
37
|
+
]),
|
|
38
|
+
message: z.string().min(1),
|
|
39
|
+
backend: BackendNameSchema.optional(),
|
|
40
|
+
});
|
|
41
|
+
export const ItemSchema = z.object({
|
|
42
|
+
item_id: z.string().min(1),
|
|
43
|
+
kind: z.enum(['symbol', 'code', 'document', 'memory', 'summary']),
|
|
44
|
+
path: z.string().nullable(),
|
|
45
|
+
excerpt: z.string().max(12000),
|
|
46
|
+
evidence_ids: z.array(z.string().min(1)),
|
|
47
|
+
rank: z.number().nonnegative(),
|
|
48
|
+
});
|
|
49
|
+
export const SymbolSchema = z.object({
|
|
50
|
+
symbol_id: z.string().min(1),
|
|
51
|
+
name: z.string().min(1),
|
|
52
|
+
path: z.string().nullable(),
|
|
53
|
+
kind: z.string().min(1),
|
|
54
|
+
signature: z.string().nullable(),
|
|
55
|
+
evidence_ids: z.array(z.string().min(1)),
|
|
56
|
+
});
|
|
57
|
+
export const ReferenceSchema = z.object({
|
|
58
|
+
symbol_id: z.string().min(1),
|
|
59
|
+
path: z.string().nullable(),
|
|
60
|
+
line: z.number().int().positive().nullable(),
|
|
61
|
+
excerpt: z.string().max(4000),
|
|
62
|
+
evidence_ids: z.array(z.string().min(1)),
|
|
63
|
+
});
|
|
64
|
+
export const DiagnosticSchema = z.object({
|
|
65
|
+
path: z.string().nullable(),
|
|
66
|
+
line: z.number().int().positive().nullable(),
|
|
67
|
+
severity: z.enum(['error', 'warning', 'info', 'unknown']),
|
|
68
|
+
message: z.string().max(4000),
|
|
69
|
+
evidence_ids: z.array(z.string().min(1)),
|
|
70
|
+
});
|
|
71
|
+
export const EdgeSchema = z.object({
|
|
72
|
+
from: z.string().min(1),
|
|
73
|
+
to: z.string().min(1),
|
|
74
|
+
relation: z.enum(['calls', 'references', 'imports', 'implements', 'extends', 'docs', 'rationale_for', 'related_to']),
|
|
75
|
+
evidence_ids: z.array(z.string().min(1)),
|
|
76
|
+
});
|
|
77
|
+
export const ResponseBaseSchema = z.object({
|
|
78
|
+
schema_version: z.literal('0.1.0'),
|
|
79
|
+
request_id: z.string().min(1),
|
|
80
|
+
workspace_id: z.string().min(1),
|
|
81
|
+
snapshot_id: z.string().min(1),
|
|
82
|
+
status: StatusSchema,
|
|
83
|
+
coverage: CoverageSchema,
|
|
84
|
+
provenance: z.array(ProvenanceSchema),
|
|
85
|
+
warnings: z.array(z.string()),
|
|
86
|
+
metrics: MetricsSchema,
|
|
87
|
+
artifact_uri: z.string().nullable(),
|
|
88
|
+
error: ErrorSchema.nullable(),
|
|
89
|
+
});
|
|
90
|
+
const BudgetSchema = z.object({
|
|
91
|
+
token_budget: z.number().int().min(128).max(16000).default(2400),
|
|
92
|
+
timeout_ms: z.number().int().min(100).max(60000).default(5000),
|
|
93
|
+
});
|
|
94
|
+
export const ContextInputSchema = z.object({
|
|
95
|
+
workspace_id: z.string().min(1), query: z.string().min(1).max(20000), snapshot_id: z.string().min(1).optional(),
|
|
96
|
+
mode: z.enum(['orient', 'locate', 'explain']).default('orient'), paths: z.array(z.string().min(1)).max(32).default([]),
|
|
97
|
+
include_docs: z.boolean().default(false), ...BudgetSchema.shape,
|
|
98
|
+
});
|
|
99
|
+
export const SymbolInputSchema = z.object({
|
|
100
|
+
workspace_id: z.string().min(1), snapshot_id: z.string().min(1), symbol_id: z.string().min(1).optional(),
|
|
101
|
+
query: z.string().min(1).optional(), path: z.string().min(1).optional(),
|
|
102
|
+
include: z.array(z.enum(['definition', 'references', 'implementations', 'diagnostics'])).default(['definition']), ...BudgetSchema.shape,
|
|
103
|
+
}).refine(v => Boolean(v.symbol_id) !== Boolean(v.query), 'exactly one of symbol_id or query is required');
|
|
104
|
+
export const TraceInputSchema = z.object({
|
|
105
|
+
workspace_id: z.string().min(1), snapshot_id: z.string().min(1), symbol_id: z.string().min(1),
|
|
106
|
+
direction: z.enum(['in', 'out']), relations: z.array(z.enum(['calls', 'references', 'imports', 'implements', 'extends'])).min(1),
|
|
107
|
+
max_depth: z.number().int().min(1).max(6).default(2), max_nodes: z.number().int().min(1).max(2000).default(200),
|
|
108
|
+
require_resolved: z.boolean().default(false), timeout_ms: z.number().int().min(100).max(60000).default(5000),
|
|
109
|
+
});
|
|
110
|
+
const TargetSchema = z.union([z.object({ path: z.string().min(1) }), z.object({ symbol_id: z.string().min(1) })]);
|
|
111
|
+
export const ImpactInputSchema = z.object({
|
|
112
|
+
workspace_id: z.string().min(1), snapshot_id: z.string().min(1), targets: z.array(TargetSchema).max(50).optional(),
|
|
113
|
+
plan_id: z.string().min(1).optional(), include_docs: z.boolean().default(true), require_semantic: z.boolean().default(true),
|
|
114
|
+
max_depth: z.number().int().min(1).max(6).default(3), timeout_ms: z.number().int().min(100).max(60000).default(10000),
|
|
115
|
+
}).refine(v => Boolean(v.targets) !== Boolean(v.plan_id), 'exactly one of targets or plan_id is required');
|
|
116
|
+
const OperationSchema = z.discriminatedUnion('kind', [
|
|
117
|
+
z.object({ kind: z.literal('rename'), symbol_id: z.string().min(1), new_name: z.string().regex(/^[A-Za-z_$][\w$]*$/) }),
|
|
118
|
+
z.object({ kind: z.literal('replace_symbol_body'), symbol_id: z.string().min(1), content: z.string().max(500000) }),
|
|
119
|
+
z.object({ kind: z.literal('insert_before_symbol'), symbol_id: z.string().min(1), content: z.string().max(500000) }),
|
|
120
|
+
z.object({ kind: z.literal('insert_after_symbol'), symbol_id: z.string().min(1), content: z.string().max(500000) }),
|
|
121
|
+
z.object({ kind: z.literal('safe_delete'), symbol_id: z.string().min(1) }),
|
|
122
|
+
]);
|
|
123
|
+
export const EditPreviewInputSchema = z.object({
|
|
124
|
+
workspace_id: z.string().min(1), snapshot_id: z.string().min(1), operations: z.array(OperationSchema).min(1).max(20),
|
|
125
|
+
validation_profile: z.enum(['default', 'extended']), timeout_ms: z.number().int().min(100).max(600000).default(120000),
|
|
126
|
+
});
|
|
127
|
+
export const EditApplyInputSchema = z.object({
|
|
128
|
+
workspace_id: z.string().min(1), plan_id: z.string().min(1), expected_snapshot_id: z.string().min(1),
|
|
129
|
+
idempotency_key: z.string().min(8).max(128),
|
|
130
|
+
});
|
|
131
|
+
export const TOOL_NAMES = ['code_context', 'code_symbol', 'code_trace', 'code_impact', 'code_edit_preview', 'code_edit_apply'];
|
|
132
|
+
export const TOOL_DEFINITIONS = TOOL_NAMES.map(name => ({
|
|
133
|
+
name,
|
|
134
|
+
description: `Yoke-controlled ${name.replace('code_', '').replace('_', ' ')} with snapshot-bound evidence.`,
|
|
135
|
+
inputSchema: { type: 'object', additionalProperties: false },
|
|
136
|
+
annotations: { openWorldHint: false, destructiveHint: name === 'code_edit_apply', readOnlyHint: name !== 'code_edit_preview' && name !== 'code_edit_apply', idempotentHint: name !== 'code_edit_apply' },
|
|
137
|
+
}));
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { ContextInputSchema, EditApplyInputSchema, EditPreviewInputSchema, ImpactInputSchema, SymbolInputSchema, TraceInputSchema, } from './contracts.js';
|
|
5
|
+
import { createGraftAdapter, createGraphifyAdapter, createSerenaAdapter } from './adapters/index.js';
|
|
6
|
+
import { normalizeEdges, normalizeItems, normalizeSymbols } from './evidence.js';
|
|
7
|
+
import { assertSnapshotCurrent, createSnapshot, loadSnapshot, workspaceId } from './snapshots.js';
|
|
8
|
+
import { applyEditPlan } from './transactions.js';
|
|
9
|
+
import { createEditPlan, loadEditPlan } from './edit-plans.js';
|
|
10
|
+
function nowMs() { return Date.now(); }
|
|
11
|
+
function compact(value, maxBytes) {
|
|
12
|
+
const encoded = JSON.stringify(value) ?? '';
|
|
13
|
+
if (Buffer.byteLength(encoded) <= maxBytes)
|
|
14
|
+
return value;
|
|
15
|
+
return { truncated: true, excerpt: encoded.slice(0, Math.max(0, maxBytes - 64)) };
|
|
16
|
+
}
|
|
17
|
+
function statusFor(missing, hard = false) { return hard ? 'error' : missing.length ? 'partial' : 'success'; }
|
|
18
|
+
function errorCode(message) {
|
|
19
|
+
if (/outside|workspace|symbolic/u.test(message))
|
|
20
|
+
return 'WORKSPACE_DENIED';
|
|
21
|
+
if (/stale/u.test(message))
|
|
22
|
+
return 'SNAPSHOT_STALE';
|
|
23
|
+
if (/approval/u.test(message))
|
|
24
|
+
return 'APPROVAL_REQUIRED';
|
|
25
|
+
if (/expired/u.test(message))
|
|
26
|
+
return 'PLAN_EXPIRED';
|
|
27
|
+
if (/idempotency/u.test(message))
|
|
28
|
+
return 'IDEMPOTENCY_CONFLICT';
|
|
29
|
+
if (/transaction|lock|conflict/u.test(message))
|
|
30
|
+
return 'WRITE_CONFLICT';
|
|
31
|
+
if (/active|unavailable|MCP/u.test(message))
|
|
32
|
+
return 'CAPABILITY_UNAVAILABLE';
|
|
33
|
+
if (/scope|does not match/u.test(message))
|
|
34
|
+
return 'SCOPE_VIOLATION';
|
|
35
|
+
return 'INVALID_ARGUMENT';
|
|
36
|
+
}
|
|
37
|
+
function symbolArgs(symbol, fallbackPath = '') {
|
|
38
|
+
if (symbol.startsWith('serena:')) {
|
|
39
|
+
try {
|
|
40
|
+
const value = JSON.parse(decodeURIComponent(symbol.slice('serena:'.length)));
|
|
41
|
+
if (value.name_path)
|
|
42
|
+
return { name_path: value.name_path, relative_path: value.relative_path ?? fallbackPath };
|
|
43
|
+
}
|
|
44
|
+
catch { /* accept user-provided names below */ }
|
|
45
|
+
}
|
|
46
|
+
return { name_path: symbol, relative_path: fallbackPath };
|
|
47
|
+
}
|
|
48
|
+
export class CodeIntelligenceCoordinator {
|
|
49
|
+
root;
|
|
50
|
+
workspace_id;
|
|
51
|
+
options;
|
|
52
|
+
adapters;
|
|
53
|
+
constructor(root, options = {}) {
|
|
54
|
+
this.root = root;
|
|
55
|
+
this.workspace_id = workspaceId(root);
|
|
56
|
+
this.options = options;
|
|
57
|
+
this.adapters = {
|
|
58
|
+
graft: options.adapters?.graft ?? createGraftAdapter(root, options.graft),
|
|
59
|
+
graphify: options.adapters?.graphify ?? createGraphifyAdapter(root, options.graphify),
|
|
60
|
+
'serena-lsp': options.adapters?.['serena-lsp'] ?? createSerenaAdapter(root, options.serena),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async close() { await Promise.all(Object.values(this.adapters).map(adapter => adapter.close())); }
|
|
64
|
+
async dispatch(name, input) {
|
|
65
|
+
const requestId = randomUUID();
|
|
66
|
+
const started = nowMs();
|
|
67
|
+
const suppliedWorkspace = typeof input === 'object' && input !== null && typeof input.workspace_id === 'string' ? input.workspace_id : this.workspace_id;
|
|
68
|
+
let snapshot;
|
|
69
|
+
try {
|
|
70
|
+
snapshot = this.snapshotFor(input);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
return this.errorResponse(requestId, suppliedWorkspace, this.safeSnapshotId(), started, error);
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
let result;
|
|
77
|
+
switch (name) {
|
|
78
|
+
case 'code_context':
|
|
79
|
+
result = await this.context(ContextInputSchema.parse(input), snapshot);
|
|
80
|
+
break;
|
|
81
|
+
case 'code_symbol':
|
|
82
|
+
result = await this.symbol(SymbolInputSchema.parse(input), snapshot);
|
|
83
|
+
break;
|
|
84
|
+
case 'code_trace':
|
|
85
|
+
result = await this.trace(TraceInputSchema.parse(input), snapshot);
|
|
86
|
+
break;
|
|
87
|
+
case 'code_impact':
|
|
88
|
+
result = await this.impact(ImpactInputSchema.parse(input), snapshot);
|
|
89
|
+
break;
|
|
90
|
+
case 'code_edit_preview':
|
|
91
|
+
result = await this.preview(EditPreviewInputSchema.parse(input), snapshot);
|
|
92
|
+
break;
|
|
93
|
+
case 'code_edit_apply':
|
|
94
|
+
result = await this.apply(EditApplyInputSchema.parse(input), snapshot);
|
|
95
|
+
break;
|
|
96
|
+
default: throw new Error(`unknown code-intelligence tool: ${name}`);
|
|
97
|
+
}
|
|
98
|
+
const bytes = Buffer.byteLength(JSON.stringify(result.data) ?? '');
|
|
99
|
+
const maxBytes = this.options.limits?.maxBytes ?? 2_000_000;
|
|
100
|
+
return {
|
|
101
|
+
schema_version: '0.1.0', request_id: requestId, workspace_id: this.workspace_id, snapshot_id: snapshot.snapshot_id,
|
|
102
|
+
status: statusFor(result.missing, result.hard), data: compact(result.data, maxBytes), coverage: this.coverage(result.used, result.missing), provenance: result.provenance,
|
|
103
|
+
warnings: result.warnings, metrics: { latency_ms: nowMs() - started, result_tokens: Math.ceil(bytes / 4), token_count_kind: 'estimated', returned_bytes: Math.min(bytes, maxBytes) }, artifact_uri: null, error: null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
return this.errorResponse(requestId, this.workspace_id, snapshot.snapshot_id, started, error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
snapshotFor(input) {
|
|
111
|
+
const candidate = input && typeof input === 'object' ? input : {};
|
|
112
|
+
if (candidate.workspace_id && candidate.workspace_id !== this.workspace_id)
|
|
113
|
+
throw new Error('workspace is not permitted');
|
|
114
|
+
const snapshot = candidate.snapshot_id ? loadSnapshot(this.root, candidate.snapshot_id) : createSnapshot(this.root, this.options.policy, this.workspace_id);
|
|
115
|
+
if (candidate.snapshot_id)
|
|
116
|
+
assertSnapshotCurrent(this.root, snapshot, this.options.policy);
|
|
117
|
+
return snapshot;
|
|
118
|
+
}
|
|
119
|
+
safeSnapshotId() {
|
|
120
|
+
try {
|
|
121
|
+
return createSnapshot(this.root, this.options.policy, this.workspace_id).snapshot_id;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return '0'.repeat(64);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
coverage(used, missing) {
|
|
128
|
+
return {
|
|
129
|
+
backends_requested: [...new Set([...used, ...missing])], backends_used: [...new Set(used)], backends_missing: [...new Set(missing)],
|
|
130
|
+
structural: used.includes('graft') ? 'complete' : 'unavailable',
|
|
131
|
+
semantic: used.includes('serena-lsp') ? 'complete' : 'unavailable',
|
|
132
|
+
documents: used.includes('graphify') ? 'complete' : 'unavailable',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async call(backend, tool, args, timeout, state) {
|
|
136
|
+
const seen = new Set([...state.used, ...state.missing]);
|
|
137
|
+
const maxBackends = this.options.limits?.maxBackends ?? 3;
|
|
138
|
+
if (!seen.has(backend) && seen.size >= maxBackends) {
|
|
139
|
+
state.missing.push(backend);
|
|
140
|
+
state.warnings.push(`${backend}: backend budget exceeded`);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const value = await this.adapters[backend].call({ tool, arguments: args }, Math.min(timeout, this.options.limits?.timeoutMs ?? timeout));
|
|
145
|
+
if (!state.used.includes(backend))
|
|
146
|
+
state.used.push(backend);
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (!state.missing.includes(backend))
|
|
151
|
+
state.missing.push(backend);
|
|
152
|
+
state.warnings.push(`${backend}: ${error.message}`);
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
evidenceOptions(snapshot, backend, maxChars = 12000) {
|
|
157
|
+
return { root: this.root, snapshot, adapter: this.adapters[backend], maxChars };
|
|
158
|
+
}
|
|
159
|
+
async context(input, snapshot) {
|
|
160
|
+
const state = { used: [], missing: [], warnings: [] };
|
|
161
|
+
const provenance = [];
|
|
162
|
+
let items = [];
|
|
163
|
+
const raw = await this.call('graft', 'context', { query: input.query, limit: Math.max(1, Math.floor(input.token_budget / 300)), ...(input.paths[0] ? { in: input.paths[0] } : {}) }, input.timeout_ms, state);
|
|
164
|
+
if (raw !== null) {
|
|
165
|
+
const normalized = normalizeItems(raw, this.evidenceOptions(snapshot, 'graft'), 'code', 100);
|
|
166
|
+
items.push(...normalized.items);
|
|
167
|
+
provenance.push(...normalized.provenance);
|
|
168
|
+
}
|
|
169
|
+
if (input.include_docs) {
|
|
170
|
+
const docs = await this.call('graphify', 'context', { question: input.query, depth: 3, token_budget: input.token_budget }, input.timeout_ms, state);
|
|
171
|
+
if (docs !== null) {
|
|
172
|
+
const normalized = normalizeItems(docs, this.evidenceOptions(snapshot, 'graphify'), 'document', 50);
|
|
173
|
+
items.push(...normalized.items);
|
|
174
|
+
provenance.push(...normalized.provenance);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { data: { items, next_cursor: null }, provenance, ...state, warnings: state.warnings };
|
|
178
|
+
}
|
|
179
|
+
async symbol(input, snapshot) {
|
|
180
|
+
const state = { used: [], missing: [], warnings: [] };
|
|
181
|
+
const provenance = [];
|
|
182
|
+
let symbols = [];
|
|
183
|
+
let references = [];
|
|
184
|
+
let diagnostics = [];
|
|
185
|
+
const name = input.symbol_id ?? input.query;
|
|
186
|
+
const reference = symbolArgs(name, input.path ?? '');
|
|
187
|
+
const graft = await this.call('graft', 'context', { query: name, limit: 10, ...(input.path ? { in: input.path } : {}) }, input.timeout_ms, state);
|
|
188
|
+
if (graft !== null) {
|
|
189
|
+
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
|
+
provenance.push(...normalized.provenance);
|
|
192
|
+
}
|
|
193
|
+
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);
|
|
194
|
+
if (symbol !== null) {
|
|
195
|
+
const normalized = normalizeSymbols(symbol, this.evidenceOptions(snapshot, 'serena-lsp'));
|
|
196
|
+
symbols.push(...normalized.symbols);
|
|
197
|
+
provenance.push(...normalized.provenance);
|
|
198
|
+
}
|
|
199
|
+
if (input.include.includes('references')) {
|
|
200
|
+
const raw = await this.call('serena-lsp', 'references', reference, input.timeout_ms, state);
|
|
201
|
+
if (raw !== null) {
|
|
202
|
+
const normalized = normalizeItems(raw, this.evidenceOptions(snapshot, 'serena-lsp'), 'code', 100);
|
|
203
|
+
references = normalized.items.map(item => ({ symbol_id: input.symbol_id ?? `symbol-${item.item_id.slice(5)}`, path: item.path, line: null, excerpt: item.excerpt, evidence_ids: item.evidence_ids }));
|
|
204
|
+
provenance.push(...normalized.provenance);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (input.include.includes('implementations')) {
|
|
208
|
+
const raw = await this.call('serena-lsp', 'implementations', reference, input.timeout_ms, state);
|
|
209
|
+
if (raw !== null) {
|
|
210
|
+
const normalized = normalizeSymbols(raw, this.evidenceOptions(snapshot, 'serena-lsp'));
|
|
211
|
+
symbols.push(...normalized.symbols);
|
|
212
|
+
provenance.push(...normalized.provenance);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (input.include.includes('diagnostics')) {
|
|
216
|
+
const raw = await this.call('serena-lsp', 'diagnostics', { relative_path: reference.relative_path }, input.timeout_ms, state);
|
|
217
|
+
if (raw !== null) {
|
|
218
|
+
const normalized = normalizeItems(raw, this.evidenceOptions(snapshot, 'serena-lsp'), 'code', 100);
|
|
219
|
+
diagnostics = normalized.items.map(item => ({ path: item.path, line: null, severity: 'unknown', message: item.excerpt, evidence_ids: item.evidence_ids }));
|
|
220
|
+
provenance.push(...normalized.provenance);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return { data: { symbols, references, diagnostics }, provenance, ...state, warnings: state.warnings };
|
|
224
|
+
}
|
|
225
|
+
async trace(input, snapshot) {
|
|
226
|
+
const state = { used: [], missing: [], warnings: [] };
|
|
227
|
+
const provenance = [];
|
|
228
|
+
let nodes = [];
|
|
229
|
+
let edges = [];
|
|
230
|
+
const raw = await this.call('graft', 'trace', { symbol: input.symbol_id, direction: input.direction, depth: input.max_depth }, input.timeout_ms, state);
|
|
231
|
+
if (raw !== null) {
|
|
232
|
+
const normalized = normalizeEdges(raw, this.evidenceOptions(snapshot, 'graft'), input.relations[0] ?? 'references');
|
|
233
|
+
nodes.push(...normalized.nodes);
|
|
234
|
+
edges.push(...normalized.edges);
|
|
235
|
+
provenance.push(...normalized.provenance);
|
|
236
|
+
}
|
|
237
|
+
if (input.require_resolved || input.relations.some(relation => relation === 'implements' || relation === 'extends')) {
|
|
238
|
+
const rawSemantic = await this.call('serena-lsp', 'references', symbolArgs(input.symbol_id), input.timeout_ms, state);
|
|
239
|
+
if (rawSemantic !== null) {
|
|
240
|
+
const normalized = normalizeEdges(rawSemantic, this.evidenceOptions(snapshot, 'serena-lsp'), 'references');
|
|
241
|
+
nodes.push(...normalized.nodes);
|
|
242
|
+
edges.push(...normalized.edges);
|
|
243
|
+
provenance.push(...normalized.provenance);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
nodes = [...new Map(nodes.map(node => [node.id, node])).values()].slice(0, input.max_nodes);
|
|
247
|
+
edges = edges.filter(edge => nodes.some(node => node.id === edge.from) && nodes.some(node => node.id === edge.to));
|
|
248
|
+
return { data: { nodes, edges, frontier_remaining: Math.max(0, edges.length - nodes.length) }, provenance, ...state, warnings: state.warnings };
|
|
249
|
+
}
|
|
250
|
+
async impact(input, snapshot) {
|
|
251
|
+
const state = { used: [], missing: [], warnings: [] };
|
|
252
|
+
const provenance = [];
|
|
253
|
+
const structural = [];
|
|
254
|
+
const semantic = [];
|
|
255
|
+
const documents = [];
|
|
256
|
+
const unresolved = [];
|
|
257
|
+
let targets = input.targets;
|
|
258
|
+
if (!targets && input.plan_id) {
|
|
259
|
+
const plan = loadEditPlan(this.root, input.plan_id);
|
|
260
|
+
if (plan.workspace_id !== this.workspace_id)
|
|
261
|
+
throw new Error('impact plan belongs to another workspace');
|
|
262
|
+
targets = plan.changed_paths.map(path => ({ path }));
|
|
263
|
+
}
|
|
264
|
+
targets ??= [];
|
|
265
|
+
for (const target of targets) {
|
|
266
|
+
const value = 'path' in target ? target.path : target.symbol_id;
|
|
267
|
+
const raw = await this.call('graft', 'context', { query: `impact of ${value}`, limit: 20, ...('path' in target ? { in: target.path } : {}) }, input.timeout_ms, state);
|
|
268
|
+
if (raw !== null) {
|
|
269
|
+
const normalized = normalizeItems(raw, this.evidenceOptions(snapshot, 'graft'), 'code', 50);
|
|
270
|
+
structural.push(...normalized.items);
|
|
271
|
+
provenance.push(...normalized.provenance);
|
|
272
|
+
}
|
|
273
|
+
else
|
|
274
|
+
unresolved.push(value);
|
|
275
|
+
if (input.require_semantic) {
|
|
276
|
+
const rawSemantic = 'symbol_id' in target
|
|
277
|
+
? await this.call('serena-lsp', 'references', symbolArgs(target.symbol_id), input.timeout_ms, state)
|
|
278
|
+
: await this.call('serena-lsp', 'overview', { relative_path: target.path, depth: 2 }, input.timeout_ms, state);
|
|
279
|
+
if (rawSemantic !== null) {
|
|
280
|
+
const normalized = normalizeItems(rawSemantic, this.evidenceOptions(snapshot, 'serena-lsp'), 'symbol', 50);
|
|
281
|
+
semantic.push(...normalized.items);
|
|
282
|
+
provenance.push(...normalized.provenance);
|
|
283
|
+
}
|
|
284
|
+
else
|
|
285
|
+
unresolved.push('symbol_id' in target ? target.symbol_id : target.path);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (input.include_docs) {
|
|
289
|
+
const docs = await this.call('graphify', 'context', { question: `architecture impact ${JSON.stringify(targets)}`, depth: input.max_depth, token_budget: 2000 }, input.timeout_ms, state);
|
|
290
|
+
if (docs !== null) {
|
|
291
|
+
const normalized = normalizeItems(docs, this.evidenceOptions(snapshot, 'graphify'), 'document', 50);
|
|
292
|
+
documents.push(...normalized.items);
|
|
293
|
+
provenance.push(...normalized.provenance);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const suggested = [...new Set(snapshot.files.map(file => file.path).filter(path => /(^|\/)(test|tests|__tests__|spec)(\/|\.)/u.test(path)))].slice(0, 20);
|
|
297
|
+
return { data: { semantic_findings: semantic, structural_candidates: structural, documents, suggested_test_paths: suggested, unresolved }, provenance, ...state, warnings: state.warnings };
|
|
298
|
+
}
|
|
299
|
+
async preview(input, snapshot) {
|
|
300
|
+
if (this.options.mode !== 'active')
|
|
301
|
+
throw new Error('code edits require code-intelligence mode=active');
|
|
302
|
+
const state = { used: [], missing: [], warnings: [] };
|
|
303
|
+
const provenance = [];
|
|
304
|
+
const sandbox = join(this.root, '.yoke', 'code-intelligence', 'worktrees', `preview-${randomUUID()}`);
|
|
305
|
+
mkdirSync(sandbox, { recursive: true });
|
|
306
|
+
for (const file of snapshot.files) {
|
|
307
|
+
const source = join(this.root, file.path);
|
|
308
|
+
const target = join(sandbox, file.path);
|
|
309
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
310
|
+
copyFileSync(source, target);
|
|
311
|
+
}
|
|
312
|
+
const adapter = createSerenaAdapter(sandbox, this.options.serena);
|
|
313
|
+
const changed = new Set();
|
|
314
|
+
try {
|
|
315
|
+
for (const operation of input.operations) {
|
|
316
|
+
const reference = symbolArgs(operation.symbol_id);
|
|
317
|
+
const args = 'new_name' in operation ? { ...reference, new_name: operation.new_name } : operation.kind === 'safe_delete' ? { name_path_pattern: reference.name_path, relative_path: reference.relative_path } : { ...reference, body: 'content' in operation ? operation.content : '' };
|
|
318
|
+
const tool = operation.kind === 'safe_delete' ? 'safe_delete' : operation.kind;
|
|
319
|
+
try {
|
|
320
|
+
await adapter.call({ tool, arguments: args }, input.timeout_ms);
|
|
321
|
+
state.used.push('serena-lsp');
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
state.missing.push('serena-lsp');
|
|
325
|
+
state.warnings.push(`serena-lsp: ${error.message}`);
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
finally {
|
|
331
|
+
await adapter.close();
|
|
332
|
+
}
|
|
333
|
+
const diffs = [];
|
|
334
|
+
for (const file of snapshot.files) {
|
|
335
|
+
const before = readFileSync(join(this.root, file.path));
|
|
336
|
+
const afterPath = join(sandbox, file.path);
|
|
337
|
+
if (!existsSync(afterPath)) {
|
|
338
|
+
changed.add(file.path);
|
|
339
|
+
diffs.push(`--- a/${file.path}\n+++ /dev/null\n`);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const after = readFileSync(afterPath);
|
|
343
|
+
if (!before.equals(after)) {
|
|
344
|
+
changed.add(file.path);
|
|
345
|
+
diffs.push(`--- a/${file.path}\n+++ b/${file.path}\n@@\n-${before.toString('utf8')}\n+${after.toString('utf8')}\n`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const diff = diffs.join('\n').slice(0, this.options.limits?.maxBytes ?? 2_000_000);
|
|
349
|
+
const plan = createEditPlan(this.root, snapshot, sandbox, input.operations, state.missing.length ? 'incomplete' : 'passed', input.validation_profile, [...changed], diff);
|
|
350
|
+
return { data: plan, provenance, ...state, warnings: state.warnings };
|
|
351
|
+
}
|
|
352
|
+
async apply(input, snapshot) {
|
|
353
|
+
if (this.options.mode !== 'active')
|
|
354
|
+
throw new Error('code edits require code-intelligence mode=active');
|
|
355
|
+
const state = { used: [], missing: [], warnings: [] };
|
|
356
|
+
const plan = loadEditPlan(this.root, input.plan_id);
|
|
357
|
+
if (plan.workspace_id !== this.workspace_id || plan.snapshot_id !== input.expected_snapshot_id)
|
|
358
|
+
throw new Error('edit plan scope does not match workspace or expected snapshot');
|
|
359
|
+
const expected = loadSnapshot(this.root, input.expected_snapshot_id);
|
|
360
|
+
assertSnapshotCurrent(this.root, expected, this.options.policy);
|
|
361
|
+
const result = applyEditPlan(this.root, plan, expected, input.idempotency_key);
|
|
362
|
+
return { data: { plan_id: result.plan_id, transaction_id: result.transaction_id, changed_paths: result.changed_paths, new_snapshot_id: result.new_snapshot_id, validation: result.validation, committed: false, merged: false }, provenance: [], ...state, warnings: state.warnings };
|
|
363
|
+
}
|
|
364
|
+
errorResponse(requestId, workspace, snapshotId, started, error) {
|
|
365
|
+
const code = errorCode(error.message);
|
|
366
|
+
const blocked = ['WORKSPACE_DENIED', 'SNAPSHOT_STALE', 'APPROVAL_REQUIRED', 'PLAN_EXPIRED', 'CAPABILITY_UNAVAILABLE', 'SCOPE_VIOLATION', 'WRITE_CONFLICT'].includes(code);
|
|
367
|
+
return { schema_version: '0.1.0', request_id: requestId, workspace_id: workspace, snapshot_id: snapshotId, status: blocked ? 'blocked' : 'error', data: null, coverage: { backends_requested: [], backends_used: [], backends_missing: [], structural: 'unavailable', semantic: 'unavailable', documents: 'unavailable' }, provenance: [], warnings: [], metrics: { latency_ms: nowMs() - started, result_tokens: 0, token_count_kind: 'exact', returned_bytes: 0 }, artifact_uri: null, error: { code, message: error.message } };
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
export function createCodeIntelligenceCoordinator(root, options) { return new CodeIntelligenceCoordinator(root, options); }
|