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
@@ -0,0 +1,189 @@
1
+ import { spawn, spawnSync } from "node:child_process"
2
+ import { lstat, readFile, realpath } from "node:fs/promises"
3
+ import path from "node:path"
4
+ import { pathToFileURL } from "node:url"
5
+
6
+ const PROVIDERS = [
7
+ { id: "typescript-language-server", extensions: new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]), commands: [["typescript-language-server", ["--stdio"]]], languageId: (ext) => [".ts", ".tsx"].includes(ext) ? "typescript" : "javascript" },
8
+ { id: "pyright-langserver", extensions: new Set([".py"]), commands: [["pyright-langserver", ["--stdio"]], ["pylsp", []]], languageId: () => "python" },
9
+ { id: "gopls", extensions: new Set([".go"]), commands: [["gopls", []]], languageId: () => "go" },
10
+ { id: "rust-analyzer", extensions: new Set([".rs"]), commands: [["rust-analyzer", []]], languageId: () => "rust" },
11
+ { id: "clangd", extensions: new Set([".c", ".cc", ".cpp", ".cxx", ".h", ".hpp"]), commands: [["clangd", []]], languageId: (ext) => [".c", ".h"].includes(ext) ? "c" : "cpp" },
12
+ ]
13
+
14
+ const commandCache = new Map()
15
+ const COMMAND_TTL_MS = 60_000
16
+
17
+ function commandExists(command) {
18
+ const now = Date.now()
19
+ const cached = commandCache.get(command)
20
+ if (cached && now - cached.at < COMMAND_TTL_MS) return cached.available
21
+ const finder = process.platform === "win32" ? "where" : "which"
22
+ const available = spawnSync(finder, [command], { stdio: "ignore", windowsHide: true }).status === 0
23
+ commandCache.set(command, { available, at: now })
24
+ return available
25
+ }
26
+
27
+ function inside(root, target) {
28
+ return target === root || target.startsWith(root + path.sep)
29
+ }
30
+
31
+ async function safeFile(root, relative) {
32
+ const base = await realpath(path.resolve(root)).catch(() => path.resolve(root))
33
+ const requested = path.resolve(base, String(relative || ""))
34
+ if (!inside(base, requested)) throw new Error("diagnostic path escapes workspace root")
35
+ const info = await lstat(requested)
36
+ if (!info.isFile()) throw new Error("diagnostic path is not a file")
37
+ const actual = await realpath(requested)
38
+ if (!inside(base, actual)) throw new Error("diagnostic symlink escapes workspace root")
39
+ return { base, file: actual, info }
40
+ }
41
+
42
+ function providerFor(file) {
43
+ const ext = path.extname(file).toLowerCase()
44
+ for (const provider of PROVIDERS) {
45
+ if (!provider.extensions.has(ext)) continue
46
+ for (const [command, args] of provider.commands) {
47
+ if (commandExists(command)) return { ...provider, command, args, languageId: provider.languageId(ext) }
48
+ }
49
+ return { ...provider, command: null, args: [], languageId: provider.languageId(ext) }
50
+ }
51
+ return null
52
+ }
53
+
54
+ export function lspProviderStatus(file = "") {
55
+ const ext = path.extname(String(file || "")).toLowerCase()
56
+ const providers = PROVIDERS
57
+ .filter((provider) => !ext || provider.extensions.has(ext))
58
+ .map((provider) => {
59
+ const selected = provider.commands.find(([command]) => commandExists(command)) || null
60
+ return { id: provider.id, available: Boolean(selected), command: selected?.[0] || null, extensions: [...provider.extensions] }
61
+ })
62
+ return { schemaVersion: 1, extension: ext, available: providers.some((row) => row.available), providers }
63
+ }
64
+
65
+ function encodeMessage(payload) {
66
+ const body = Buffer.from(JSON.stringify(payload), "utf8")
67
+ return Buffer.concat([Buffer.from("Content-Length: " + body.length + "\r\n\r\n", "ascii"), body])
68
+ }
69
+
70
+ function sanitizeDiagnostic(value = {}) {
71
+ return {
72
+ range: value.range || null,
73
+ severity: Number.isFinite(Number(value.severity)) ? Number(value.severity) : null,
74
+ code: value.code == null ? null : String(value.code),
75
+ source: value.source == null ? null : String(value.source),
76
+ message: String(value.message || "").slice(0, 4000),
77
+ }
78
+ }
79
+
80
+ export async function diagnoseCode(root, relative, options = {}) {
81
+ const target = await safeFile(root, relative)
82
+ const provider = providerFor(target.file)
83
+ const relativeFile = path.relative(target.base, target.file).replaceAll("\\", "/")
84
+ if (!provider?.command) {
85
+ return { schemaVersion: 1, file: relativeFile, available: false, provider: provider?.id || null, diagnostics: [], reason: provider ? "lsp-command-unavailable" : "unsupported-extension" }
86
+ }
87
+
88
+ const maxBytes = Math.max(8 * 1024, Math.min(4 * 1024 * 1024, Number(options.maxBytes || 1024 * 1024)))
89
+ if (target.info.size > maxBytes) throw new Error("diagnostic file exceeds limit (" + target.info.size + " > " + maxBytes + " bytes)")
90
+ const source = await readFile(target.file, "utf8")
91
+ const uri = pathToFileURL(target.file).href
92
+ const rootUri = pathToFileURL(target.base + path.sep).href
93
+ const timeoutMs = Math.max(500, Math.min(10_000, Number(options.timeoutMs || 3000)))
94
+ const maxDiagnostics = Math.max(1, Math.min(500, Number(options.maxDiagnostics || 100)))
95
+
96
+ return await new Promise((resolve) => {
97
+ const child = spawn(provider.command, provider.args, { cwd: target.base, stdio: ["pipe", "pipe", "pipe"], windowsHide: true, shell: false })
98
+ let buffer = Buffer.alloc(0)
99
+ let stderr = ""
100
+ let settled = false
101
+ let initialized = false
102
+ let diagnostics = []
103
+ let timer = null
104
+
105
+ const finish = (reason, error = null) => {
106
+ if (settled) return
107
+ settled = true
108
+ if (timer) clearTimeout(timer)
109
+ try {
110
+ if (initialized && child.stdin.writable) {
111
+ child.stdin.write(encodeMessage({ jsonrpc: "2.0", id: 2, method: "shutdown", params: null }))
112
+ child.stdin.write(encodeMessage({ jsonrpc: "2.0", method: "exit", params: null }))
113
+ }
114
+ } catch {}
115
+ setTimeout(() => { try { child.kill() } catch {} }, 50).unref?.()
116
+ resolve({
117
+ schemaVersion: 1,
118
+ file: relativeFile,
119
+ available: true,
120
+ provider: provider.id,
121
+ command: provider.command,
122
+ diagnostics: diagnostics.slice(0, maxDiagnostics),
123
+ reason,
124
+ stderr: stderr.trim().slice(0, 2000) || null,
125
+ error: error ? String(error instanceof Error ? error.message : error).slice(0, 1000) : null,
126
+ })
127
+ }
128
+
129
+ const handle = (message) => {
130
+ if (message?.id === 1 && message?.result) {
131
+ initialized = true
132
+ try {
133
+ child.stdin.write(encodeMessage({ jsonrpc: "2.0", method: "initialized", params: {} }))
134
+ child.stdin.write(encodeMessage({
135
+ jsonrpc: "2.0",
136
+ method: "textDocument/didOpen",
137
+ params: { textDocument: { uri, languageId: provider.languageId, version: 1, text: source } },
138
+ }))
139
+ } catch (error) {
140
+ finish("write-failed", error)
141
+ }
142
+ return
143
+ }
144
+ if (message?.method === "textDocument/publishDiagnostics" && message?.params?.uri === uri) {
145
+ diagnostics = Array.isArray(message.params.diagnostics) ? message.params.diagnostics.map(sanitizeDiagnostic).slice(0, maxDiagnostics) : []
146
+ finish("publishDiagnostics")
147
+ }
148
+ }
149
+
150
+ const parse = () => {
151
+ while (!settled) {
152
+ const headerEnd = buffer.indexOf("\r\n\r\n")
153
+ if (headerEnd < 0) return
154
+ const header = buffer.subarray(0, headerEnd).toString("ascii")
155
+ const match = header.match(/Content-Length:\s*(\d+)/i)
156
+ if (!match) { buffer = buffer.subarray(headerEnd + 4); continue }
157
+ const length = Number(match[1])
158
+ const bodyStart = headerEnd + 4
159
+ if (buffer.length < bodyStart + length) return
160
+ const body = buffer.subarray(bodyStart, bodyStart + length).toString("utf8")
161
+ buffer = buffer.subarray(bodyStart + length)
162
+ try { handle(JSON.parse(body)) } catch {}
163
+ }
164
+ }
165
+
166
+ child.stdout.on("data", (chunk) => { buffer = Buffer.concat([buffer, Buffer.from(chunk)]); parse() })
167
+ child.stderr.on("data", (chunk) => { if (stderr.length < 64 * 1024) stderr += chunk.toString() })
168
+ child.on("error", (error) => finish("spawn-error", error))
169
+ child.on("close", (code) => { if (!settled) finish("server-exit:" + (code ?? "unknown")) })
170
+
171
+ try {
172
+ child.stdin.write(encodeMessage({
173
+ jsonrpc: "2.0",
174
+ id: 1,
175
+ method: "initialize",
176
+ params: {
177
+ processId: process.pid,
178
+ rootUri,
179
+ capabilities: { textDocument: { publishDiagnostics: { relatedInformation: true } } },
180
+ workspaceFolders: [{ uri: rootUri, name: path.basename(target.base) || "workspace" }],
181
+ },
182
+ }))
183
+ } catch (error) {
184
+ finish("initialize-write-failed", error)
185
+ }
186
+ timer = setTimeout(() => finish("timeout"), timeoutMs)
187
+ timer.unref?.()
188
+ })
189
+ }
@@ -0,0 +1,82 @@
1
+ function text(value) {
2
+ return String(value ?? "").trim()
3
+ }
4
+
5
+ function reportSections(run) {
6
+ const sections = run?.report?.sections
7
+ return sections && typeof sections === "object" ? sections : {}
8
+ }
9
+
10
+ function passRun(run) {
11
+ return Boolean(run && Number(run.exitCode) === 0 && run.verdict === "PASS")
12
+ }
13
+
14
+ function benignEmptySection(value) {
15
+ const normalized = text(value)
16
+ .toLowerCase()
17
+ .replace(/[`*_#]/g, "")
18
+ .replace(/[.!:;]+$/g, "")
19
+ .trim()
20
+ return !normalized || /^(?:none(?: observed| found| remaining)?|n\/a|na|nil|0|no failures?(?: observed| found| remaining)?|no unresolved gaps?(?: observed| found| remaining)?|no gaps?(?: observed| found| remaining)?|not applicable)$/.test(normalized)
21
+ }
22
+
23
+ function receiptPassed(row = {}) {
24
+ const receipt = row?.receipt || row
25
+ return Boolean(receipt?.passed === true && Number(receipt?.exitCode) === 0)
26
+ }
27
+
28
+ export function auditCompletion(input = {}) {
29
+ const failures = []
30
+ const warnings = []
31
+ const verification = input.verification || null
32
+ const integration = input.integration || null
33
+ const visual = input.visual || null
34
+ const requireIntegration = input.requireIntegration === true
35
+ const requireVisual = input.requireVisual === true
36
+ const receipts = Array.isArray(input.behavioralReceipts) ? input.behavioralReceipts : []
37
+
38
+ if (!passRun(verification)) failures.push("primary-verification-not-pass")
39
+ if (requireIntegration && !passRun(integration)) failures.push("integration-verification-not-pass")
40
+ if (requireVisual && !passRun(visual)) failures.push("visual-verification-not-pass")
41
+
42
+ const sections = reportSections(verification)
43
+ for (const key of ["checks-run", "acceptance-criteria-proven", "completion-evidence"]) {
44
+ if (!text(sections[key])) failures.push(`missing-report-section:${key}`)
45
+ }
46
+ if (Object.hasOwn(sections, "failures") && !benignEmptySection(sections.failures)) failures.push("verification-reports-failures")
47
+ if (Object.hasOwn(sections, "unresolved-gaps") && !benignEmptySection(sections["unresolved-gaps"])) failures.push("verification-reports-unresolved-gaps")
48
+ if (Object.hasOwn(sections, "checks-not-run") && !benignEmptySection(sections["checks-not-run"])) warnings.push("verification-reports-checks-not-run")
49
+
50
+ const snapshot = input.workspaceSnapshot || null
51
+ if (snapshot) {
52
+ if (snapshot.cacheable === true && !text(snapshot.fingerprint)) failures.push("workspace-fingerprint-missing")
53
+ if (Array.isArray(snapshot.changedFiles) && snapshot.changedFiles.length === 0) warnings.push("workspace-has-no-detected-changes")
54
+ } else {
55
+ warnings.push("workspace-snapshot-unavailable")
56
+ }
57
+
58
+ const passingReceipts = receipts.filter(receiptPassed)
59
+ if (input.requireBehavioralReceipt === true && passingReceipts.length === 0) failures.push("fresh-behavioral-receipt-missing")
60
+ else if (passingReceipts.length === 0) warnings.push("no-fresh-behavioral-receipt-attached")
61
+
62
+ const verifierOutput = text(verification?.output)
63
+ if (verification?.report?.valid === false) failures.push("verification-report-invalid")
64
+ if (verification?.report?.verdict && verification.report.verdict !== "PASS") failures.push("verification-report-verdict-mismatch")
65
+ if (/UES_VERDICT:\s*FAIL/i.test(verifierOutput)) failures.push("verifier-output-contains-fail-verdict")
66
+
67
+ return {
68
+ schemaVersion: 1,
69
+ passed: failures.length === 0,
70
+ failures: [...new Set(failures)],
71
+ warnings: [...new Set(warnings)],
72
+ evidence: {
73
+ primary: passRun(verification),
74
+ integration: requireIntegration ? passRun(integration) : null,
75
+ visual: requireVisual ? passRun(visual) : null,
76
+ reportSections: Object.keys(sections).sort(),
77
+ passingReceiptCount: passingReceipts.length,
78
+ workspaceFingerprint: snapshot?.fingerprint || null,
79
+ changedFiles: Array.isArray(snapshot?.changedFiles) ? snapshot.changedFiles.slice(0, 200) : [],
80
+ },
81
+ }
82
+ }
@@ -4,6 +4,8 @@ import { putEvidence } from "./evidence-store.mjs"
4
4
  import { inferTaskCapabilities } from "./capability-registry.mjs"
5
5
  import { buildPromptEnvelope, comparePromptEnvelopes } from "./prompt-cache.mjs"
6
6
  import { measureContextQuality } from "./context-quality.mjs"
7
+ import { retrieveMemories } from "./memory-engine.mjs"
8
+ import { capabilityFabricStatus } from "./capability-fabric.mjs"
7
9
 
8
10
  function taskText(task = {}) {
9
11
  return [
@@ -14,6 +16,20 @@ function taskText(task = {}) {
14
16
  ].filter(Boolean).join(" ")
15
17
  }
16
18
 
19
+ function taskFilesForMemory(task = {}) {
20
+ const files = task.files
21
+ const values = []
22
+ if (Array.isArray(files)) values.push(...files)
23
+ else if (files && typeof files === "object") {
24
+ for (const value of Object.values(files)) {
25
+ if (Array.isArray(value)) values.push(...value)
26
+ else if (typeof value === "string") values.push(value)
27
+ }
28
+ }
29
+ if (Array.isArray(task.requiredFiles)) values.push(...task.requiredFiles)
30
+ return [...new Set(values.filter(Boolean))]
31
+ }
32
+
17
33
  function rankExcerpt(item = {}, task = {}) {
18
34
  const text = taskText(task).toLowerCase()
19
35
  const pathValue = String(item.path || "").toLowerCase()
@@ -91,6 +107,8 @@ export async function buildAdaptiveTaskContext(root, task, options = {}) {
91
107
  strategy: options.strategy || policy?.profile?.contextStrategy || "incremental-semantic+git",
92
108
  semanticMaxFiles: options.semanticMaxFiles,
93
109
  maxFiles: options.maxFiles,
110
+ changedFiles: options.changedFiles,
111
+ workspaceFingerprint: options.workspaceFingerprint,
94
112
  })
95
113
 
96
114
  const externalized = await externalizeContextExcerpts(root, manifest, {
@@ -99,6 +117,24 @@ export async function buildAdaptiveTaskContext(root, task, options = {}) {
99
117
  inlineChars: options.inlineChars,
100
118
  })
101
119
 
120
+ const memoryRetrieval = await retrieveMemories(root, taskText(task), {
121
+ files: taskFilesForMemory(task),
122
+ limit: options.memoryLimit ?? 6,
123
+ taskClass: options.taskClass || task.type || policy.executionProfile || policy.profile?.name || null,
124
+ touch: options.touchMemories !== false,
125
+ }).catch(() => ({ schemaVersion: 1, query: taskText(task), eligible: 0, results: [] }))
126
+ const memories = memoryRetrieval.results || []
127
+ const fabric = await capabilityFabricStatus(root).catch(() => null)
128
+ const providerHints = (fabric?.rows || [])
129
+ .filter((row) => row.selected)
130
+ .slice(0, 12)
131
+ .map((row) => ({
132
+ capability: row.capability,
133
+ selected: row.selected,
134
+ status: row.status,
135
+ fallbacks: (row.fallbacks || []).slice(0, 3),
136
+ }))
137
+
102
138
  const promptEnvelope = buildPromptEnvelope({
103
139
  invariants: options.invariants || "evidence-first; scoped edits; fresh verification; no unsupported completion claims",
104
140
  role: options.role || "executor",
@@ -114,6 +150,23 @@ export async function buildAdaptiveTaskContext(root, task, options = {}) {
114
150
  ...(externalized.manifest?.rankedReferences || []).slice(0, 12).map((item) => item.path),
115
151
  ...(options.evidence || []),
116
152
  ],
153
+ memories: memories.map((item) => ({
154
+ id: item.id,
155
+ type: item.type,
156
+ scope: item.scope,
157
+ content: item.content,
158
+ confidence: item.confidence,
159
+ files: item.files,
160
+ retrieval: item.retrieval,
161
+ })),
162
+ contextHints: {
163
+ hierarchy: (externalized.manifest?.hierarchy?.scopes || []).slice(0, 6).map((item) => ({
164
+ path: item.path,
165
+ score: item.score,
166
+ l0: item.l0,
167
+ })),
168
+ providers: providerHints,
169
+ },
117
170
  recentFailure: options.recentFailure || null,
118
171
  nextAction: options.nextAction || null,
119
172
  recentMessages: options.recentMessages || [],
@@ -132,6 +185,17 @@ export async function buildAdaptiveTaskContext(root, task, options = {}) {
132
185
  evidenceBudget,
133
186
  contextManifest: externalized.manifest,
134
187
  contextQuality,
188
+ memories,
189
+ memoryRetrieval: {
190
+ schemaVersion: memoryRetrieval.schemaVersion || 1,
191
+ eligible: memoryRetrieval.eligible || 0,
192
+ returned: memories.length,
193
+ },
194
+ capabilityFabric: {
195
+ healthyCapabilities: fabric?.healthyCapabilities || 0,
196
+ totalCapabilities: fabric?.totalCapabilities || 0,
197
+ providers: providerHints,
198
+ },
135
199
  evidenceStore: {
136
200
  refs: externalized.externalized.length,
137
201
  externalizedBytes: externalized.externalizedBytes,
@@ -147,4 +211,4 @@ export async function buildAdaptiveTaskContext(root, task, options = {}) {
147
211
  ...(cache || {}),
148
212
  },
149
213
  }
150
- }
214
+ }
@@ -0,0 +1,118 @@
1
+ function normalizePath(value) {
2
+ return String(value || "").replaceAll("\\", "/")
3
+ }
4
+
5
+ function normalizeSeeds(nodes, semanticResults = [], declared = [], changed = []) {
6
+ const raw = new Map(nodes.map((node) => [node, 0]))
7
+ const semanticMax = Math.max(1, ...semanticResults.map((item) => Number(item.score || 0)))
8
+ for (const item of semanticResults) {
9
+ const file = normalizePath(item.path)
10
+ if (!raw.has(file)) continue
11
+ raw.set(file, raw.get(file) + Number(item.score || 0) / semanticMax)
12
+ }
13
+ for (const file of declared.map(normalizePath)) {
14
+ if (raw.has(file)) raw.set(file, raw.get(file) + 1.25)
15
+ }
16
+ for (const file of changed.map(normalizePath)) {
17
+ if (raw.has(file)) raw.set(file, raw.get(file) + 0.75)
18
+ }
19
+
20
+ let total = [...raw.values()].reduce((sum, value) => sum + value, 0)
21
+ if (total <= 0) {
22
+ const uniform = nodes.length ? 1 / nodes.length : 0
23
+ return new Map(nodes.map((node) => [node, uniform]))
24
+ }
25
+ return new Map([...raw.entries()].map(([key, value]) => [key, value / total]))
26
+ }
27
+
28
+ export function rankContextGraph(graph = {}, options = {}) {
29
+ const nodes = [...new Set((graph.nodes || []).map((node) => normalizePath(node.path)).filter(Boolean))]
30
+ if (!nodes.length) return []
31
+
32
+ const nodeSet = new Set(nodes)
33
+ const neighbors = new Map(nodes.map((node) => [node, new Map()]))
34
+ const addEdge = (from, to, weight) => {
35
+ if (!nodeSet.has(from) || !nodeSet.has(to) || from === to) return
36
+ const row = neighbors.get(from)
37
+ row.set(to, Number(row.get(to) || 0) + weight)
38
+ }
39
+
40
+ for (const edge of graph.edges || []) {
41
+ const from = normalizePath(edge.from)
42
+ const to = normalizePath(edge.to)
43
+ // Imports carry relevance in both directions: the dependency is relevant to
44
+ // its caller, while callers are useful when a dependency itself is seeded.
45
+ addEdge(from, to, 1)
46
+ addEdge(to, from, 0.55)
47
+ }
48
+
49
+ const seeds = normalizeSeeds(
50
+ nodes,
51
+ options.semanticResults || [],
52
+ options.declared || [],
53
+ options.changed || [],
54
+ )
55
+ const damping = Math.max(0.5, Math.min(0.95, Number(options.damping || 0.85)))
56
+ const iterations = Math.max(4, Math.min(40, Number(options.iterations || 14)))
57
+ let scores = new Map(seeds)
58
+
59
+ for (let iteration = 0; iteration < iterations; iteration += 1) {
60
+ const next = new Map(nodes.map((node) => [node, (1 - damping) * Number(seeds.get(node) || 0)]))
61
+ let dangling = 0
62
+
63
+ for (const node of nodes) {
64
+ const score = Number(scores.get(node) || 0)
65
+ const row = neighbors.get(node)
66
+ const totalWeight = [...row.values()].reduce((sum, weight) => sum + weight, 0)
67
+ if (totalWeight <= 0) {
68
+ dangling += score
69
+ continue
70
+ }
71
+ for (const [target, weight] of row) {
72
+ next.set(target, Number(next.get(target) || 0) + damping * score * (weight / totalWeight))
73
+ }
74
+ }
75
+
76
+ if (dangling > 0) {
77
+ for (const node of nodes) {
78
+ next.set(
79
+ node,
80
+ Number(next.get(node) || 0) + damping * dangling * Number(seeds.get(node) || 0),
81
+ )
82
+ }
83
+ }
84
+ scores = next
85
+ }
86
+
87
+ const semantic = new Map(
88
+ (options.semanticResults || []).map((item) => [normalizePath(item.path), Number(item.score || 0)]),
89
+ )
90
+ const maxRank = Math.max(1e-12, ...scores.values())
91
+ const maxSemantic = Math.max(1, ...semantic.values())
92
+
93
+ return nodes
94
+ .map((file) => {
95
+ const pageRank = Number(scores.get(file) || 0)
96
+ const seed = Number(seeds.get(file) || 0)
97
+ const semanticScore = Number(semantic.get(file) || 0)
98
+ const normalizedRank = pageRank / maxRank
99
+ const normalizedSemantic = semanticScore / maxSemantic
100
+ const score = normalizedRank * 60 + normalizedSemantic * 40
101
+ return {
102
+ path: file,
103
+ score: Number(score.toFixed(6)),
104
+ pageRank: Number(pageRank.toFixed(10)),
105
+ personalization: Number(seed.toFixed(10)),
106
+ semanticScore,
107
+ reasons: [
108
+ semanticScore > 0 ? "semantic-seed" : null,
109
+ (options.declared || []).map(normalizePath).includes(file) ? "declared" : null,
110
+ (options.changed || []).map(normalizePath).includes(file) ? "changed" : null,
111
+ Number(neighbors.get(file)?.size || 0) > 0 ? "dependency-graph" : null,
112
+ ].filter(Boolean),
113
+ }
114
+ })
115
+ .filter((item) => item.score > 0)
116
+ .sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
117
+ .slice(0, Math.max(1, Math.min(100, Number(options.limit || 32))))
118
+ }
@@ -2,9 +2,11 @@ import { existsSync } from "node:fs"
2
2
  import { readFile, stat } from "node:fs/promises"
3
3
  import { spawnSync } from "node:child_process"
4
4
  import path from "node:path"
5
- import { buildRepoGraph } from "./repo-graph.mjs"
5
+ import { buildRepoGraphCached } from "./repo-graph.mjs"
6
6
  import { taskFiles } from "./task-graph.mjs"
7
- import { querySemanticIndex } from "./semantic-index.mjs"
7
+ import { buildSemanticIndexCached, querySemanticIndex } from "./semantic-index.mjs"
8
+ import { queryContextHierarchy } from "./hierarchical-context.mjs"
9
+ import { rankContextGraph } from "./context-graph-rank.mjs"
8
10
 
9
11
  const ROOT_INSTRUCTIONS = [
10
12
  "AGENTS.md",
@@ -247,7 +249,9 @@ export async function buildContextManifest(root, task, options = {}) {
247
249
  const evidenceBudget = options.evidenceBudget || null
248
250
  const declared = taskFiles(task)
249
251
  const terms = taskTerms(task)
250
- const changed = gitChangedFiles(root)
252
+ const changed = Array.isArray(options.changedFiles)
253
+ ? options.changedFiles.map((file) => String(file || "").replaceAll("\\", "/")).filter(Boolean)
254
+ : gitChangedFiles(root)
251
255
  const strategy = String(options.strategy || "semantic+graph+git")
252
256
  const queryText = [
253
257
  task?.title,
@@ -256,15 +260,45 @@ export async function buildContextManifest(root, task, options = {}) {
256
260
  ...(task?.verification || []),
257
261
  ].filter(Boolean).join(" ")
258
262
 
263
+ const semanticSnapshot = await buildSemanticIndexCached(root, {
264
+ maxFiles: options.semanticMaxFiles ?? 6000,
265
+ maxDepth: options.semanticMaxDepth ?? 14,
266
+ workspaceFingerprint: options.workspaceFingerprint,
267
+ }).catch(() => null)
268
+
269
+ const hierarchy = await queryContextHierarchy(root, queryText, {
270
+ declaredFiles: declared,
271
+ maxScopes: options.hierarchyMaxScopes ?? 6,
272
+ maxFiles: options.semanticMaxFiles ?? 6000,
273
+ builtIndex: semanticSnapshot || undefined,
274
+ }).catch(() => null)
275
+ const hierarchyScopes = hierarchy?.scopes?.map((item) => item.path) || []
276
+
259
277
  const semantic = await querySemanticIndex(root, queryText, {
260
278
  limit: 32,
261
279
  maxFiles: options.semanticMaxFiles ?? 6000,
280
+ builtIndex: semanticSnapshot || undefined,
262
281
  }).catch(() => null)
263
- const semanticPaths = semantic?.results?.map((item) => item.path) || []
282
+ const scopedSemanticResults = (semantic?.results || []).map((item) => {
283
+ const scopeRank = hierarchyScopes.findIndex((scope) =>
284
+ scope === "." || item.path === scope || item.path.startsWith(scope + "/"),
285
+ )
286
+ const scopeBoost = scopeRank >= 0 ? Math.max(2, 12 - scopeRank * 2) : 0
287
+ return {
288
+ ...item,
289
+ baseScore: item.score,
290
+ scopeBoost,
291
+ score: Number((Number(item.score || 0) + scopeBoost).toFixed(3)),
292
+ }
293
+ }).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
294
+ const semanticPaths = scopedSemanticResults.map((item) => item.path)
264
295
 
265
296
  const useGraph = strategy.includes("graph")
266
297
  const graph = useGraph
267
- ? await buildRepoGraph(root, { maxFiles: options.maxFiles ?? 2500 })
298
+ ? await buildRepoGraphCached(root, {
299
+ maxFiles: options.maxFiles ?? 2500,
300
+ workspaceFingerprint: options.workspaceFingerprint,
301
+ })
268
302
  : { nodes: [], edges: [], hotspots: [], scannedFiles: 0, truncated: false }
269
303
  const nodeMap = new Map(graph.nodes.map((node) => [node.path, node]))
270
304
  const incoming = new Map()
@@ -297,17 +331,46 @@ export async function buildContextManifest(root, task, options = {}) {
297
331
  .map((item) => item.file)
298
332
 
299
333
  const instructions = await nearbyInstructions(root, declared)
300
- const ranked = semantic?.results?.length
301
- ? semantic.results.map((item) => ({
302
- path: item.path,
303
- score: item.score,
304
- matches: item.reasons || [],
305
- symbolHits: item.definitions || [],
306
- evidenceLevel: semantic.evidenceLevel,
307
- }))
308
- : useGraph
309
- ? await rankedReferences(root, graph.nodes, terms, declared, changed)
310
- : []
334
+ const graphRanked = useGraph
335
+ ? rankContextGraph(graph, {
336
+ semanticResults: scopedSemanticResults,
337
+ declared,
338
+ changed,
339
+ limit: 32,
340
+ })
341
+ : []
342
+
343
+ const semanticByPath = new Map(scopedSemanticResults.map((item) => [item.path, item]))
344
+ const ranked = graphRanked.length
345
+ ? graphRanked.map((item) => {
346
+ const semanticItem = semanticByPath.get(item.path)
347
+ return {
348
+ path: item.path,
349
+ score: item.score,
350
+ pageRank: item.pageRank,
351
+ personalization: item.personalization,
352
+ baseScore: semanticItem?.baseScore ?? semanticItem?.score ?? item.semanticScore,
353
+ scopeBoost: semanticItem?.scopeBoost || 0,
354
+ matches: [...new Set([...(semanticItem?.reasons || []), ...(item.reasons || [])])],
355
+ symbolHits: semanticItem?.definitions || [],
356
+ evidenceLevel: semanticItem
357
+ ? String(semantic?.evidenceLevel || "syntax-aware-lexical") + "+dependency-graph-rank"
358
+ : "dependency-graph-rank",
359
+ }
360
+ })
361
+ : scopedSemanticResults.length
362
+ ? scopedSemanticResults.map((item) => ({
363
+ path: item.path,
364
+ score: item.score,
365
+ baseScore: item.baseScore ?? item.score,
366
+ scopeBoost: item.scopeBoost || 0,
367
+ matches: item.reasons || [],
368
+ symbolHits: item.definitions || [],
369
+ evidenceLevel: semantic.evidenceLevel,
370
+ }))
371
+ : useGraph
372
+ ? await rankedReferences(root, graph.nodes, terms, declared, changed)
373
+ : []
311
374
  const changedRelevant = changed
312
375
  .filter((file) =>
313
376
  declared.includes(file) ||
@@ -372,6 +435,18 @@ export async function buildContextManifest(root, task, options = {}) {
372
435
  rankedReferences: ranked,
373
436
  files,
374
437
  excerpts,
438
+ hierarchy: hierarchy ? {
439
+ schemaVersion: hierarchy.schemaVersion,
440
+ levels: hierarchy.levels,
441
+ scopes: hierarchy.scopes.slice(0, 6).map((item) => ({
442
+ path: item.path,
443
+ score: item.score,
444
+ reasons: item.reasons,
445
+ fileCount: item.fileCount,
446
+ l0: item.l0,
447
+ l1: item.l1,
448
+ })),
449
+ } : null,
375
450
  semantic: semantic ? {
376
451
  evidenceLevel: semantic.evidenceLevel,
377
452
  resultCount: semantic.results.length,
@@ -382,11 +457,15 @@ export async function buildContextManifest(root, task, options = {}) {
382
457
  scannedFiles: graph.scannedFiles,
383
458
  truncated: graph.truncated,
384
459
  hotspots: graph.hotspots.slice(0, 12),
460
+ ranking: graphRanked.length ? {
461
+ strategy: "personalized-dependency-graph-rank",
462
+ resultCount: graphRanked.length,
463
+ top: graphRanked.slice(0, 12),
464
+ } : null,
385
465
  },
386
466
  budget,
387
467
  evidenceBudget,
388
468
  categoryRemaining,
389
469
  used: budget - remaining,
390
470
  }
391
- }
392
-
471
+ }