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
@@ -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
- return typeof value === "string" ? value : JSON.stringify(canonical(value))
11
- }
12
-
13
- function digest(value) {
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
- function chars(value) {
18
- return stringify(value ?? "").length
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
  }
@@ -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", "bin", "obj",
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
+ }
@@ -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", "bin", "obj",
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
+ }
@@ -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 = {