opencode-agent-skill 13.0.0-beta.2 → 14.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +1571 -607
  2. package/bin/ocskill.mjs +172 -24
  3. package/docs/OPENCODE-COMPAT.md +34 -97
  4. package/docs/PI-COMPAT.md +203 -0
  5. package/docs/V14-CONTEXT-MEMORY-FABRIC.md +70 -0
  6. package/docs/V14.1-QUALITY-PERFORMANCE-FABRIC.md +114 -0
  7. package/docs/V14.2-TURBO-WEAK-MODEL-RUNTIME.md +448 -0
  8. package/evals/v14/tasks.json +46 -0
  9. package/global-config/agents/executor.md +7 -0
  10. package/global-config/agents/visual-verifier.md +22 -3
  11. package/global-config/plugins/ues-router/index.js +13 -8
  12. package/global-config/plugins/ues-router/policy-runtime.js +7 -0
  13. package/global-config/plugins/ues-router/router.js +12 -2
  14. package/global-config/skills/ecommerce-engineering/SKILL.md +1 -1
  15. package/global-config/skills/file-upload-engineering/SKILL.md +1 -1
  16. package/global-config/skills/git-safety/SKILL.md +1 -1
  17. package/global-config/skills/nestjs-engineering/SKILL.md +1 -1
  18. package/global-config/skills/performance-engineering/SKILL.md +1 -1
  19. package/global-config/skills/react-native-engineering/SKILL.md +1 -1
  20. package/global-config/skills/rest-api-design/SKILL.md +1 -1
  21. package/global-config/skills/ui-ux-engineering/SKILL.md +1 -1
  22. package/lib/adaptive-context-budget.mjs +97 -0
  23. package/lib/affected-tests.mjs +260 -0
  24. package/lib/benchmark-confidence.mjs +41 -2
  25. package/lib/browser-mcp-routing.mjs +166 -0
  26. package/lib/capability-fabric.mjs +359 -0
  27. package/lib/capability-registry.mjs +9 -0
  28. package/lib/code-intelligence/edit-anchor.mjs +102 -0
  29. package/lib/code-intelligence/index.mjs +95 -0
  30. package/lib/code-intelligence/lsp-provider.mjs +189 -0
  31. package/lib/completion-auditor.mjs +82 -0
  32. package/lib/context-engine-v11.mjs +65 -1
  33. package/lib/context-graph-rank.mjs +118 -0
  34. package/lib/context-manifest.mjs +97 -18
  35. package/lib/control-center.mjs +19 -1
  36. package/lib/document-ingestion.mjs +60 -0
  37. package/lib/dynamic-workflow.mjs +3 -1
  38. package/lib/evidence-store.mjs +82 -1
  39. package/lib/fast-verification-gate.mjs +69 -0
  40. package/lib/hierarchical-context.mjs +215 -0
  41. package/lib/mcp-health.mjs +144 -0
  42. package/lib/mcp-tool-policy.mjs +19 -0
  43. package/lib/memory-engine.mjs +493 -0
  44. package/lib/model-performance.mjs +33 -8
  45. package/lib/model-policy.mjs +3 -3
  46. package/lib/orchestrator-policy.mjs +5 -209
  47. package/lib/performance-fabric.mjs +229 -0
  48. package/lib/pi-rpc-pool.mjs +433 -0
  49. package/lib/process-hang-detector.mjs +83 -0
  50. package/lib/process-supervisor.mjs +193 -0
  51. package/lib/prompt-cache.mjs +27 -11
  52. package/lib/repo-graph.mjs +53 -2
  53. package/lib/reversible-context.mjs +49 -0
  54. package/lib/runtime-config.mjs +31 -0
  55. package/lib/safety.mjs +132 -0
  56. package/lib/semantic-index.mjs +52 -3
  57. package/lib/skill-compiler.mjs +128 -0
  58. package/lib/skill-quality.mjs +48 -2
  59. package/lib/task-engine.mjs +62 -5
  60. package/lib/task-policy.mjs +291 -0
  61. package/lib/verification-broker.mjs +284 -0
  62. package/lib/verification-command.mjs +111 -0
  63. package/lib/windows-shim.mjs +35 -0
  64. package/lib/workspace-fingerprint.mjs +198 -0
  65. package/package.json +52 -42
  66. package/pi/extensions/ues-child-runtime.ts +393 -0
  67. package/pi/extensions/ues.ts +3327 -0
  68. package/pi/prompts/ues-audit.md +9 -0
  69. package/pi/prompts/ues-critique.md +9 -0
  70. package/pi/prompts/ues-debug.md +9 -0
  71. package/pi/prompts/ues-feature.md +9 -0
  72. package/pi/prompts/ues-fix.md +9 -0
  73. package/pi/prompts/ues-plan.md +9 -0
  74. package/pi/prompts/ues-research.md +9 -0
  75. package/pi/prompts/ues-resume.md +9 -0
  76. package/pi/prompts/ues-review.md +7 -0
  77. package/pi/prompts/ues-run.md +17 -0
  78. package/pi/prompts/ues-verify.md +9 -0
  79. package/scripts/check-release-consistency.mjs +119 -185
  80. package/scripts/check-runtime-exports.mjs +66 -0
  81. package/scripts/check-source-integrity.mjs +246 -0
  82. package/scripts/eval-pi.mjs +492 -0
  83. package/scripts/install.mjs +16 -0
  84. package/scripts/smoke-package-closure.mjs +110 -0
  85. package/scripts/smoke-packed-install.mjs +24 -11
  86. package/scripts/smoke-pi-extension.mjs +144 -0
  87. package/scripts/uninstall.mjs +44 -0
  88. package/CHANGELOG.md +0 -415
  89. package/docs/DETERMINISTIC-TOOLS.md +0 -105
  90. package/docs/ENGINEERING-DESIGN.md +0 -194
  91. package/docs/EVALS.md +0 -158
  92. package/docs/GITHUB-RULESET.md +0 -50
  93. package/docs/NPM-PUBLISH.md +0 -116
  94. package/docs/RESEARCH-SOURCES.md +0 -37
  95. package/docs/TRACE-SCHEMA.md +0 -122
  96. package/docs/V11-PERCEPTION-ADAPTIVE-EXECUTION.md +0 -75
  97. package/docs/V11-PERCEPTION-ADAPTIVE.md +0 -220
  98. package/docs/V12-WEAK-MODEL-INTELLIGENCE.md +0 -27
  99. package/docs/V13-PARALLEL-WEAK-MODEL-RUNTIME.md +0 -86
  100. package/docs/V7-INTELLIGENCE-RUNTIME.md +0 -166
  101. package/docs/V8-INTELLIGENCE-RELIABILITY.md +0 -206
  102. package/docs/V9-SPEED-INTELLIGENCE.md +0 -102
