@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.
Files changed (61) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +36 -0
  4. package/README.md +15 -9
  5. package/canon/AGENTS.md +7 -0
  6. package/canon/manifest.yaml +1 -1
  7. package/canon/skills/executing-plans/SKILL.md +1 -1
  8. package/canon/skills/requesting-code-review/SKILL.md +2 -2
  9. package/canon/skills/requesting-code-review/code-reviewer.md +12 -0
  10. package/canon/skills/subagent-driven-development/SKILL.md +7 -3
  11. package/canon/skills/subagent-driven-development/code-quality-reviewer-prompt.md +7 -0
  12. package/canon/skills/subagent-driven-development/implementer-prompt.md +7 -0
  13. package/canon/skills/subagent-driven-development/spec-reviewer-prompt.md +7 -0
  14. package/canon/skills/systematic-debugging/SKILL.md +7 -11
  15. package/canon/skills/systematic-debugging/condition-based-waiting.md +7 -0
  16. package/canon/skills/systematic-debugging/defense-in-depth.md +7 -0
  17. package/canon/skills/systematic-debugging/root-cause-tracing.md +9 -0
  18. package/canon/skills/tdd/SKILL.md +1 -1
  19. package/canon/skills/tdd/testing-anti-patterns.md +9 -0
  20. package/dist/agents/pi-telemetry.js +53 -0
  21. package/dist/agents/process-streams.js +13 -0
  22. package/dist/agents/providers.js +4 -1
  23. package/dist/agents/supervision.js +2 -0
  24. package/dist/agents/telemetry.js +22 -25
  25. package/dist/cli.js +16 -2
  26. package/dist/code-intelligence/adapters/graft.js +11 -0
  27. package/dist/code-intelligence/adapters/graphify.js +8 -0
  28. package/dist/code-intelligence/adapters/index.js +5 -0
  29. package/dist/code-intelligence/adapters/mcp.js +37 -0
  30. package/dist/code-intelligence/adapters/serena.js +11 -0
  31. package/dist/code-intelligence/adapters/types.js +1 -0
  32. package/dist/code-intelligence/contracts.js +137 -0
  33. package/dist/code-intelligence/coordinator.js +370 -0
  34. package/dist/code-intelligence/edit-plans.js +53 -0
  35. package/dist/code-intelligence/evidence.js +77 -0
  36. package/dist/code-intelligence/index.js +5 -0
  37. package/dist/code-intelligence/internal-types.js +1 -0
  38. package/dist/code-intelligence/mcp-client.js +139 -0
  39. package/dist/code-intelligence/mcp-server.js +93 -0
  40. package/dist/code-intelligence/snapshots.js +117 -0
  41. package/dist/code-intelligence/transactions.js +60 -0
  42. package/dist/estimation/schedule.js +40 -26
  43. package/dist/retrofit/command.js +3 -1
  44. package/dist/retrofit/config.js +10 -0
  45. package/dist/retrofit/gitignore.js +1 -0
  46. package/dist/retrofit/plan.js +3 -3
  47. package/dist/retrofit/planners/claude.js +3 -3
  48. package/dist/retrofit/planners/codex.js +5 -5
  49. package/dist/retrofit/planners/gemini.js +2 -2
  50. package/dist/retrofit/planners/kilo.js +2 -2
  51. package/dist/retrofit/planners/opencode.js +2 -2
  52. package/dist/retrofit/planners/pi.js +2 -2
  53. package/dist/retrofit/planners/qwen.js +2 -2
  54. package/dist/retrofit/skill-actions.js +1 -1
  55. package/dist/retrofit/tools.js +9 -7
  56. package/dist/setup/command.js +3 -1
  57. package/docs/AGENT-HARDENING-2026-09-16.md +32 -0
  58. package/docs/CODE-INTELLIGENCE.md +36 -0
  59. package/docs/HARNESSES.md +6 -0
  60. package/gemini-extension.json +1 -1
  61. 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
- return runRetrofit(targetDir, { loop, agents, codeGraph });
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] [--reviewer=<${AGENT_LIST}>] [--base=<ref>] [--focus="..."] | design-scan [dir] [--max=N] [--report] | flow-smoke [dir] [--url=<baseUrl>] [--label=<name>] | upgrade>`);
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,5 @@
1
+ export * from './types.js';
2
+ export * from './mcp.js';
3
+ export * from './graft.js';
4
+ export * from './graphify.js';
5
+ export * from './serena.js';
@@ -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); }