opencode-agent-skill 14.2.0-beta.1 → 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.
package/README.md CHANGED
@@ -4,8 +4,8 @@
4
4
 
5
5
  **Package:** <code>opencode-agent-skill</code>
6
6
  **Host chính:** Pi Agent
7
- **Phiên bản package hiện tại:** <code>14.2.0-beta.1</code>
8
- **Nhánh phát triển hiện tại:** V14.2 Turbo Weak-Model Runtime
7
+ **Phiên bản package hiện tại:** <code>14.4.0</code>
8
+ **Release hiện tại:** V14.4 Stable Weak-Model Intelligence Runtime
9
9
  **Runtime:** Node.js 22.19+
10
10
  **License:** MIT
11
11
 
@@ -1352,7 +1352,22 @@ npm install -g @earendil-works/pi-coding-agent
1352
1352
  pi --version
1353
1353
  ~~~
1354
1354
 
1355
- Cài UES trực tiếp từ GitHub:
1355
+ Cài UES stable từ npm (khuyến nghị):
1356
+
1357
+ ~~~cmd
1358
+ pi install npm:opencode-agent-skill
1359
+ pi list
1360
+ pi
1361
+ ~~~
1362
+
1363
+ Hoặc cài CLI toàn cục:
1364
+
1365
+ ~~~cmd
1366
+ npm install -g opencode-agent-skill@latest
1367
+ ues version
1368
+ ~~~
1369
+
1370
+ Cài trực tiếp từ GitHub main nếu muốn dùng source mới nhất:
1356
1371
 
1357
1372
  ~~~cmd
1358
1373
  pi install git:github.com/laivannha0202/opencode-agent-skill-
@@ -1396,18 +1411,18 @@ npm pack --dry-run
1396
1411
  npm pack
1397
1412
  ~~~
1398
1413
 
1399
- Package version hiện tại trong <code>package.json</code> là:
1414
+ Package stable hiện tại trong <code>package.json</code> là:
1400
1415
 
1401
1416
  ~~~text
1402
- 14.2.0-beta.1
1417
+ 14.4.0
1403
1418
  ~~~
1404
1419
 
1405
- V14.2 Turbo Weak-Model Runtime hiện là incremental work trên package version đó.
1420
+ V14.4 gom V14/V14.1/V14.2 cùng code intelligence, hash-anchored editing, completion auditor, stable memory snapshots, reversible context/document ingestion và MCP health-aware routing thành release stable.
1406
1421
 
1407
1422
  Test file packed với Pi:
1408
1423
 
1409
1424
  ~~~cmd
1410
- pi install .\opencode-agent-skill-14.2.0-beta.1.tgz
1425
+ pi install .\opencode-agent-skill-14.4.0.tgz
1411
1426
  pi list
1412
1427
  ~~~
1413
1428
 
@@ -1757,7 +1772,7 @@ UES cố ý **không**:
1757
1772
 
1758
1773
  ~~~cmd
1759
1774
  npm install -g @earendil-works/pi-coding-agent
1760
- pi install git:github.com/laivannha0202/opencode-agent-skill-
1775
+ pi install npm:opencode-agent-skill
1761
1776
  pi
1762
1777
  ~~~
1763
1778
 
package/docs/PI-COMPAT.md CHANGED
@@ -10,13 +10,28 @@ Repository này hiện đóng gói UES cho **Pi Agent**.
10
10
 
11
11
  ## Install
12
12
 
13
+ Stable npm release:
14
+
13
15
  ```cmd
14
16
  npm install -g @earendil-works/pi-coding-agent
15
- pi install git:github.com/laivannha0202/opencode-agent-skill-
17
+ pi install npm:opencode-agent-skill
16
18
  pi list
17
19
  pi
18
20
  ```
19
21
 
22
+ Optional global CLI:
23
+
24
+ ```cmd
25
+ npm install -g opencode-agent-skill@latest
26
+ ues version
27
+ ```
28
+
29
+ GitHub main can still be installed when source-head testing is intended:
30
+
31
+ ```cmd
32
+ pi install git:github.com/laivannha0202/opencode-agent-skill-
33
+ ```
34
+
20
35
  Project-local:
21
36
 