@@ -4,6 +4,8 @@ import path from "node:path"
4
4
  import { readLearningState } from "./learning-engine.mjs"
5
5
  import { readRuntimeEvents } from "./runtime-events.mjs"
6
6
  import { evidenceStoreStatus } from "./evidence-store.mjs"
7
+ import { memoryStatus } from "./memory-engine.mjs"
8
+ import { capabilityFabricStatus } from "./capability-fabric.mjs"
7
9
 
8
10
  function escapeHtml(value) {
9
11
  return String(value ?? "")
@@ -65,11 +67,13 @@ async function collectEvalSummary(root) {
65
67
 
66
68
  export async function collectControlCenterData(root = process.cwd()) {
67
69
  root = path.resolve(root)
68
- const [work, learning, evals, evidenceStore] = await Promise.all([
70
+ const [work, learning, evals, evidenceStore, memory, capabilityFabric] = await Promise.all([
69
71
  collectWork(root),
70
72
  readLearningState(root),
71
73
  collectEvalSummary(root),
72
74
  evidenceStoreStatus(root),
75
+ memoryStatus(root),
76
+ capabilityFabricStatus(root),
73
77
  ])
74
78
  return {
75
79
  schemaVersion: 2,
@@ -82,6 +86,16 @@ export async function collectControlCenterData(root = process.cwd()) {
82
86
  evidenceStore,
83
87
  runtime: "perception-adaptive-execution",
84
88
  },
89
+ v14: {
90
+ memory,
91
+ capabilityFabric: {
92
+ healthyCapabilities: capabilityFabric.healthyCapabilities,
93
+ totalCapabilities: capabilityFabric.totalCapabilities,
94
+ rows: capabilityFabric.rows,
95
+ observationState: capabilityFabric.observationState,
96
+ },
97
+ runtime: "context-memory-capability-fabric",
98
+ },
85
99
  }
86
100
  }
87
101
 
@@ -110,6 +124,7 @@ details{margin-top:10px;border-top:1px solid #243049;padding-top:8px}summary{cur
110
124
  <div class="section"><h2>Long-horizon work</h2><div class="grid" id="work"></div></div>
111
125
  <div class="section"><h2>Learning loop</h2><div class="grid" id="learning"></div></div>
112
126
  <div class="section"><h2>V11 runtime efficiency</h2><div class="grid" id="v11"></div></div>
127
+ <div class="section"><h2>V14 context & memory fabric</h2><div class="grid" id="v14"></div></div>
113
128
  <div class="section"><h2>Recent runtime events</h2><div class="card"><table><thead><tr><th>Work</th><th>Event</th><th>Task</th><th>Time</th></tr></thead><tbody id="events"></tbody></table></div></div>
114
129
  <div class="section"><h2>Recent evaluations</h2><div class="card"><table><thead><tr><th>Suite</th><th>Model</th><th>Baseline</th><th>UES</th></tr></thead><tbody id="evals"></tbody></table></div></div>
115
130
  </div>
@@ -136,6 +151,9 @@ function render(next){
136
151
  const v11=document.querySelector("#v11");
137
152
  const store=data.v11?.evidenceStore||{};
138
153
  v11.innerHTML='<div class="card"><h3>Evidence store</h3><strong>'+esc(store.entries||0)+'</strong><div class="muted">'+esc(store.bytes||0)+' bytes externalized</div></div><div class="card"><h3>Runtime</h3><strong>V11</strong><div class="muted">'+esc(data.v11?.runtime||'adaptive')+'</div></div>';
154
+ const v14=document.querySelector("#v14");
155
+ const memory=data.v14?.memory||{};
156
+ v14.innerHTML='<div class="card"><h3>Verified memory</h3><strong>'+esc(memory.byStatus?.verified||0)+'</strong><div class="muted">'+esc(memory.entries||0)+' total entries</div></div><div class="card"><h3>Runtime</h3><strong>V14</strong><div class="muted">'+esc(data.v14?.runtime||'context-memory-fabric')+'</div></div>';
139
157
  const eventBody=document.querySelector("#events");
140
158
  eventBody.innerHTML="";
141
159
  for(const item of data.work){
@@ -0,0 +1,60 @@
1
+ import { lstat, readFile, realpath } from "node:fs/promises"
2
+ import { spawnSync } from "node:child_process"
3
+ import path from "node:path"
4
+
5
+ const BUILTIN_TEXT_EXTENSIONS = new Set([
6
+ ".txt", ".md", ".markdown", ".json", ".jsonl", ".csv", ".tsv", ".xml", ".html", ".htm",
7
+ ".yaml", ".yml", ".toml", ".ini", ".log", ".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx",
8
+ ".py", ".java", ".kt", ".kts", ".cs", ".go", ".rs", ".rb", ".php", ".sql", ".css", ".scss",
9
+ ])
10
+ const MARKITDOWN_EXTENSIONS = new Set([".pdf", ".docx", ".pptx", ".xlsx", ".xls", ".epub", ".zip"])
11
+
12
+ function inside(root, target) { return target === root || target.startsWith(root + path.sep) }
13
+ async function safeFile(root, relative) {
14
+ const base = await realpath(path.resolve(root)).catch(() => path.resolve(root))
15
+ const requested = path.resolve(base, String(relative || ""))
16
+ if (!inside(base, requested)) throw new Error("document path escapes workspace root")
17
+ const info = await lstat(requested)
18
+ if (!info.isFile()) throw new Error("document path is not a file")
19
+ const actual = await realpath(requested)
20
+ if (!inside(base, actual)) throw new Error("document symlink escapes workspace root")
21
+ return { base, file: actual, info }
22
+ }
23
+
24
+ function runMarkItDown(file, options = {}) {
25
+ const timeout = Math.max(5_000, Math.min(120_000, Number(options.timeoutMs || 30_000)))
26
+ const maxBuffer = Math.max(256 * 1024, Math.min(16 * 1024 * 1024, Number(options.maxBuffer || 4 * 1024 * 1024)))
27
+ const attempts = process.platform === "win32"
28
+ ? [["markitdown.exe", [file]], ["markitdown", [file]], ["py", ["-m", "markitdown", file]], ["python", ["-m", "markitdown", file]]]
29
+ : [["markitdown", [file]], ["python3", ["-m", "markitdown", file]], ["python", ["-m", "markitdown", file]]]
30
+ for (const [command, args] of attempts) {
31
+ const result = spawnSync(command, args, { encoding: "utf8", timeout, maxBuffer, windowsHide: true })
32
+ if (result.status === 0 && String(result.stdout || "").trim()) return { provider: command.includes("python") || command === "py" ? "markitdown-python" : "markitdown-cli", markdown: String(result.stdout) }
33
+ if (result.error?.code !== "ENOENT" && result.status != null) return { provider: "markitdown-unavailable", error: String(result.stderr || result.error?.message || `exit ${result.status}`) }
34
+ }
35
+ return { provider: "markitdown-unavailable", error: "MarkItDown command/module was not found" }
36
+ }
37
+
38
+ export function documentIngestionSupport(file = "") {
39
+ const ext = path.extname(String(file)).toLowerCase()
40
+ return { schemaVersion: 1, extension: ext, builtin: BUILTIN_TEXT_EXTENSIONS.has(ext), markitdown: MARKITDOWN_EXTENSIONS.has(ext), supported: BUILTIN_TEXT_EXTENSIONS.has(ext) || MARKITDOWN_EXTENSIONS.has(ext) }
41
+ }
42
+
43
+ export async function ingestDocument(root, relative, options = {}) {
44
+ const target = await safeFile(root, relative)
45
+ const support = documentIngestionSupport(target.file)
46
+ const maxBytes = Math.max(16 * 1024, Math.min(32 * 1024 * 1024, Number(options.maxBytes || 4 * 1024 * 1024)))
47
+ if (target.info.size > maxBytes) throw new Error(`document exceeds ingestion limit (${target.info.size} > ${maxBytes} bytes)`)
48
+ if (support.builtin) {
49
+ const content = await readFile(target.file, "utf8")
50
+ return { schemaVersion: 1, provider: "builtin-text", file: path.relative(target.base, target.file).replaceAll("\\", "/"), bytes: target.info.size, markdown: content, optionalDependency: false }
51
+ }
52
+ if (support.markitdown) {
53
+ const converted = runMarkItDown(target.file, options)
54
+ if (converted.markdown) return { schemaVersion: 1, provider: converted.provider, file: path.relative(target.base, target.file).replaceAll("\\", "/"), bytes: target.info.size, markdown: converted.markdown, optionalDependency: true }
55
+ const error = new Error(`${converted.error}. Install Microsoft MarkItDown only when Office/PDF ingestion is needed.`)
56
+ error.code = "UES_MARKITDOWN_UNAVAILABLE"
57
+ throw error
58
+ }
59
+ throw new Error(`unsupported document extension: ${support.extension || "(none)"}`)
60
+ }
@@ -50,6 +50,7 @@ function conflict(a, b) {
50
50
 
51
51
  function executionMode(classification, options) {
52
52
  if (classification.kind === "deterministic") return "deterministic"
53
+ if (options.allowInline === false) return "agent"
53
54
  if (classification.kind === "vision") {
54
55
  return classification.estimatedCost >= options.minVisionAgentCost ? "agent" : "inline"
55
56
  }
@@ -81,6 +82,7 @@ export function planDynamicWorkflow(tasks = [], inputOptions = {}) {
81
82
  minAgentCost: clampInt(inputOptions.minAgentCost, 5, 2, 12),
82
83
  minVisionAgentCost: clampInt(inputOptions.minVisionAgentCost, 4, 2, 12),
83
84
  deterministicFirst: inputOptions.deterministicFirst !== false,
85
+ allowInline: inputOptions.allowInline !== false,
84
86
  }
85
87
 
86
88
  const byID = new Map(tasks.map((task) => [task.id, task]))
@@ -176,4 +178,4 @@ export function planDynamicWorkflow(tasks = [], inputOptions = {}) {
176
178
  visionAgentTaskCount: flat.filter((task) => task.spawnAgent && task.kind === "vision").length,
177
179
  estimatedCoordinationSaved: flat.filter((task) => task.execution !== "agent").reduce((sum, task) => sum + task.estimatedCost, 0),
178
180
  }
179
- }
181
+ }
@@ -99,6 +99,87 @@ export async function getEvidence(root, ref, options = {}) {
99
99
  }
100
100
  }
101
101
 
102
+
103
+ function evidenceSelector(ref) {
104
+ const value = String(ref || "").trim()
105
+ const index = value.indexOf("#")
106
+ if (index < 0) return { ref: value, selector: null }
107
+ return {
108
+ ref: value.slice(0, index),
109
+ selector: value.slice(index + 1),
110
+ }
111
+ }
112
+
113
+ function selectorParts(selector) {
114
+ const value = String(selector || "").trim()
115
+ if (!value) return []
116
+ if (value.startsWith("/")) {
117
+ return value
118
+ .split("/")
119
+ .slice(1)
120
+ .map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~"))
121
+ }
122
+ return value
123
+ .replace(/\[(\d+)\]/g, ".$1")
124
+ .split(".")
125
+ .map((part) => part.trim())
126
+ .filter(Boolean)
127
+ }
128
+
129
+ function selectJsonValue(value, selector) {
130
+ let current = value
131
+ for (const part of selectorParts(selector)) {
132
+ if (Array.isArray(current)) {
133
+ if (!/^\d+$/.test(part)) throw new Error("Invalid array selector segment: " + part)
134
+ const index = Number(part)
135
+ if (index < 0 || index >= current.length) throw new Error("Evidence selector index out of range: " + part)
136
+ current = current[index]
137
+ continue
138
+ }
139
+ if (!current || typeof current !== "object" || !Object.hasOwn(current, part)) {
140
+ throw new Error("Evidence selector path not found: " + part)
141
+ }
142
+ current = current[part]
143
+ }
144
+ return current
145
+ }
146
+
147
+ export async function getEvidenceSelected(root, refWithSelector, options = {}) {
148
+ const split = evidenceSelector(refWithSelector)
149
+ if (!split.selector) return getEvidence(root, split.ref, options)
150
+
151
+ const hash = normalizeHash(split.ref)
152
+ const target = evidencePaths(root, hash)
153
+ const metadata = JSON.parse(await readFile(target.meta, "utf8"))
154
+ if (metadata.encoding !== "utf8" || !String(metadata.mediaType || "").includes("json")) {
155
+ throw new Error("Evidence selectors require JSON evidence")
156
+ }
157
+
158
+ const maxObjectBytes = Math.max(1024, Math.min(16 * 1024 * 1024, Number(options.maxObjectBytes || 4 * 1024 * 1024)))
159
+ if (Number(metadata.bytes || 0) > maxObjectBytes) {
160
+ throw new Error("JSON evidence is too large for selector retrieval; use byte slices instead")
161
+ }
162
+ const source = await readFile(target.data, "utf8")
163
+ const selected = selectJsonValue(JSON.parse(source), split.selector)
164
+ const encoded = typeof selected === "string"
165
+ ? selected
166
+ : JSON.stringify(stableValue(selected), null, 2)
167
+ const maxBytes = Math.max(1, Number(options.maxBytes || options.maxChars || 24_000))
168
+ const bytes = Buffer.from(encoded, "utf8")
169
+ const start = Math.max(0, Number(options.start || 0))
170
+ const slice = bytes.subarray(start, Math.min(bytes.length, start + maxBytes))
171
+ return {
172
+ ...metadata,
173
+ ref: split.ref + "#" + split.selector,
174
+ selector: split.selector,
175
+ selectedBytes: bytes.length,
176
+ truncated: start + slice.length < bytes.length,
177
+ start,
178
+ returnedBytes: slice.length,
179
+ content: slice.toString("utf8"),
180
+ }
181
+ }
182
+
102
183
  async function metadataFiles(root) {
103
184
  const base = evidenceStoreRoot(root)
104
185
  const prefixes = await readdir(base, { withFileTypes: true }).catch(() => [])
@@ -175,4 +256,4 @@ export async function evidenceExists(root, ref) {
175
256
  } catch {
176
257
  return false
177
258
  }
178
- }
259
+ }
@@ -0,0 +1,69 @@
1
+ const BEHAVIORAL_TEST_COMMAND = [
2
+ /(?:^|\s)(?:node|bun)\s+--test(?:\s|$)/i,
3
+ /(?:^|\s)(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?test(?:\s|$|:)/i,
4
+ /(?:^|\s)(?:npx\s+)?(?:jest|vitest|pytest)(?:\s|$)/i,
5
+ /(?:^|\s)go\s+test(?:\s|$)/i,
6
+ /(?:^|\s)cargo\s+test(?:\s|$)/i,
7
+ /(?:^|\s)dotnet\s+test(?:\s|$)/i,
8
+ /(?:^|\s)(?:mvn|mvnw|gradle|gradlew|\.\/gradlew|gradlew\.bat)\b[^\n]*\btest\b/i,
9
+ ]
10
+
11
+ function commandText(row = {}) {
12
+ const receipt = row?.receipt || {}
13
+ return [receipt.command, ...(Array.isArray(receipt.args) ? receipt.args : [])].filter(Boolean).join(" ").trim()
14
+ }
15
+
16
+ function finishedAtMs(row = {}) {
17
+ const value = Date.parse(row?.finishedAt || row?.receipt?.finishedAt || "")
18
+ return Number.isFinite(value) ? value : null
19
+ }
20
+
21
+ export function isBehavioralVerificationReceipt(row = {}) {
22
+ const receipt = row?.receipt || {}
23
+ if (receipt.passed !== true || Number(receipt.exitCode) !== 0) return false
24
+ const command = commandText(row)
25
+ return Boolean(command) && BEHAVIORAL_TEST_COMMAND.some((pattern) => pattern.test(command))
26
+ }
27
+
28
+ export function evaluateFastVerificationGate(input = {}) {
29
+ const policy = input.policy || {}
30
+ const implementation = input.implementation || {}
31
+ const receipts = Array.isArray(input.receipts) ? input.receipts : []
32
+ const attemptStartedAtMs = Number(input.attemptStartedAtMs || 0)
33
+ const eligible =
34
+ policy.executionProfile === "fast" &&
35
+ policy.singleFileBounded === true &&
36
+ policy.risk === "low" &&
37
+ input.visualRequired !== true
38
+ const implementationOk =
39
+ Number(implementation.exitCode) === 0 &&
40
+ String(implementation.stopReason || "").toLowerCase() !== "error"
41
+ const verificationText = String(implementation?.report?.sections?.verification || "").trim()
42
+ const implementationReportedVerification =
43
+ Boolean(verificationText) &&
44
+ !/(?:not\s+run|not\s+available|skipped|unverified|no\s+(?:tests?|checks?)\s+(?:run|available))/i.test(verificationText)
45
+ const behavioralReceipts = receipts
46
+ .filter((row) => {
47
+ const finished = finishedAtMs(row)
48
+ return isBehavioralVerificationReceipt(row) &&
49
+ (attemptStartedAtMs <= 0 || (finished != null && finished >= attemptStartedAtMs))
50
+ })
51
+ .map((row) => ({
52
+ command: commandText(row),
53
+ finishedAt: row?.finishedAt || row?.receipt?.finishedAt || null,
54
+ receiptId: row?.receipt?.id || null,
55
+ }))
56
+ let reason = "fresh-behavioral-evidence"
57
+ if (!eligible) reason = "not-fast-bounded"
58
+ else if (!implementationOk) reason = "implementation-not-clean"
59
+ else if (!implementationReportedVerification) reason = "implementation-did-not-report-verification"
60
+ else if (!behavioralReceipts.length) reason = "no-fresh-behavioral-receipt"
61
+ return {
62
+ schemaVersion: 1,
63
+ eligible,
64
+ passed: eligible && implementationOk && implementationReportedVerification && behavioralReceipts.length > 0,
65
+ reason,
66
+ behavioralReceiptCount: behavioralReceipts.length,
67
+ behavioralReceipts,
68
+ }
69
+ }
@@ -0,0 +1,215 @@
1
+ import path from "node:path"
2
+ import { buildSemanticIndex } from "./semantic-index.mjs"
3
+
4
+ const DEFAULT_L0 = 256
5
+ const DEFAULT_L1 = 4000
6
+ const STOP = new Set([
7
+ "this","that","with","from","into","then","than","when","where","what","your","have","will",
8
+ "task","code","file","files","fix","update","change","changes","được","các","cho","với","trong","này","một","những","không","theo",
9
+ ])
10
+
11
+ function terms(value) {
12
+ return [...new Set(
13
+ String(value || "")
14
+ .toLowerCase()
15
+ .split(/[^\p{L}\p{N}_$.-]+/u)
16
+ .map((item) => item.replace(/^[-.$]+|[-.$]+$/g, ""))
17
+ .filter((item) => item.length >= 2 && !STOP.has(item)),
18
+ )].slice(0, 32)
19
+ }
20
+
21
+ function ancestors(file) {
22
+ const parts = String(file || "").replaceAll("\\", "/").split("/").filter(Boolean)
23
+ const result = ["."]
24
+ let current = ""
25
+ for (let index = 0; index < parts.length - 1; index += 1) {
26
+ current = current ? `${current}/${parts[index]}` : parts[index]
27
+ result.push(current)
28
+ }
29
+ return result
30
+ }
31
+
32
+ function parentDir(dir) {
33
+ if (!dir || dir === ".") return null
34
+ const value = path.posix.dirname(dir)
35
+ return value === "." ? "." : value
36
+ }
37
+
38
+ function clip(value, limit) {
39
+ const text = String(value || "")
40
+ return text.length <= limit ? text : text.slice(0, Math.max(0, limit - 15)) + "...[truncated]"
41
+ }
42
+
43
+ function topCounts(map, limit) {
44
+ return [...map.entries()]
45
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
46
+ .slice(0, limit)
47
+ .map(([name, count]) => ({ name, count }))
48
+ }
49
+
50
+ function buildNodeSummary(node, children, options = {}) {
51
+ const l0Limit = Math.max(96, Number(options.l0Chars || DEFAULT_L0))
52
+ const l1Limit = Math.max(512, Number(options.l1Chars || DEFAULT_L1))
53
+ const symbols = topCounts(node.symbols, 18)
54
+ const extensions = topCounts(node.extensions, 10)
55
+ const files = [...node.files].sort()
56
+ const childNames = (children.get(node.path) || []).sort()
57
+ const l0 = clip([
58
+ `${node.path}: ${files.length} source file(s), ${node.bytes} bytes.`,
59
+ symbols.length ? `Key symbols: ${symbols.slice(0, 8).map((item) => item.name).join(", ")}.` : "",
60
+ childNames.length ? `Child areas: ${childNames.slice(0, 6).join(", ")}.` : "",
61
+ ].filter(Boolean).join(" "), l0Limit)
62
+ const l1 = clip([
63
+ l0,
64
+ childNames.length ? `Children: ${childNames.join(", ")}` : "",
65
+ extensions.length ? `Extensions: ${extensions.map((item) => `${item.name || "(none)"}:${item.count}`).join(", ")}` : "",
66
+ symbols.length ? `Symbols: ${symbols.map((item) => `${item.name}:${item.count}`).join(", ")}` : "",
67
+ files.length ? `Files: ${files.slice(0, 60).join(", ")}` : "",
68
+ ].filter(Boolean).join("\n"), l1Limit)
69
+ return {
70
+ path: node.path,
71
+ fileCount: files.length,
72
+ bytes: node.bytes,
73
+ files,
74
+ symbols,
75
+ children: childNames,
76
+ l0,
77
+ l1,
78
+ }
79
+ }
80
+
81
+ export async function buildContextHierarchy(root = process.cwd(), options = {}) {
82
+ root = path.resolve(root)
83
+ const built = options.builtIndex || await buildSemanticIndex(root, {
84
+ maxFiles: options.maxFiles ?? 6000,
85
+ maxDepth: options.maxDepth ?? 14,
86
+ })
87
+ const nodes = new Map()
88
+ const children = new Map()
89
+ const ensure = (dir) => {
90
+ if (!nodes.has(dir)) {
91
+ nodes.set(dir, { path: dir, files: new Set(), bytes: 0, symbols: new Map(), extensions: new Map() })
92
+ }
93
+ return nodes.get(dir)
94
+ }
95
+
96
+ ensure(".")
97
+ for (const [file, entry] of Object.entries(built.index.files || {})) {
98
+ const dirs = ancestors(file)
99
+ const extension = path.posix.extname(file).toLowerCase()
100
+ for (const dir of dirs) {
101
+ const node = ensure(dir)
102
+ node.files.add(file)
103
+ node.bytes += Number(entry.bytes || 0)
104
+ node.extensions.set(extension, (node.extensions.get(extension) || 0) + 1)
105
+ for (const symbol of entry.symbols || []) {
106
+ const name = String(symbol.name || "")
107
+ if (!name) continue
108
+ node.symbols.set(name, (node.symbols.get(name) || 0) + 1)
109
+ }
110
+ }
111
+ }
112
+
113
+ for (const dir of nodes.keys()) {
114
+ const parent = parentDir(dir)
115
+ if (!parent) continue
116
+ const list = children.get(parent) || []
117
+ if (!list.includes(dir)) list.push(dir)
118
+ children.set(parent, list)
119
+ }
120
+
121
+ const summaries = [...nodes.values()]
122
+ .map((node) => buildNodeSummary(node, children, options))
123
+ .sort((a, b) => a.path.localeCompare(b.path))
124
+
125
+ return {
126
+ schemaVersion: 1,
127
+ kind: "ues-context-hierarchy",
128
+ root,
129
+ generatedAt: new Date().toISOString(),
130
+ levels: {
131
+ L0: { maxChars: Math.max(96, Number(options.l0Chars || DEFAULT_L0)), purpose: "routing abstract" },
132
+ L1: { maxChars: Math.max(512, Number(options.l1Chars || DEFAULT_L1)), purpose: "scope overview" },
133
+ L2: { purpose: "source excerpts loaded on demand by the context manifest" },
134
+ },
135
+ nodes: summaries,
136
+ stats: built.stats,
137
+ }
138
+ }
139
+
140
+ function containsFile(scope, file) {
141
+ if (scope === ".") return true
142
+ return file === scope || file.startsWith(scope + "/")
143
+ }
144
+
145
+ function scopesOverlap(left, right) {
146
+ if (left === "." || right === ".") return true
147
+ return left === right || left.startsWith(right + "/") || right.startsWith(left + "/")
148
+ }
149
+
150
+ export function selectDiverseHierarchyScopes(rows = [], maxScopes = 6) {
151
+ const limit = Math.max(1, Math.min(Number(maxScopes || 6), 20))
152
+ const selected = []
153
+ for (const row of rows) {
154
+ if (selected.length >= limit) break
155
+ if (selected.some((item) => scopesOverlap(item.path, row.path))) continue
156
+ selected.push(row)
157
+ }
158
+ if (!selected.length && rows.length) selected.push(rows[0])
159
+ return selected
160
+ }
161
+
162
+ export async function queryContextHierarchy(root, query, options = {}) {
163
+ const hierarchy = await buildContextHierarchy(root, options)
164
+ const wanted = terms(query)
165
+ const declared = (options.declaredFiles || []).map((file) => String(file || "").replaceAll("\\", "/")).filter(Boolean)
166
+ const scored = hierarchy.nodes.map((node) => {
167
+ const lowerPath = node.path.toLowerCase()
168
+ const symbolText = node.symbols.map((item) => item.name.toLowerCase()).join(" ")
169
+ const fileText = node.files.slice(0, 100).join(" ").toLowerCase()
170
+ let score = node.path === "." ? 0.25 : 0
171
+ const reasons = []
172
+ for (const term of wanted) {
173
+ if (lowerPath.includes(term)) {
174
+ score += 8
175
+ reasons.push("path:" + term)
176
+ }
177
+ if (symbolText.includes(term)) {
178
+ score += 5
179
+ reasons.push("symbol:" + term)
180
+ }
181
+ if (fileText.includes(term)) {
182
+ score += 2
183
+ reasons.push("file:" + term)
184
+ }
185
+ }
186
+ const declaredHits = declared.filter((file) => containsFile(node.path, file)).length
187
+ if (declaredHits) {
188
+ score += 20 + declaredHits * 4
189
+ reasons.push("declared:" + declaredHits)
190
+ }
191
+ const depth = node.path === "." ? 0 : node.path.split("/").length
192
+ if (score > 0) score += Math.min(3, depth * 0.4)
193
+ score -= Math.min(2.5, Math.log1p(node.fileCount) * 0.15)
194
+ return { ...node, score: Number(score.toFixed(3)), reasons: [...new Set(reasons)].slice(0, 12) }
195
+ })
196
+
197
+ const maxScopes = Math.max(1, Math.min(Number(options.maxScopes || 6), 20))
198
+ const positive = scored
199
+ .filter((item) => item.score > 0)
200
+ .sort((a, b) => b.score - a.score || a.fileCount - b.fileCount || a.path.localeCompare(b.path))
201
+ const scopes = selectDiverseHierarchyScopes(
202
+ positive.length ? positive : scored.filter((item) => item.path === "."),
203
+ maxScopes,
204
+ )
205
+
206
+ return {
207
+ schemaVersion: 1,
208
+ kind: "ues-context-hierarchy-query",
209
+ query: String(query || ""),
210
+ terms: wanted,
211
+ levels: hierarchy.levels,
212
+ scopes,
213
+ stats: hierarchy.stats,
214
+ }
215
+ }
@@ -0,0 +1,144 @@
1
+ const TRANSIENT_ERROR =
2
+ /(timeout|timed out|econnreset|econnrefused|socket|connection (?:closed|reset|lost)|transport|network|temporar(?:y|ily)|service unavailable|bad gateway|gateway timeout|\b502\b|\b503\b|\b504\b|rate limit|\b429\b)/i
3
+
4
+ function nowMs(value) {
5
+ const number = Number(value)
6
+ return Number.isFinite(number) ? number : Date.now()
7
+ }
8
+
9
+ export function isTransientMcpFailure(value = "") {
10
+ return TRANSIENT_ERROR.test(String(value || ""))
11
+ }
12
+
13
+ export function mcpReconnectAdvice(input = {}) {
14
+ const policy = input.policy || {}
15
+ const attempt = Math.max(1, Math.trunc(Number(input.attempt || 1)))
16
+ const transient = isTransientMcpFailure(input.error || input.message || "")
17
+ const destructive = policy.confirmationRequired === true || policy.annotations?.destructiveHint === true
18
+ const retryable = policy.retryAllowed === true && transient && !destructive && attempt < Math.max(1, Number(input.maxAttempts || 3))
19
+ return {
20
+ schemaVersion: 1,
21
+ retryable,
22
+ reconnectRecommended: retryable,
23
+ reason: destructive
24
+ ? "destructive-tool-never-auto-reconnect"
25
+ : !transient
26
+ ? "non-transient-failure"
27
+ : policy.retryAllowed !== true
28
+ ? "tool-not-explicitly-idempotent"
29
+ : retryable
30
+ ? "transient-idempotent-failure"
31
+ : "retry-budget-exhausted",
32
+ backoffMs: retryable ? Math.min(5_000, 250 * (2 ** Math.max(0, attempt - 1))) : 0,
33
+ }
34
+ }
35
+
36
+ export class McpHealthTracker {
37
+ constructor(options = {}) {
38
+ this.failureThreshold = Math.max(1, Math.min(10, Number(options.failureThreshold || 2)))
39
+ this.cooldownMs = Math.max(1_000, Math.min(5 * 60_000, Number(options.cooldownMs || 15_000)))
40
+ this.maxEntries = Math.max(16, Math.min(2_000, Number(options.maxEntries || 256)))
41
+ this.state = new Map()
42
+ this.active = new Map()
43
+ }
44
+
45
+ clear() {
46
+ this.state.clear()
47
+ this.active.clear()
48
+ }
49
+
50
+ begin(callId, toolName, policy = {}, at = Date.now()) {
51
+ const id = String(callId || "")
52
+ const name = String(toolName || "")
53
+ if (id && name) this.active.set(id, { name, policy, startedAt: nowMs(at) })
54
+ this.#trim()
55
+ return this.status(name, at)
56
+ }
57
+
58
+ finish(callId, result = {}, at = Date.now()) {
59
+ const id = String(callId || "")
60
+ const active = this.active.get(id)
61
+ if (!active) return null
62
+ this.active.delete(id)
63
+ const timestamp = nowMs(at)
64
+ const previous = this.state.get(active.name) || {
65
+ successes: 0,
66
+ failures: 0,
67
+ consecutiveFailures: 0,
68
+ cooldownUntil: 0,
69
+ lastError: null,
70
+ lastFinishedAt: 0,
71
+ }
72
+ const errorText = [
73
+ result.error,
74
+ result.message,
75
+ result.stderr,
76
+ result.text,
77
+ ].filter(Boolean).join("\n").slice(0, 4000)
78
+ const failed = result.isError === true || Number(result.exitCode) > 0
79
+ const transient = failed && isTransientMcpFailure(errorText)
80
+ const consecutiveFailures = failed ? Number(previous.consecutiveFailures || 0) + 1 : 0
81
+ const cooldownUntil = transient && consecutiveFailures >= this.failureThreshold
82
+ ? timestamp + this.cooldownMs
83
+ : failed
84
+ ? Number(previous.cooldownUntil || 0)
85
+ : 0
86
+ const reconnectAdvice = failed
87
+ ? mcpReconnectAdvice({ policy: active.policy, error: errorText, attempt: consecutiveFailures })
88
+ : null
89
+ const next = {
90
+ successes: Number(previous.successes || 0) + (failed ? 0 : 1),
91
+ failures: Number(previous.failures || 0) + (failed ? 1 : 0),
92
+ consecutiveFailures,
93
+ cooldownUntil,
94
+ lastError: failed ? errorText || "tool-result-error" : null,
95
+ lastTransientFailure: transient,
96
+ reconnectAdvice,
97
+ lastFinishedAt: timestamp,
98
+ policy: active.policy,
99
+ }
100
+ this.state.set(active.name, next)
101
+ this.#trim()
102
+ return this.status(active.name, timestamp)
103
+ }
104
+
105
+ status(toolName, at = Date.now()) {
106
+ const name = String(toolName || "")
107
+ const row = this.state.get(name)
108
+ const timestamp = nowMs(at)
109
+ if (!row) {
110
+ return { schemaVersion: 1, tool: name, status: "unknown", available: true, consecutiveFailures: 0, cooldownUntil: null }
111
+ }
112
+ const coolingDown = Number(row.cooldownUntil || 0) > timestamp
113
+ return {
114
+ schemaVersion: 1,
115
+ tool: name,
116
+ status: coolingDown ? "degraded" : row.consecutiveFailures > 0 ? "recovering" : "healthy",
117
+ available: !coolingDown,
118
+ successes: row.successes,
119
+ failures: row.failures,
120
+ consecutiveFailures: row.consecutiveFailures,
121
+ cooldownUntil: coolingDown ? new Date(row.cooldownUntil).toISOString() : null,
122
+ lastError: row.lastError,
123
+ lastTransientFailure: row.lastTransientFailure === true,
124
+ reconnectAdvice: row.reconnectAdvice || null,
125
+ lastFinishedAt: row.lastFinishedAt ? new Date(row.lastFinishedAt).toISOString() : null,
126
+ }
127
+ }
128
+
129
+ available(toolName, at = Date.now()) {
130
+ return this.status(toolName, at).available !== false
131
+ }
132
+
133
+ snapshot(at = Date.now()) {
134
+ return [...this.state.keys()]
135
+ .sort()
136
+ .map((name) => this.status(name, at))
137
+ }
138
+
139
+ #trim() {
140
+ if (this.state.size <= this.maxEntries) return
141
+ const ordered = [...this.state.entries()].sort((a, b) => Number(a[1]?.lastFinishedAt || 0) - Number(b[1]?.lastFinishedAt || 0))
142
+ for (const [name] of ordered.slice(0, Math.max(0, this.state.size - this.maxEntries))) this.state.delete(name)
143
+ }
144
+ }