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.
- package/README.md +1571 -607
- package/bin/ocskill.mjs +172 -24
- package/docs/OPENCODE-COMPAT.md +34 -97
- package/docs/PI-COMPAT.md +203 -0
- package/docs/V14-CONTEXT-MEMORY-FABRIC.md +70 -0
- package/docs/V14.1-QUALITY-PERFORMANCE-FABRIC.md +114 -0
- package/docs/V14.2-TURBO-WEAK-MODEL-RUNTIME.md +448 -0
- package/evals/v14/tasks.json +46 -0
- package/global-config/agents/executor.md +7 -0
- package/global-config/agents/visual-verifier.md +22 -3
- package/global-config/plugins/ues-router/index.js +13 -8
- package/global-config/plugins/ues-router/policy-runtime.js +7 -0
- package/global-config/plugins/ues-router/router.js +12 -2
- package/global-config/skills/ecommerce-engineering/SKILL.md +1 -1
- package/global-config/skills/file-upload-engineering/SKILL.md +1 -1
- package/global-config/skills/git-safety/SKILL.md +1 -1
- package/global-config/skills/nestjs-engineering/SKILL.md +1 -1
- package/global-config/skills/performance-engineering/SKILL.md +1 -1
- package/global-config/skills/react-native-engineering/SKILL.md +1 -1
- package/global-config/skills/rest-api-design/SKILL.md +1 -1
- package/global-config/skills/ui-ux-engineering/SKILL.md +1 -1
- package/lib/adaptive-context-budget.mjs +97 -0
- package/lib/affected-tests.mjs +260 -0
- package/lib/benchmark-confidence.mjs +41 -2
- package/lib/browser-mcp-routing.mjs +166 -0
- package/lib/capability-fabric.mjs +359 -0
- package/lib/capability-registry.mjs +9 -0
- package/lib/code-intelligence/edit-anchor.mjs +102 -0
- package/lib/code-intelligence/index.mjs +95 -0
- package/lib/code-intelligence/lsp-provider.mjs +189 -0
- package/lib/completion-auditor.mjs +82 -0
- package/lib/context-engine-v11.mjs +65 -1
- package/lib/context-graph-rank.mjs +118 -0
- package/lib/context-manifest.mjs +97 -18
- package/lib/control-center.mjs +19 -1
- package/lib/document-ingestion.mjs +60 -0
- package/lib/dynamic-workflow.mjs +3 -1
- package/lib/evidence-store.mjs +82 -1
- package/lib/fast-verification-gate.mjs +69 -0
- package/lib/hierarchical-context.mjs +215 -0
- package/lib/mcp-health.mjs +144 -0
- package/lib/mcp-tool-policy.mjs +19 -0
- package/lib/memory-engine.mjs +493 -0
- package/lib/model-performance.mjs +33 -8
- package/lib/model-policy.mjs +3 -3
- package/lib/orchestrator-policy.mjs +5 -209
- package/lib/performance-fabric.mjs +229 -0
- package/lib/pi-rpc-pool.mjs +433 -0
- package/lib/process-hang-detector.mjs +83 -0
- package/lib/process-supervisor.mjs +193 -0
- package/lib/prompt-cache.mjs +27 -11
- package/lib/repo-graph.mjs +53 -2
- package/lib/reversible-context.mjs +49 -0
- package/lib/runtime-config.mjs +31 -0
- package/lib/safety.mjs +132 -0
- package/lib/semantic-index.mjs +52 -3
- package/lib/skill-compiler.mjs +128 -0
- package/lib/skill-quality.mjs +48 -2
- package/lib/task-engine.mjs +62 -5
- package/lib/task-policy.mjs +291 -0
- package/lib/verification-broker.mjs +284 -0
- package/lib/verification-command.mjs +111 -0
- package/lib/windows-shim.mjs +35 -0
- package/lib/workspace-fingerprint.mjs +198 -0
- package/package.json +52 -42
- package/pi/extensions/ues-child-runtime.ts +393 -0
- package/pi/extensions/ues.ts +3327 -0
- package/pi/prompts/ues-audit.md +9 -0
- package/pi/prompts/ues-critique.md +9 -0
- package/pi/prompts/ues-debug.md +9 -0
- package/pi/prompts/ues-feature.md +9 -0
- package/pi/prompts/ues-fix.md +9 -0
- package/pi/prompts/ues-plan.md +9 -0
- package/pi/prompts/ues-research.md +9 -0
- package/pi/prompts/ues-resume.md +9 -0
- package/pi/prompts/ues-review.md +7 -0
- package/pi/prompts/ues-run.md +17 -0
- package/pi/prompts/ues-verify.md +9 -0
- package/scripts/check-release-consistency.mjs +119 -185
- package/scripts/check-runtime-exports.mjs +66 -0
- package/scripts/check-source-integrity.mjs +246 -0
- package/scripts/eval-pi.mjs +492 -0
- package/scripts/install.mjs +16 -0
- package/scripts/smoke-package-closure.mjs +110 -0
- package/scripts/smoke-packed-install.mjs +24 -11
- package/scripts/smoke-pi-extension.mjs +144 -0
- package/scripts/uninstall.mjs +44 -0
- package/CHANGELOG.md +0 -415
- package/docs/DETERMINISTIC-TOOLS.md +0 -105
- package/docs/ENGINEERING-DESIGN.md +0 -194
- package/docs/EVALS.md +0 -158
- package/docs/GITHUB-RULESET.md +0 -50
- package/docs/NPM-PUBLISH.md +0 -116
- package/docs/RESEARCH-SOURCES.md +0 -37
- package/docs/TRACE-SCHEMA.md +0 -122
- package/docs/V11-PERCEPTION-ADAPTIVE-EXECUTION.md +0 -75
- package/docs/V11-PERCEPTION-ADAPTIVE.md +0 -220
- package/docs/V12-WEAK-MODEL-INTELLIGENCE.md +0 -27
- package/docs/V13-PARALLEL-WEAK-MODEL-RUNTIME.md +0 -86
- package/docs/V7-INTELLIGENCE-RUNTIME.md +0 -166
- package/docs/V8-INTELLIGENCE-RELIABILITY.md +0 -206
- package/docs/V9-SPEED-INTELLIGENCE.md +0 -102
package/lib/prompt-cache.mjs
CHANGED
|
@@ -5,30 +5,36 @@ function canonical(value) {
|
|
|
5
5
|
if (!value || typeof value !== "object") return value
|
|
6
6
|
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]))
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
function stringify(value)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return createHash("sha256").update(stringify(value)).digest("hex")
|
|
8
|
+
function stringify(value) { return typeof value === "string" ? value : JSON.stringify(canonical(value)) }
|
|
9
|
+
function digest(value) { return createHash("sha256").update(stringify(value)).digest("hex") }
|
|
10
|
+
function chars(value) { return stringify(value ?? "").length }
|
|
11
|
+
function finiteTokenCount(value) {
|
|
12
|
+
const number = Number(value)
|
|
13
|
+
return Number.isFinite(number) && number >= 0 ? number : null
|
|
15
14
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
function changedStableKeys(previous, current) {
|
|
16
|
+
const left = previous?.stable || {}
|
|
17
|
+
const right = current?.stable || {}
|
|
18
|
+
return [...new Set([...Object.keys(left), ...Object.keys(right)])]
|
|
19
|
+
.filter((key) => digest(left[key]) !== digest(right[key]))
|
|
20
|
+
.sort()
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export function buildPromptEnvelope(input = {}) {
|
|
24
|
+
const memorySnapshot = input.memorySnapshot || null
|
|
22
25
|
const stable = {
|
|
23
26
|
invariants: input.invariants || "",
|
|
24
27
|
role: input.role || "",
|
|
25
28
|
skills: input.skills || [],
|
|
26
29
|
projectFacts: input.projectFacts || {},
|
|
27
30
|
toolPolicy: input.toolPolicy || {},
|
|
31
|
+
memorySnapshot,
|
|
28
32
|
}
|
|
29
33
|
const dynamic = {
|
|
30
34
|
task: input.task || {},
|
|
31
35
|
evidence: input.evidence || [],
|
|
36
|
+
memories: memorySnapshot ? [] : (input.memories || []),
|
|
37
|
+
contextHints: input.contextHints || {},
|
|
32
38
|
recentFailure: input.recentFailure || null,
|
|
33
39
|
nextAction: input.nextAction || null,
|
|
34
40
|
recentMessages: input.recentMessages || [],
|
|
@@ -43,18 +49,28 @@ export function buildPromptEnvelope(input = {}) {
|
|
|
43
49
|
dynamicHash: digest(dynamic),
|
|
44
50
|
stableChars,
|
|
45
51
|
dynamicChars,
|
|
52
|
+
cacheableChars: stableChars,
|
|
46
53
|
cacheableRatio: stableChars + dynamicChars ? stableChars / (stableChars + dynamicChars) : 0,
|
|
54
|
+
cacheReadTokens: finiteTokenCount(input.cacheReadTokens),
|
|
55
|
+
cacheWriteTokens: finiteTokenCount(input.cacheWriteTokens),
|
|
47
56
|
}
|
|
48
57
|
}
|
|
49
58
|
|
|
50
|
-
export function comparePromptEnvelopes(previous, current) {
|
|
59
|
+
export function comparePromptEnvelopes(previous, current, usage = {}) {
|
|
51
60
|
const stableReused = Boolean(previous?.stablePrefixHash && previous.stablePrefixHash === current?.stablePrefixHash)
|
|
52
61
|
const repeatedStableChars = stableReused ? Number(current?.stableChars || 0) : 0
|
|
62
|
+
const mutated = stableReused ? [] : changedStableKeys(previous, current)
|
|
53
63
|
return {
|
|
54
64
|
schemaVersion: 1,
|
|
55
65
|
stableReused,
|
|
56
66
|
repeatedStableChars,
|
|
57
67
|
avoidableRepeatedChars: stableReused ? 0 : Math.min(Number(previous?.stableChars || 0), Number(current?.stableChars || 0)),
|
|
58
68
|
currentCacheableRatio: Number(current?.cacheableRatio || 0),
|
|
69
|
+
cacheableChars: Number(current?.cacheableChars ?? current?.stableChars ?? 0),
|
|
70
|
+
dynamicChars: Number(current?.dynamicChars || 0),
|
|
71
|
+
cacheReadTokens: finiteTokenCount(usage.cacheReadTokens ?? current?.cacheReadTokens),
|
|
72
|
+
cacheWriteTokens: finiteTokenCount(usage.cacheWriteTokens ?? current?.cacheWriteTokens),
|
|
73
|
+
prefixMutationReason: stableReused ? null : (mutated.length ? mutated.join(",") : "stable-prefix-changed"),
|
|
74
|
+
mutatedStableKeys: mutated,
|
|
59
75
|
}
|
|
60
76
|
}
|
package/lib/repo-graph.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { readFile, readdir, stat } from "node:fs/promises"
|
|
2
2
|
import path from "node:path"
|
|
3
|
+
import { runtimeWorkspaceFingerprint } from "./workspace-fingerprint.mjs"
|
|
3
4
|
|
|
4
5
|
const SKIP = new Set([
|
|
5
6
|
".git", "node_modules", ".next", "dist", "build", "coverage", ".venv", "venv",
|
|
6
|
-
"Pods", "DerivedData", ".gradle", ".idea", ".cache", ".turbo", "target", "
|
|
7
|
-
".ues-work",
|
|
7
|
+
"Pods", "DerivedData", ".gradle", ".idea", ".cache", ".turbo", "target", "obj",
|
|
8
|
+
".ues-work", ".ues-cache", ".ues-traces", ".ues-learning", ".ues-dashboard",
|
|
9
|
+
".ues-sandboxes", ".ues-memory", ".ues-evals",
|
|
8
10
|
])
|
|
9
11
|
|
|
10
12
|
const SOURCE_EXTENSIONS = new Set([
|
|
@@ -133,3 +135,52 @@ export async function buildRepoGraph(root = process.cwd(), options = {}) {
|
|
|
133
135
|
.slice(0, 50),
|
|
134
136
|
}
|
|
135
137
|
}
|
|
138
|
+
|
|
139
|
+
const RUNTIME_GRAPH_CACHE = new Map()
|
|
140
|
+
const RUNTIME_GRAPH_INFLIGHT = new Map()
|
|
141
|
+
|
|
142
|
+
export async function buildRepoGraphCached(root = process.cwd(), options = {}) {
|
|
143
|
+
root = path.resolve(root)
|
|
144
|
+
const maxFiles = Number(options.maxFiles ?? 2500)
|
|
145
|
+
const maxDepth = Number(options.maxDepth ?? 8)
|
|
146
|
+
let fingerprint = String(options.workspaceFingerprint || "")
|
|
147
|
+
if (!fingerprint || fingerprint === "unknown") {
|
|
148
|
+
try {
|
|
149
|
+
fingerprint = runtimeWorkspaceFingerprint(root)
|
|
150
|
+
} catch {
|
|
151
|
+
return buildRepoGraph(root, options)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const key = [root, fingerprint, maxFiles, maxDepth].join("\u0000")
|
|
155
|
+
if (RUNTIME_GRAPH_CACHE.has(key)) {
|
|
156
|
+
const value = RUNTIME_GRAPH_CACHE.get(key)
|
|
157
|
+
RUNTIME_GRAPH_CACHE.delete(key)
|
|
158
|
+
RUNTIME_GRAPH_CACHE.set(key, value)
|
|
159
|
+
return value
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (RUNTIME_GRAPH_INFLIGHT.has(key)) {
|
|
163
|
+
return RUNTIME_GRAPH_INFLIGHT.get(key)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const pending = buildRepoGraph(root, options)
|
|
167
|
+
RUNTIME_GRAPH_INFLIGHT.set(key, pending)
|
|
168
|
+
let graph
|
|
169
|
+
try {
|
|
170
|
+
graph = await pending
|
|
171
|
+
} finally {
|
|
172
|
+
RUNTIME_GRAPH_INFLIGHT.delete(key)
|
|
173
|
+
}
|
|
174
|
+
RUNTIME_GRAPH_CACHE.set(key, graph)
|
|
175
|
+
while (RUNTIME_GRAPH_CACHE.size > 8) {
|
|
176
|
+
const oldest = RUNTIME_GRAPH_CACHE.keys().next().value
|
|
177
|
+
if (!oldest) break
|
|
178
|
+
RUNTIME_GRAPH_CACHE.delete(oldest)
|
|
179
|
+
}
|
|
180
|
+
return graph
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function clearRepoGraphRuntimeCache() {
|
|
184
|
+
RUNTIME_GRAPH_CACHE.clear()
|
|
185
|
+
RUNTIME_GRAPH_INFLIGHT.clear()
|
|
186
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { putEvidence, getEvidence, getEvidenceSelected } from "./evidence-store.mjs"
|
|
2
|
+
|
|
3
|
+
function normalize(value) { return String(value ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n") }
|
|
4
|
+
function significantLines(text) {
|
|
5
|
+
const lines = normalize(text).split("\n")
|
|
6
|
+
const scored = lines.map((line, index) => {
|
|
7
|
+
const trimmed = line.trim(); let score = 0
|
|
8
|
+
if (/^#{1,6}\s/.test(trimmed)) score += 8
|
|
9
|
+
if (/^(?:[-*+] |\d+[.)]\s)/.test(trimmed)) score += 5
|
|
10
|
+
if (/\b(?:error|fail|warning|todo|fixme|pass|test|result|summary|important|must|required)\b/i.test(trimmed)) score += 4
|
|
11
|
+
if (/^(?:export|import|class|function|def|interface|type|CREATE|ALTER)\b/i.test(trimmed)) score += 3
|
|
12
|
+
if (trimmed.length > 0 && trimmed.length <= 180) score += 1
|
|
13
|
+
return { index, line, score }
|
|
14
|
+
})
|
|
15
|
+
return { lines, scored }
|
|
16
|
+
}
|
|
17
|
+
function summary(text, maxChars) {
|
|
18
|
+
const { lines, scored } = significantLines(text)
|
|
19
|
+
if (normalize(text).length <= maxChars) return normalize(text)
|
|
20
|
+
const wanted = new Set(scored.filter((row) => row.score > 0).sort((a,b) => b.score - a.score || a.index - b.index).slice(0,80).map((row) => row.index))
|
|
21
|
+
for (let i = Math.max(0, lines.length - 8); i < lines.length; i += 1) wanted.add(i)
|
|
22
|
+
let out = ""
|
|
23
|
+
for (const index of [...wanted].sort((a,b) => a-b)) {
|
|
24
|
+
const candidate = (out ? out + "\n" : "") + lines[index]
|
|
25
|
+
if (candidate.length > maxChars) break
|
|
26
|
+
out = candidate
|
|
27
|
+
}
|
|
28
|
+
return out || normalize(text).slice(0, maxChars)
|
|
29
|
+
}
|
|
30
|
+
export async function compactContext(root, content, options = {}) {
|
|
31
|
+
const raw = normalize(content)
|
|
32
|
+
const stored = await putEvidence(root, raw, { kind: options.kind || "context-block", source: options.source || "ues-context", summary: options.summary || "Reversible raw context block" })
|
|
33
|
+
return { schemaVersion: 1, ref: stored.ref, originalChars: raw.length, levels: { T1: summary(raw, Math.max(512, Number(options.t1Chars || 1600))), T2: summary(raw, Math.max(1200, Number(options.t2Chars || 5000))), T3: summary(raw, Math.max(2400, Number(options.t3Chars || 12000))) } }
|
|
34
|
+
}
|
|
35
|
+
export async function expandContext(root, ref, options = {}) {
|
|
36
|
+
return getEvidenceSelected(root, ref, { start: Math.max(0, Number(options.start || 0)), maxBytes: Math.max(1, Math.min(128_000, Number(options.maxBytes || 16_000))) })
|
|
37
|
+
}
|
|
38
|
+
export async function searchContext(root, ref, query, options = {}) {
|
|
39
|
+
const maxBytes = Math.max(16_000, Math.min(4 * 1024 * 1024, Number(options.maxBytes || 512_000)))
|
|
40
|
+
const evidence = await getEvidence(root, ref, { maxBytes })
|
|
41
|
+
const source = normalize(evidence?.content ?? evidence ?? "")
|
|
42
|
+
const terms = [...new Set(String(query || "").toLowerCase().split(/[^\p{L}\p{N}_$.-]+/u).filter((item) => item.length >= 2))].slice(0, 16)
|
|
43
|
+
const lines = source.split("\n"); const radius = Math.max(0, Math.min(12, Number(options.radius || 2))); const maxMatches = Math.max(1, Math.min(50, Number(options.maxMatches || 10))); const hits = []
|
|
44
|
+
for (let index = 0; index < lines.length && hits.length < maxMatches; index += 1) {
|
|
45
|
+
const lower = lines[index].toLowerCase(); const matched = terms.filter((term) => lower.includes(term)); if (!matched.length) continue
|
|
46
|
+
hits.push({ line: index + 1, terms: matched, excerpt: lines.slice(Math.max(0,index-radius), Math.min(lines.length,index+radius+1)).join("\n") })
|
|
47
|
+
}
|
|
48
|
+
return { schemaVersion: 1, ref, query: String(query || ""), terms, hits, truncatedSource: source.length >= maxBytes }
|
|
49
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import os from "node:os"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
|
|
5
|
+
function xdgConfigRoot() {
|
|
6
|
+
return process.env.XDG_CONFIG_HOME
|
|
7
|
+
? path.resolve(process.env.XDG_CONFIG_HOME)
|
|
8
|
+
: path.join(os.homedir(), ".config")
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getUesConfigDir() {
|
|
12
|
+
if (process.env.UES_CONFIG_DIR) return path.resolve(process.env.UES_CONFIG_DIR)
|
|
13
|
+
|
|
14
|
+
const nativeDir = path.join(xdgConfigRoot(), "ues")
|
|
15
|
+
const legacyDir = process.env.OPENCODE_CONFIG_DIR
|
|
16
|
+
? path.resolve(process.env.OPENCODE_CONFIG_DIR)
|
|
17
|
+
: path.join(xdgConfigRoot(), "opencode")
|
|
18
|
+
|
|
19
|
+
const nativePolicy = path.join(nativeDir, ".ues", "model-policy.json")
|
|
20
|
+
const legacyPolicy = path.join(legacyDir, ".ues", "model-policy.json")
|
|
21
|
+
|
|
22
|
+
if (existsSync(nativePolicy)) return nativeDir
|
|
23
|
+
if (existsSync(legacyPolicy)) return legacyDir
|
|
24
|
+
return nativeDir
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getLegacyOpenCodeConfigDir() {
|
|
28
|
+
return process.env.OPENCODE_CONFIG_DIR
|
|
29
|
+
? path.resolve(process.env.OPENCODE_CONFIG_DIR)
|
|
30
|
+
: path.join(xdgConfigRoot(), "opencode")
|
|
31
|
+
}
|
package/lib/safety.mjs
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const RULES = [
|
|
2
|
+
{ id: "git-force", pattern: /\bgit\s+(?:push\b[^\n]*(?:--force(?:-with-lease)?|(?:^|\s)-f(?:\s|$))|reset\s+--hard|clean\s+-[^\n]*f)/i },
|
|
3
|
+
{ id: "history-rewrite", pattern: /\bgit\s+(?:rebase\b|filter-branch\b|filter-repo\b)/i },
|
|
4
|
+
{ id: "publish", pattern: /\b(?:npm\s+publish|pnpm\s+publish|yarn\s+npm\s+publish)\b/i },
|
|
5
|
+
{ id: "destructive-files", pattern: /(?:^|[;&|]\s*)(?:rm\s+-[^\n]*r[^\n]*f|rmdir\s+\/s|del\s+\/s|remove-item\b[^\n]*-recurse[^\n]*-force)/i },
|
|
6
|
+
{ id: "database-drop", pattern: /\b(?:drop\s+(?:database|schema|table)|truncate\s+table)\b/i },
|
|
7
|
+
{ id: "deployment", pattern: /\b(?:kubectl\s+(?:delete|apply)|terraform\s+(?:apply|destroy)|helm\s+(?:install|upgrade|uninstall))\b/i },
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
export function shellCommandSegments(command) {
|
|
11
|
+
const value = String(command || "")
|
|
12
|
+
const segments = []
|
|
13
|
+
let current = ""
|
|
14
|
+
let quote = null
|
|
15
|
+
let escaped = false
|
|
16
|
+
let depth = 0
|
|
17
|
+
|
|
18
|
+
const push = (operator = null) => {
|
|
19
|
+
const text = current.trim()
|
|
20
|
+
if (text) segments.push({ text, operator })
|
|
21
|
+
current = ""
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
25
|
+
const char = value[index]
|
|
26
|
+
const next = value[index + 1] || ""
|
|
27
|
+
|
|
28
|
+
if (escaped) {
|
|
29
|
+
current += char
|
|
30
|
+
escaped = false
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
if (char === "\\" && quote !== "'") {
|
|
34
|
+
current += char
|
|
35
|
+
escaped = true
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
if (quote) {
|
|
39
|
+
current += char
|
|
40
|
+
if (char === quote) quote = null
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
if (char === "'" || char === '"') {
|
|
44
|
+
quote = char
|
|
45
|
+
current += char
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
if (char === "(" || char === "{" || char === "[") {
|
|
49
|
+
depth += 1
|
|
50
|
+
current += char
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
if (char === ")" || char === "}" || char === "]") {
|
|
54
|
+
depth = Math.max(0, depth - 1)
|
|
55
|
+
current += char
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (depth === 0 && char === "&" && next === "&") {
|
|
60
|
+
push("&&")
|
|
61
|
+
index += 1
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
if (depth === 0 && char === "|" && next === "|") {
|
|
65
|
+
push("||")
|
|
66
|
+
index += 1
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
if (depth === 0 && (char === ";" || char === "|")) {
|
|
70
|
+
push(char)
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
if (depth === 0 && char === "\n") {
|
|
74
|
+
push("\n")
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
current += char
|
|
78
|
+
}
|
|
79
|
+
push(null)
|
|
80
|
+
return segments
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function destructiveShellAnalysis(command) {
|
|
84
|
+
const value = String(command || "")
|
|
85
|
+
const segments = shellCommandSegments(value)
|
|
86
|
+
const findings = []
|
|
87
|
+
|
|
88
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
89
|
+
const segment = segments[index]
|
|
90
|
+
for (const rule of RULES) {
|
|
91
|
+
// Clone regex state defensively if future rules add the global flag.
|
|
92
|
+
rule.pattern.lastIndex = 0
|
|
93
|
+
if (!rule.pattern.test(segment.text)) continue
|
|
94
|
+
findings.push({
|
|
95
|
+
id: rule.id,
|
|
96
|
+
segmentIndex: index,
|
|
97
|
+
segment: segment.text,
|
|
98
|
+
operator: segment.operator,
|
|
99
|
+
})
|
|
100
|
+
break
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Retain a whole-command fallback so nested shell syntax that the bounded
|
|
105
|
+
// splitter intentionally does not fully parse still fails closed.
|
|
106
|
+
if (!findings.length) {
|
|
107
|
+
for (const rule of RULES) {
|
|
108
|
+
rule.pattern.lastIndex = 0
|
|
109
|
+
if (rule.pattern.test(value)) {
|
|
110
|
+
findings.push({ id: rule.id, segmentIndex: null, segment: value, operator: null })
|
|
111
|
+
break
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
risky: findings.length > 0,
|
|
118
|
+
id: findings[0]?.id || null,
|
|
119
|
+
findings,
|
|
120
|
+
segmentCount: segments.length,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function destructiveShellRisk(command) {
|
|
125
|
+
const analysis = destructiveShellAnalysis(command)
|
|
126
|
+
return {
|
|
127
|
+
risky: analysis.risky,
|
|
128
|
+
id: analysis.id,
|
|
129
|
+
segmentIndex: analysis.findings[0]?.segmentIndex ?? null,
|
|
130
|
+
segment: analysis.findings[0]?.segment || null,
|
|
131
|
+
}
|
|
132
|
+
}
|
package/lib/semantic-index.mjs
CHANGED
|
@@ -11,8 +11,8 @@ const SOURCE_EXTENSIONS = new Set([
|
|
|
11
11
|
])
|
|
12
12
|
const SKIP_DIRS = new Set([
|
|
13
13
|
".git", "node_modules", ".next", "dist", "build", "coverage", ".venv", "venv",
|
|
14
|
-
"Pods", "DerivedData", ".gradle", ".idea", ".cache", ".turbo", "target", "
|
|
15
|
-
".ues-work", ".ues-learning", ".ues-dashboard", ".ues-sandboxes", ".ues-cache", ".ues-traces",
|
|
14
|
+
"Pods", "DerivedData", ".gradle", ".idea", ".cache", ".turbo", "target", "obj",
|
|
15
|
+
".ues-work", ".ues-learning", ".ues-memory", ".ues-dashboard", ".ues-sandboxes", ".ues-cache", ".ues-traces",
|
|
16
16
|
])
|
|
17
17
|
const STOP_IDENTIFIERS = new Set([
|
|
18
18
|
"const","let","var","function","class","interface","type","export","import","from","return",
|
|
@@ -233,7 +233,7 @@ function queryTerms(query) {
|
|
|
233
233
|
export async function querySemanticIndex(root, query, options = {}) {
|
|
234
234
|
const terms = queryTerms(query)
|
|
235
235
|
if (!terms.length) return { query: String(query || ""), terms: [], results: [], stats: null }
|
|
236
|
-
const built = await buildSemanticIndex(root, options)
|
|
236
|
+
const built = options.builtIndex || await buildSemanticIndex(root, options)
|
|
237
237
|
const lowerTerms = terms.map((term) => term.toLowerCase())
|
|
238
238
|
const results = []
|
|
239
239
|
|
|
@@ -316,3 +316,52 @@ export async function semanticIndexStatus(root = process.cwd()) {
|
|
|
316
316
|
truncated: Boolean(cached?.truncated),
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
|
+
|
|
320
|
+
const RUNTIME_SEMANTIC_CACHE = new Map()
|
|
321
|
+
const RUNTIME_SEMANTIC_INFLIGHT = new Map()
|
|
322
|
+
|
|
323
|
+
export async function buildSemanticIndexCached(root = process.cwd(), options = {}) {
|
|
324
|
+
root = path.resolve(root)
|
|
325
|
+
const fingerprint = String(options.workspaceFingerprint || "")
|
|
326
|
+
if (!fingerprint || fingerprint === "unknown") {
|
|
327
|
+
return buildSemanticIndex(root, options)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const maxFiles = Number(options.maxFiles ?? 6000)
|
|
331
|
+
const maxDepth = Number(options.maxDepth ?? 14)
|
|
332
|
+
const maxBytes = Number(options.maxBytes ?? 768 * 1024)
|
|
333
|
+
const key = [root, fingerprint, maxFiles, maxDepth, maxBytes].join("\u0000")
|
|
334
|
+
if (RUNTIME_SEMANTIC_CACHE.has(key)) {
|
|
335
|
+
const value = RUNTIME_SEMANTIC_CACHE.get(key)
|
|
336
|
+
RUNTIME_SEMANTIC_CACHE.delete(key)
|
|
337
|
+
RUNTIME_SEMANTIC_CACHE.set(key, value)
|
|
338
|
+
return { ...value, runtimeCacheHit: true }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (RUNTIME_SEMANTIC_INFLIGHT.has(key)) {
|
|
342
|
+
const value = await RUNTIME_SEMANTIC_INFLIGHT.get(key)
|
|
343
|
+
return { ...value, runtimeCacheHit: true, runtimeCacheCoalesced: true }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const pending = buildSemanticIndex(root, options)
|
|
347
|
+
.then((built) => ({ ...built, runtimeCacheHit: false }))
|
|
348
|
+
RUNTIME_SEMANTIC_INFLIGHT.set(key, pending)
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
const value = await pending
|
|
352
|
+
RUNTIME_SEMANTIC_CACHE.set(key, value)
|
|
353
|
+
while (RUNTIME_SEMANTIC_CACHE.size > 6) {
|
|
354
|
+
const oldest = RUNTIME_SEMANTIC_CACHE.keys().next().value
|
|
355
|
+
if (!oldest) break
|
|
356
|
+
RUNTIME_SEMANTIC_CACHE.delete(oldest)
|
|
357
|
+
}
|
|
358
|
+
return value
|
|
359
|
+
} finally {
|
|
360
|
+
RUNTIME_SEMANTIC_INFLIGHT.delete(key)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function clearSemanticIndexRuntimeCache() {
|
|
365
|
+
RUNTIME_SEMANTIC_CACHE.clear()
|
|
366
|
+
RUNTIME_SEMANTIC_INFLIGHT.clear()
|
|
367
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { readFile } from "node:fs/promises"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { fileURLToPath } from "node:url"
|
|
5
|
+
|
|
6
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
|
|
7
|
+
const SKILLS_ROOT = path.join(PACKAGE_ROOT, "global-config", "skills")
|
|
8
|
+
const SKILL_SOURCE_CACHE = new Map()
|
|
9
|
+
const COMPILED_SKILL_CACHE = new Map()
|
|
10
|
+
|
|
11
|
+
const DOMAIN_SKILLS = Object.freeze({
|
|
12
|
+
"auth-security": "auth-security",
|
|
13
|
+
payment: "payment-engineering",
|
|
14
|
+
database: "database-engineering",
|
|
15
|
+
"api-contract": "api-contract",
|
|
16
|
+
"react-native": "react-native-engineering",
|
|
17
|
+
nextjs: "nextjs-engineering",
|
|
18
|
+
react: "react-engineering",
|
|
19
|
+
devops: "devops-engineering",
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const ROLE_SKILLS = Object.freeze({
|
|
23
|
+
architect: ["software-architect", "task-planner"],
|
|
24
|
+
"codebase-mapper": ["repo-explorer", "context-engineering"],
|
|
25
|
+
critic: ["code-review", "change-impact-analysis"],
|
|
26
|
+
debugger: ["bug-diagnosis", "test-verification"],
|
|
27
|
+
executor: ["implementation-engineer"],
|
|
28
|
+
"integration-verifier": ["test-verification", "change-impact-analysis"],
|
|
29
|
+
"merge-arbiter": ["git-safety", "change-impact-analysis"],
|
|
30
|
+
"plan-checker": ["task-planner", "change-impact-analysis"],
|
|
31
|
+
researcher: ["research-verification"],
|
|
32
|
+
reviewer: ["code-review"],
|
|
33
|
+
verifier: ["test-verification"],
|
|
34
|
+
"visual-verifier": ["visual-fidelity", "responsive-verification", "browser-qa"],
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
function stripFrontmatter(raw) {
|
|
38
|
+
return String(raw || "").replace(/^---\s*\r?\n[\s\S]*?\r?\n---\s*\r?\n/, "")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function priorityLine(line) {
|
|
42
|
+
return /^#{1,4}\s/.test(line) ||
|
|
43
|
+
/^[-*]\s+/.test(line) ||
|
|
44
|
+
/\b(must|never|always|verify|prefer|avoid|require|do not|don't|fail|evidence|security|safety|rollback|idempot|transaction|accessib)/i.test(line)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function compileText(raw, maxChars) {
|
|
48
|
+
const source = stripFrontmatter(raw)
|
|
49
|
+
const lines = source.split(/\r?\n/).map((line) => line.trimEnd())
|
|
50
|
+
const selected = []
|
|
51
|
+
let chars = 0
|
|
52
|
+
|
|
53
|
+
// Keep a short orientation prefix, then the highest-value rules/checklists.
|
|
54
|
+
for (const line of lines.slice(0, 20)) {
|
|
55
|
+
if (!line.trim()) continue
|
|
56
|
+
if (chars + line.length + 1 > Math.floor(maxChars * 0.35)) break
|
|
57
|
+
selected.push(line)
|
|
58
|
+
chars += line.length + 1
|
|
59
|
+
}
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
if (!line.trim() || !priorityLine(line) || selected.includes(line)) continue
|
|
62
|
+
if (chars + line.length + 1 > maxChars) break
|
|
63
|
+
selected.push(line)
|
|
64
|
+
chars += line.length + 1
|
|
65
|
+
}
|
|
66
|
+
return selected.join("\n").slice(0, maxChars)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function selectSkillNames(taskPolicy = {}, role = "executor", options = {}) {
|
|
70
|
+
const maxSkills = Math.max(1, Math.min(5, Number(options.maxSkills || taskPolicy.maxSkills || 3)))
|
|
71
|
+
const names = []
|
|
72
|
+
for (const name of ROLE_SKILLS[role] || []) names.push(name)
|
|
73
|
+
for (const domain of taskPolicy.domains || []) {
|
|
74
|
+
const mapped = DOMAIN_SKILLS[domain]
|
|
75
|
+
if (mapped) names.push(mapped)
|
|
76
|
+
}
|
|
77
|
+
return [...new Set(names)].slice(0, maxSkills)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function skillSource(name) {
|
|
81
|
+
if (SKILL_SOURCE_CACHE.has(name)) return SKILL_SOURCE_CACHE.get(name)
|
|
82
|
+
const file = path.join(SKILLS_ROOT, name, "SKILL.md")
|
|
83
|
+
if (!existsSync(file)) {
|
|
84
|
+
SKILL_SOURCE_CACHE.set(name, "")
|
|
85
|
+
return ""
|
|
86
|
+
}
|
|
87
|
+
const raw = await readFile(file, "utf8").catch(() => "")
|
|
88
|
+
SKILL_SOURCE_CACHE.set(name, raw)
|
|
89
|
+
return raw
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function compileSkillContext(taskPolicy = {}, role = "executor", options = {}) {
|
|
93
|
+
const names = selectSkillNames(taskPolicy, role, options)
|
|
94
|
+
const totalChars = Math.max(800, Math.min(8_000, Number(options.totalChars || 3_200)))
|
|
95
|
+
const perSkill = Math.max(450, Math.floor(totalChars / Math.max(1, names.length)))
|
|
96
|
+
const cacheKey = JSON.stringify([role, names, totalChars, perSkill])
|
|
97
|
+
if (COMPILED_SKILL_CACHE.has(cacheKey)) {
|
|
98
|
+
return { ...COMPILED_SKILL_CACHE.get(cacheKey), cacheHit: true }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const skills = []
|
|
102
|
+
for (const name of names) {
|
|
103
|
+
const raw = await skillSource(name)
|
|
104
|
+
if (!raw) continue
|
|
105
|
+
const text = compileText(raw, perSkill)
|
|
106
|
+
if (text) skills.push({ name, text })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const result = {
|
|
110
|
+
schemaVersion: 1,
|
|
111
|
+
role,
|
|
112
|
+
requested: names,
|
|
113
|
+
loaded: skills.map((item) => item.name),
|
|
114
|
+
chars: skills.reduce((sum, item) => sum + item.text.length, 0),
|
|
115
|
+
text: skills
|
|
116
|
+
.map((item) => `### Skill: ${item.name}\n${item.text}`)
|
|
117
|
+
.join("\n\n")
|
|
118
|
+
.slice(0, totalChars),
|
|
119
|
+
cacheHit: false,
|
|
120
|
+
}
|
|
121
|
+
COMPILED_SKILL_CACHE.set(cacheKey, result)
|
|
122
|
+
return result
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function clearSkillCompilerCache() {
|
|
126
|
+
SKILL_SOURCE_CACHE.clear()
|
|
127
|
+
COMPILED_SKILL_CACHE.clear()
|
|
128
|
+
}
|
package/lib/skill-quality.mjs
CHANGED
|
@@ -15,9 +15,53 @@ function similarity(a, b) {
|
|
|
15
15
|
return union.size ? intersection / union.size : 0
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export function validateFrontmatterSource(source, options = {}) {
|
|
19
|
+
const text = String(source || "")
|
|
20
|
+
const errors = []
|
|
21
|
+
if (!text.startsWith("---\n") && !text.startsWith("---\r\n")) {
|
|
22
|
+
return { valid: false, errors: [{ issue: "missing-frontmatter-open", file: options.file || null }] }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const match = text.match(/^---\s*\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/)
|
|
26
|
+
if (!match) {
|
|
27
|
+
return { valid: false, errors: [{ issue: "missing-frontmatter-close", file: options.file || null }] }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const lines = match[1].split(/\r?\n/)
|
|
31
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
32
|
+
const line = lines[index]
|
|
33
|
+
if (!line.trim() || /^\s/.test(line) || line.trimStart().startsWith("#")) continue
|
|
34
|
+
if (/\t/.test(line)) {
|
|
35
|
+
errors.push({ issue: "frontmatter-tab", line: index + 2, file: options.file || null })
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
const field = line.match(/^([A-Za-z0-9_-]+):(?:\s*(.*))?$/)
|
|
39
|
+
if (!field) {
|
|
40
|
+
errors.push({ issue: "invalid-frontmatter-line", line: index + 2, file: options.file || null })
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
const value = String(field[2] || "").trim()
|
|
44
|
+
if (!value) continue
|
|
45
|
+
const quoted = (value.startsWith('"') && value.endsWith('"')) ||
|
|
46
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
47
|
+
const blockScalar = value === "|" || value === ">"
|
|
48
|
+
const flowValue = value.startsWith("[") || value.startsWith("{")
|
|
49
|
+
if (!quoted && !blockScalar && !flowValue && /:\s/.test(value)) {
|
|
50
|
+
errors.push({
|
|
51
|
+
issue: "unquoted-colon-in-scalar",
|
|
52
|
+
field: field[1],
|
|
53
|
+
line: index + 2,
|
|
54
|
+
file: options.file || null,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { valid: errors.length === 0, errors }
|
|
60
|
+
}
|
|
61
|
+
|
|
18
62
|
function frontmatter(source) {
|
|
19
|
-
const name = source.match(/^name:\s*([^\r\n]+)/m)?.[1]?.trim() || null
|
|
20
|
-
const description = source.match(/^description:\s*([^\r\n]+)/m)?.[1]?.trim() || null
|
|
63
|
+
const name = source.match(/^name:\s*([^\r\n]+)/m)?.[1]?.trim()?.replace(/^(["'])(.*)\1$/, "$2") || null
|
|
64
|
+
const description = source.match(/^description:\s*([^\r\n]+)/m)?.[1]?.trim()?.replace(/^(["'])(.*)\1$/, "$2") || null
|
|
21
65
|
return { name, description }
|
|
22
66
|
}
|
|
23
67
|
|
|
@@ -36,6 +80,8 @@ export async function lintSkillCatalog(root = process.cwd(), options = {}) {
|
|
|
36
80
|
continue
|
|
37
81
|
}
|
|
38
82
|
const source = await readFile(file, "utf8")
|
|
83
|
+
const yaml = validateFrontmatterSource(source, { file })
|
|
84
|
+
for (const error of yaml.errors) errors.push({ skill: entry.name, ...error })
|
|
39
85
|
const meta = frontmatter(source)
|
|
40
86
|
const body = source.replace(/^---[\s\S]*?---\s*/m, "")
|
|
41
87
|
const row = {
|