22
37
  ```cmd
@@ -106,6 +106,29 @@ export function defaultCapabilityRegistry(root = process.cwd()) {
106
106
  "code.search": [
107
107
  { id: "ues-semantic-index", kind: "builtin", priority: 100, quality: 0.9, costClass: "low", latencyClass: "fast" },
108
108
  ],
109
+ "code.edit.anchored": [
110
+ { id: "ues-hashline", kind: "builtin", priority: 120, quality: 0.98, costClass: "low", latencyClass: "fast", metadata: { failClosed: true, staleAnchorReject: true } },
111
+ ],
112
+ "code.structure": [
113
+ { id: "ast-grep", kind: "command", command: "ast-grep", priority: 120, quality: 0.96, costClass: "low", latencyClass: "fast" },
114
+ { id: "sg", kind: "command", command: "sg", priority: 115, quality: 0.96, costClass: "low", latencyClass: "fast" },
115
+ { id: "ues-semantic-index", kind: "builtin", priority: 80, quality: 0.8, costClass: "low", latencyClass: "fast" },
116
+ ],
117
+ "code.diagnostics": [
118
+ { id: "typescript-language-server", kind: "command", command: "typescript-language-server", priority: 110, quality: 0.95, costClass: "low", latencyClass: "medium" },
119
+ { id: "pyright-langserver", kind: "command", command: "pyright-langserver", priority: 105, quality: 0.95, costClass: "low", latencyClass: "medium" },
120
+ { id: "pylsp", kind: "command", command: "pylsp", priority: 95, quality: 0.85, costClass: "low", latencyClass: "medium" },
121
+ { id: "gopls", kind: "command", command: "gopls", priority: 105, quality: 0.95, costClass: "low", latencyClass: "medium" },
122
+ { id: "rust-analyzer", kind: "command", command: "rust-analyzer", priority: 105, quality: 0.95, costClass: "low", latencyClass: "medium" },
123
+ { id: "clangd", kind: "command", command: "clangd", priority: 100, quality: 0.9, costClass: "low", latencyClass: "medium" },
124
+ ],
125
+ "document.ingestion": [
126
+ { id: "ues-builtin-text", kind: "builtin", priority: 100, quality: 0.95, costClass: "low", latencyClass: "fast", metadata: { formats: ["text", "markdown", "json", "csv", "html", "xml"] } },
127
+ { id: "markitdown-cli", kind: "command", command: "markitdown", priority: 95, quality: 0.95, costClass: "low", latencyClass: "medium", metadata: { optional: true, formats: ["pdf", "docx", "pptx", "xlsx", "epub", "zip"] } },
128
+ ],
129
+ "context.reversible": [
130
+ { id: "ues-evidence-context", kind: "builtin", priority: 120, quality: 0.98, costClass: "low", latencyClass: "fast", metadata: { reversible: true, searchable: true, levels: ["T1", "T2", "T3", "raw"] } },
131
+ ],
109
132
  memory: [
110
133
  { id: "ues-memory", kind: "builtin", priority: 100, quality: 0.9, costClass: "low", latencyClass: "fast" },
111
134
  ],
@@ -0,0 +1,102 @@
1
+ import { createHash } from "node:crypto"
2
+
3
+ const DEFAULT_HASH_CHARS = 8
4
+
5
+ function normalizeNewlines(value) {
6
+ return String(value ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n")
7
+ }
8
+
9
+ function hashText(value, chars = DEFAULT_HASH_CHARS) {
10
+ return createHash("sha256").update(String(value ?? "")).digest("hex").slice(0, Math.max(6, Math.min(16, Number(chars || DEFAULT_HASH_CHARS))))
11
+ }
12
+
13
+ export function lineAnchor(lineNumber, lineText, options = {}) {
14
+ const line = Math.max(1, Math.trunc(Number(lineNumber || 1)))
15
+ return `L${line}:${hashText(lineText, options.hashChars)}`
16
+ }
17
+
18
+ export function parseLineAnchor(anchor) {
19
+ const match = String(anchor || "").trim().match(/^L([1-9]\d*):([a-f0-9]{6,16})$/i)
20
+ if (!match) return null
21
+ return { line: Number(match[1]), hash: match[2].toLowerCase() }
22
+ }
23
+
24
+ export function anchoredLines(source, options = {}) {
25
+ const text = normalizeNewlines(source)
26
+ const lines = text.split("\n")
27
+ const startLine = Math.max(1, Math.trunc(Number(options.startLine || 1)))
28
+ const requestedEnd = options.endLine == null ? lines.length : Math.trunc(Number(options.endLine))
29
+ const endLine = Math.max(startLine, Math.min(lines.length, requestedEnd || lines.length))
30
+ const rows = []
31
+ for (let line = startLine; line <= endLine; line += 1) {
32
+ const value = lines[line - 1] ?? ""
33
+ rows.push({ line, anchor: lineAnchor(line, value, options), text: value })
34
+ }
35
+ return {
36
+ schemaVersion: 1,
37
+ startLine,
38
+ endLine,
39
+ lineCount: lines.length,
40
+ sourceHash: hashText(text, 16),
41
+ rows,
42
+ text: rows.map((row) => `${row.anchor}|${row.text}`).join("\n"),
43
+ }
44
+ }
45
+
46
+ function validateAnchor(lines, rawAnchor, label, options = {}) {
47
+ const parsed = parseLineAnchor(rawAnchor)
48
+ if (!parsed) throw new Error(`${label} must use L<line>:<hash> format`)
49
+ if (parsed.line > lines.length) throw new Error(`${label} points beyond end of file`)
50
+ const actual = lineAnchor(parsed.line, lines[parsed.line - 1] ?? "", options)
51
+ if (actual.toLowerCase() !== String(rawAnchor).toLowerCase()) {
52
+ const error = new Error(`${label} is stale or ambiguous; re-read anchored source before editing`)
53
+ error.code = "UES_STALE_ANCHOR"
54
+ error.expected = actual
55
+ error.received = String(rawAnchor)
56
+ throw error
57
+ }
58
+ return parsed
59
+ }
60
+
61
+ export function applyAnchoredEdits(source, edits = [], options = {}) {
62
+ const text = normalizeNewlines(source)
63
+ const lines = text.split("\n")
64
+ if (!Array.isArray(edits) || edits.length === 0) throw new Error("at least one anchored edit is required")
65
+ const maxEdits = Math.max(1, Math.min(200, Number(options.maxEdits || 50)))
66
+ if (edits.length > maxEdits) throw new Error(`too many anchored edits; maximum is ${maxEdits}`)
67
+
68
+ const prepared = edits.map((edit, index) => {
69
+ const start = validateAnchor(lines, edit?.anchor || edit?.startAnchor, `edit[${index}].anchor`, options)
70
+ const end = edit?.endAnchor
71
+ ? validateAnchor(lines, edit.endAnchor, `edit[${index}].endAnchor`, options)
72
+ : start
73
+ if (end.line < start.line) throw new Error(`edit[${index}] endAnchor precedes anchor`)
74
+ return {
75
+ index,
76
+ startLine: start.line,
77
+ endLine: end.line,
78
+ replacement: normalizeNewlines(edit?.replacement ?? ""),
79
+ }
80
+ })
81
+
82
+ prepared.sort((a, b) => a.startLine - b.startLine || a.endLine - b.endLine)
83
+ for (let index = 1; index < prepared.length; index += 1) {
84
+ if (prepared[index].startLine <= prepared[index - 1].endLine) {
85
+ throw new Error(`anchored edits overlap at edit[${prepared[index].index}]`)
86
+ }
87
+ }
88
+
89
+ const next = [...lines]
90
+ for (const edit of [...prepared].sort((a, b) => b.startLine - a.startLine)) {
91
+ const replacementLines = edit.replacement.split("\n")
92
+ next.splice(edit.startLine - 1, edit.endLine - edit.startLine + 1, ...replacementLines)
93
+ }
94
+ const output = next.join("\n")
95
+ return {
96
+ schemaVersion: 1,
97
+ sourceHash: hashText(text, 16),
98
+ outputHash: hashText(output, 16),
99
+ applied: prepared.length,
100
+ text: output,
101
+ }
102
+ }
@@ -0,0 +1,95 @@
1
+ import { lstat, readFile, realpath, rename, rm, writeFile } from "node:fs/promises"
2
+ import { spawnSync } from "node:child_process"
3
+ import path from "node:path"
4
+ import { querySemanticIndex } from "../semantic-index.mjs"
5
+ import { anchoredLines, applyAnchoredEdits } from "./edit-anchor.mjs"
6
+ import { diagnoseCode, lspProviderStatus } from "./lsp-provider.mjs"
7
+
8
+ const AST_COMMANDS = ["ast-grep", "sg"]
9
+ const commandCache = new Map()
10
+ const COMMAND_TTL_MS = 60_000
11
+
12
+ function commandExists(command) {
13
+ const now = Date.now()
14
+ const cached = commandCache.get(command)
15
+ if (cached && now - cached.at < COMMAND_TTL_MS) return cached.available
16
+ const finder = process.platform === "win32" ? "where" : "which"
17
+ const available = spawnSync(finder, [command], { stdio: "ignore", windowsHide: true }).status === 0
18
+ commandCache.set(command, { available, at: now })
19
+ return available
20
+ }
21
+
22
+ function inside(root, target) { return target === root || target.startsWith(root + path.sep) }
23
+
24
+ async function safeFile(root, relative) {
25
+ const base = await realpath(path.resolve(root)).catch(() => path.resolve(root))
26
+ const requested = path.resolve(base, String(relative || ""))
27
+ if (!inside(base, requested)) throw new Error("code path escapes workspace root")
28
+ const info = await lstat(requested)
29
+ if (!info.isFile()) throw new Error("code path is not a file")
30
+ const actual = await realpath(requested)
31
+ if (!inside(base, actual)) throw new Error("code symlink escapes workspace root")
32
+ return { base, file: actual, info }
33
+ }
34
+
35
+ export function probeCodeIntelligence(file = "") {
36
+ const astProvider = AST_COMMANDS.find(commandExists) || null
37
+ return {
38
+ schemaVersion: 1,
39
+ anchoredEditing: true,
40
+ semanticIndex: true,
41
+ astProvider,
42
+ lsp: lspProviderStatus(file),
43
+ }
44
+ }
45
+
46
+ export async function readAnchoredCode(root, relative, options = {}) {
47
+ const target = await safeFile(root, relative)
48
+ const maxBytes = Math.max(8 * 1024, Math.min(4 * 1024 * 1024, Number(options.maxBytes || 512 * 1024)))
49
+ if (target.info.size > maxBytes) throw new Error("code file exceeds anchored-read limit (" + target.info.size + " > " + maxBytes + " bytes)")
50
+ const source = await readFile(target.file, "utf8")
51
+ return { file: path.relative(target.base, target.file).replaceAll("\\", "/"), ...anchoredLines(source, options) }
52
+ }
53
+
54
+ export async function applyAnchoredFileEdits(root, relative, edits, options = {}) {
55
+ const target = await safeFile(root, relative)
56
+ const maxBytes = Math.max(8 * 1024, Math.min(4 * 1024 * 1024, Number(options.maxBytes || 512 * 1024)))
57
+ if (target.info.size > maxBytes) throw new Error("code file exceeds anchored-edit limit (" + target.info.size + " > " + maxBytes + " bytes)")
58
+ const source = await readFile(target.file, "utf8")
59
+ const result = applyAnchoredEdits(source, edits, options)
60
+ const relativeFile = path.relative(target.base, target.file).replaceAll("\\", "/")
61
+ if (options.dryRun === true) return { ...result, file: relativeFile, dryRun: true }
62
+ const temp = target.file + "." + process.pid + "." + Date.now() + ".ues.tmp"
63
+ await writeFile(temp, result.text, "utf8")
64
+ try { await rename(temp, target.file) } catch (error) { await rm(temp, { force: true }).catch(() => {}); throw error }
65
+ return { ...result, file: relativeFile, dryRun: false }
66
+ }
67
+
68
+ function runAstGrep(root, pattern, language, limit) {
69
+ const command = AST_COMMANDS.find(commandExists) || null
70
+ if (!command || !String(pattern || "").trim()) return { provider: null, results: [], error: null }
71
+ const args = ["run", "--pattern", String(pattern), "--json=stream"]
72
+ if (language) args.push("--lang", String(language))
73
+ args.push(".")
74
+ const result = spawnSync(command, args, { cwd: path.resolve(root), encoding: "utf8", timeout: 20_000, maxBuffer: 4 * 1024 * 1024, windowsHide: true })
75
+ if (result.status !== 0) return { provider: command, results: [], error: String(result.stderr || ("ast-grep exit " + result.status)).trim().slice(0, 1000) }
76
+ const rows = String(result.stdout || "").split(/\r?\n/).filter(Boolean).slice(0, limit).map((line) => {
77
+ try { return JSON.parse(line) } catch { return { raw: line } }
78
+ })
79
+ return { provider: command, results: rows, error: null }
80
+ }
81
+
82
+ export async function searchCodeIntelligence(root, query, options = {}) {
83
+ const maxResults = Math.max(1, Math.min(50, Number(options.maxResults || 12)))
84
+ const semantic = await querySemanticIndex(root, query, { maxFiles: options.maxFiles || 6000, limit: maxResults })
85
+ const structural = options.structuralPattern ? runAstGrep(root, options.structuralPattern, options.language, maxResults) : { provider: null, results: [], error: null }
86
+ return {
87
+ schemaVersion: 1,
88
+ query: String(query || ""),
89
+ providers: probeCodeIntelligence(options.file || ""),
90
+ semantic: { ...semantic, results: (semantic.results || []).slice(0, maxResults) },
91
+ structural,
92
+ }
93
+ }
94
+
95
+ export { diagnoseCode, lspProviderStatus } from "./lsp-provider.mjs"
@@ -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
+ }
@@ -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
+ }