opencode-agent-skill 7.7.0 → 9.0.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/CHANGELOG.md +63 -3
- package/README.md +675 -581
- package/bin/ocskill.mjs +358 -156
- package/docs/DETERMINISTIC-TOOLS.md +25 -8
- package/docs/ENGINEERING-DESIGN.md +31 -13
- package/docs/EVALS.md +34 -12
- package/docs/NPM-PUBLISH.md +6 -6
- package/docs/OPENCODE-COMPAT.md +11 -8
- package/docs/TRACE-SCHEMA.md +15 -2
- package/docs/V8-INTELLIGENCE-RELIABILITY.md +206 -0
- package/docs/V9-SPEED-INTELLIGENCE.md +102 -0
- package/evals/live/tasks.json +6 -6
- package/evals/polyglot/fixtures/polyglot-bench/api/generated/client.ts +2 -0
- package/evals/polyglot/fixtures/polyglot-bench/api/openapi.json +25 -0
- package/evals/polyglot/fixtures/polyglot-bench/db/migrations/20260920_add_order_key.sql +1 -0
- package/evals/polyglot/fixtures/polyglot-bench/dotnet/OrderService.cs +8 -0
- package/evals/polyglot/fixtures/polyglot-bench/java/PriceService.java +5 -0
- package/evals/polyglot/fixtures/polyglot-bench/monorepo/package.json +6 -0
- package/evals/polyglot/fixtures/polyglot-bench/monorepo/packages/api/package.json +4 -0
- package/evals/polyglot/fixtures/polyglot-bench/monorepo/packages/web/package.json +7 -0
- package/evals/polyglot/fixtures/polyglot-bench/monorepo/pnpm-lock.yaml +5 -0
- package/evals/polyglot/fixtures/polyglot-bench/next/app/api/products/route.ts +7 -0
- package/evals/polyglot/fixtures/polyglot-bench/python/tenant_auth.py +4 -0
- package/evals/polyglot/fixtures/polyglot-bench/react-native/keyboard.ts +3 -0
- package/evals/polyglot/graders/polyglot-bench.mjs +101 -0
- package/evals/polyglot/tasks.json +54 -0
- package/global-config/AGENTS.md +10 -7
- package/global-config/agents/integration-verifier.md +1 -1
- package/global-config/agents/plan-checker.md +1 -1
- package/global-config/commands/run.md +9 -5
- package/global-config/plugins/ues-router/capabilities.js +4 -0
- package/global-config/plugins/ues-router/index.js +414 -21
- package/global-config/plugins/ues-router/router.js +140 -23
- package/global-config/skills/engineering-orchestrator/references/long-horizon.md +6 -4
- package/lib/aci.mjs +128 -0
- package/lib/benchmark-confidence.mjs +135 -0
- package/lib/cli-utils.mjs +41 -0
- package/lib/container-sandbox.mjs +102 -0
- package/lib/context-manifest.mjs +300 -22
- package/lib/control-center.mjs +36 -3
- package/lib/eval-order.mjs +9 -0
- package/lib/eval-telemetry.mjs +5 -2
- package/lib/gate-receipt.mjs +52 -0
- package/lib/installer.mjs +39 -25
- package/lib/learning-engine.mjs +236 -38
- package/lib/opencode-compat.mjs +25 -10
- package/lib/orchestrator-policy.mjs +101 -20
- package/lib/process-runner.mjs +30 -9
- package/lib/runtime-events.mjs +31 -0
- package/lib/semantic-index.mjs +318 -0
- package/lib/task-engine.mjs +416 -24
- package/lib/trajectory.mjs +89 -0
- package/lib/windows-shim.mjs +227 -0
- package/lib/worktree-sandbox.mjs +85 -3
- package/package.json +10 -4
- package/scripts/check-release-tag.mjs +22 -0
- package/scripts/control-center.mjs +25 -0
- package/scripts/eval-live.mjs +39 -58
- package/scripts/eval-matrix.mjs +155 -0
- package/scripts/smoke-packed-install.mjs +138 -4
- package/scripts/smoke-plain-install.mjs +91 -0
- package/scripts/validate-live-suite.mjs +3 -3
- package/scripts/validate.mjs +27 -5
package/lib/context-manifest.mjs
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { existsSync } from "node:fs"
|
|
2
2
|
import { readFile, stat } from "node:fs/promises"
|
|
3
|
+
import { spawnSync } from "node:child_process"
|
|
3
4
|
import path from "node:path"
|
|
4
5
|
import { buildRepoGraph } from "./repo-graph.mjs"
|
|
5
6
|
import { taskFiles } from "./task-graph.mjs"
|
|
7
|
+
import { querySemanticIndex } from "./semantic-index.mjs"
|
|
6
8
|
|
|
7
|
-
const
|
|
9
|
+
const ROOT_INSTRUCTIONS = [
|
|
10
|
+
"AGENTS.md",
|
|
11
|
+
"package.json",
|
|
12
|
+
"pyproject.toml",
|
|
13
|
+
"Cargo.toml",
|
|
14
|
+
"go.mod",
|
|
15
|
+
"pom.xml",
|
|
16
|
+
"build.gradle",
|
|
17
|
+
"build.gradle.kts",
|
|
18
|
+
]
|
|
19
|
+
const SOURCE_EXT = /\.(?:[cm]?[jt]sx?|py|rb|php|java|kt|kts|cs|go|rs|swift|dart|vue|svelte|sql)$/i
|
|
8
20
|
|
|
9
21
|
function rel(root, file) {
|
|
10
22
|
return path.relative(root, file).replaceAll("\\", "/")
|
|
@@ -14,31 +26,248 @@ function unique(values) {
|
|
|
14
26
|
return [...new Set(values.filter(Boolean))]
|
|
15
27
|
}
|
|
16
28
|
|
|
29
|
+
function safeFull(root, relative) {
|
|
30
|
+
const base = path.resolve(root)
|
|
31
|
+
const full = path.resolve(base, relative)
|
|
32
|
+
if (full !== base && !full.startsWith(base + path.sep)) return null
|
|
33
|
+
return full
|
|
34
|
+
}
|
|
35
|
+
|
|
17
36
|
async function fileInfo(root, relative) {
|
|
18
|
-
const full =
|
|
19
|
-
if (!full
|
|
37
|
+
const full = safeFull(root, relative)
|
|
38
|
+
if (!full) return null
|
|
20
39
|
const info = await stat(full).catch(() => null)
|
|
21
40
|
if (!info?.isFile()) return null
|
|
22
41
|
return { path: rel(root, full), bytes: info.size }
|
|
23
42
|
}
|
|
24
43
|
|
|
25
|
-
|
|
26
|
-
|
|
44
|
+
function centeredSlice(source, terms, limit) {
|
|
45
|
+
if (source.length <= limit) return { text: source, startOffset: 0, centerTerm: null }
|
|
46
|
+
const lower = source.toLowerCase()
|
|
47
|
+
let matchIndex = -1
|
|
48
|
+
let centerTerm = null
|
|
49
|
+
for (const term of terms || []) {
|
|
50
|
+
const index = lower.indexOf(term.toLowerCase())
|
|
51
|
+
if (index >= 0 && (matchIndex < 0 || index < matchIndex)) {
|
|
52
|
+
matchIndex = index
|
|
53
|
+
centerTerm = term
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (matchIndex < 0) return { text: source.slice(0, limit) + "\n...[truncated]", startOffset: 0, centerTerm: null }
|
|
57
|
+
const start = Math.max(0, Math.min(matchIndex - Math.floor(limit / 3), source.length - limit))
|
|
58
|
+
const prefix = start > 0 ? "...[truncated]\n" : ""
|
|
59
|
+
const suffix = start + limit < source.length ? "\n...[truncated]" : ""
|
|
60
|
+
return {
|
|
61
|
+
text: prefix + source.slice(start, start + limit) + suffix,
|
|
62
|
+
startOffset: start,
|
|
63
|
+
centerTerm,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function excerpt(root, relative, limit, terms = []) {
|
|
68
|
+
const full = safeFull(root, relative)
|
|
69
|
+
if (!full) return null
|
|
27
70
|
const source = await readFile(full, "utf8").catch(() => "")
|
|
28
71
|
if (!source) return null
|
|
72
|
+
const slice = centeredSlice(source, terms, limit)
|
|
29
73
|
return {
|
|
30
74
|
path: rel(root, full),
|
|
31
|
-
|
|
75
|
+
...slice,
|
|
32
76
|
}
|
|
33
77
|
}
|
|
34
78
|
|
|
79
|
+
function taskTerms(task) {
|
|
80
|
+
const source = [
|
|
81
|
+
task?.title,
|
|
82
|
+
task?.summary,
|
|
83
|
+
...(task?.acceptance || []),
|
|
84
|
+
...(task?.verification || []),
|
|
85
|
+
].filter(Boolean).join(" ").toLowerCase()
|
|
86
|
+
|
|
87
|
+
const stop = new Set([
|
|
88
|
+
"this","that","with","from","into","then","than","when","where","what","your",
|
|
89
|
+
"task","test","tests","file","files","code","change","changes","should","must",
|
|
90
|
+
"được","các","cho","với","trong","này","một","những","không","theo","sau",
|
|
91
|
+
])
|
|
92
|
+
return unique(
|
|
93
|
+
source
|
|
94
|
+
.split(/[^\p{L}\p{N}_$.-]+/u)
|
|
95
|
+
.map((item) => item.replace(/^[-.$]+|[-.$]+$/g, ""))
|
|
96
|
+
.filter((item) => item.length >= 4 && !stop.has(item)),
|
|
97
|
+
).slice(0, 24)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function tokenCounts(text, wantedTerms) {
|
|
101
|
+
const counts = new Map()
|
|
102
|
+
const wanted = new Set(wantedTerms)
|
|
103
|
+
const tokens = String(text || "")
|
|
104
|
+
.toLowerCase()
|
|
105
|
+
.split(/[^\p{L}\p{N}_$.-]+/u)
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
for (const token of tokens) {
|
|
108
|
+
if (!wanted.has(token)) continue
|
|
109
|
+
counts.set(token, (counts.get(token) || 0) + 1)
|
|
110
|
+
}
|
|
111
|
+
return { counts, length: tokens.length }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function symbolHits(source, terms, limit = 12) {
|
|
115
|
+
const hits = []
|
|
116
|
+
const lines = String(source || "").split(/\r?\n/)
|
|
117
|
+
const patterns = [
|
|
118
|
+
/(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/,
|
|
119
|
+
/(?:export\s+)?class\s+([A-Za-z_$][\w$]*)/,
|
|
120
|
+
/(?:export\s+)?interface\s+([A-Za-z_$][\w$]*)/,
|
|
121
|
+
/(?:export\s+)?type\s+([A-Za-z_$][\w$]*)/,
|
|
122
|
+
/(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)/,
|
|
123
|
+
/^\s*(?:async\s+)?def\s+([A-Za-z_][\w]*)/,
|
|
124
|
+
/^\s*class\s+([A-Za-z_][\w]*)/,
|
|
125
|
+
/^\s*(?:public|private|protected|internal|static|final|virtual|override|async|synchronized|abstract|sealed|partial|readonly|\s)+[\w<>,?\[\].:]+\s+([A-Za-z_$][\w$]*)\s*\(/,
|
|
126
|
+
]
|
|
127
|
+
for (let index = 0; index < lines.length && hits.length < limit; index += 1) {
|
|
128
|
+
const line = lines[index]
|
|
129
|
+
let symbol = null
|
|
130
|
+
for (const pattern of patterns) {
|
|
131
|
+
const match = line.match(pattern)
|
|
132
|
+
if (match?.[1]) {
|
|
133
|
+
symbol = match[1]
|
|
134
|
+
break
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!symbol) continue
|
|
138
|
+
const haystack = (symbol + " " + line).toLowerCase()
|
|
139
|
+
const matches = terms.filter((term) => haystack.includes(term.toLowerCase()))
|
|
140
|
+
if (!matches.length) continue
|
|
141
|
+
hits.push({ symbol, line: index + 1, matches: unique(matches).slice(0, 6), preview: line.trim().slice(0, 240) })
|
|
142
|
+
}
|
|
143
|
+
return hits
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function gitChangedFiles(root) {
|
|
147
|
+
const result = spawnSync(
|
|
148
|
+
"git",
|
|
149
|
+
["status", "--porcelain=v1", "--untracked-files=all"],
|
|
150
|
+
{ cwd: root, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 },
|
|
151
|
+
)
|
|
152
|
+
if (result.status !== 0) return []
|
|
153
|
+
return unique(
|
|
154
|
+
result.stdout
|
|
155
|
+
.split(/\r?\n/)
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.map((line) => line.slice(3).trim())
|
|
158
|
+
.map((value) => value.includes(" -> ") ? value.split(" -> ").at(-1) : value)
|
|
159
|
+
.map((value) => value.replaceAll("\\", "/")),
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function nearbyInstructions(root, declared) {
|
|
164
|
+
const found = new Set()
|
|
165
|
+
for (const name of ROOT_INSTRUCTIONS) {
|
|
166
|
+
if (existsSync(path.join(root, name))) found.add(name)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const file of declared) {
|
|
170
|
+
let dir = path.dirname(file)
|
|
171
|
+
while (dir && dir !== ".") {
|
|
172
|
+
for (const name of ["AGENTS.md", "package.json", "pyproject.toml", "Cargo.toml", "go.mod"]) {
|
|
173
|
+
const candidate = path.posix.join(dir.replaceAll("\\", "/"), name)
|
|
174
|
+
if (existsSync(path.join(root, candidate))) found.add(candidate)
|
|
175
|
+
}
|
|
176
|
+
const next = path.dirname(dir)
|
|
177
|
+
if (next === dir) break
|
|
178
|
+
dir = next
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return [...found]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function rankedReferences(root, nodes, terms, declared, changed, limit = 24) {
|
|
185
|
+
if (!terms.length) return []
|
|
186
|
+
const declaredSet = new Set(declared)
|
|
187
|
+
const changedSet = new Set(changed)
|
|
188
|
+
const candidates = nodes
|
|
189
|
+
.map((node) => node.path)
|
|
190
|
+
.filter((file) => SOURCE_EXT.test(file))
|
|
191
|
+
.filter((file) => !declaredSet.has(file))
|
|
192
|
+
.slice(0, 500)
|
|
193
|
+
|
|
194
|
+
const docs = []
|
|
195
|
+
const documentFrequency = new Map(terms.map((term) => [term, 0]))
|
|
196
|
+
for (const file of candidates) {
|
|
197
|
+
const full = safeFull(root, file)
|
|
198
|
+
const info = full ? await stat(full).catch(() => null) : null
|
|
199
|
+
if (!info?.isFile() || info.size > 512 * 1024) continue
|
|
200
|
+
const source = await readFile(full, "utf8").catch(() => "")
|
|
201
|
+
if (!source) continue
|
|
202
|
+
const stats = tokenCounts(source, terms)
|
|
203
|
+
for (const term of terms) {
|
|
204
|
+
if ((stats.counts.get(term) || 0) > 0) {
|
|
205
|
+
documentFrequency.set(term, (documentFrequency.get(term) || 0) + 1)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
docs.push({ file, source, stats })
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const population = Math.max(1, docs.length)
|
|
212
|
+
const scored = []
|
|
213
|
+
for (const doc of docs) {
|
|
214
|
+
const lowerPath = doc.file.toLowerCase()
|
|
215
|
+
let score = changedSet.has(doc.file) ? 8 : 0
|
|
216
|
+
const matches = []
|
|
217
|
+
for (const term of terms) {
|
|
218
|
+
if (lowerPath.includes(term)) score += 6
|
|
219
|
+
const tf = doc.stats.counts.get(term) || 0
|
|
220
|
+
if (tf > 0) {
|
|
221
|
+
const df = documentFrequency.get(term) || 0
|
|
222
|
+
const idf = Math.log((population + 1) / (df + 1)) + 1
|
|
223
|
+
score += (1 + Math.log(tf)) * idf * 3
|
|
224
|
+
if (matches.length < 8) matches.push(term)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const symbols = symbolHits(doc.source, terms)
|
|
228
|
+
score += symbols.length * 4
|
|
229
|
+
if (score > 0) {
|
|
230
|
+
scored.push({
|
|
231
|
+
path: doc.file,
|
|
232
|
+
score: Number(score.toFixed(3)),
|
|
233
|
+
matches,
|
|
234
|
+
symbolHits: symbols,
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return scored
|
|
240
|
+
.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
|
|
241
|
+
.slice(0, limit)
|
|
242
|
+
}
|
|
243
|
+
|
|
35
244
|
export async function buildContextManifest(root, task, options = {}) {
|
|
36
245
|
root = path.resolve(root)
|
|
37
246
|
const budget = Math.max(4_000, Number(options.budget ?? 24_000))
|
|
38
247
|
const declared = taskFiles(task)
|
|
39
|
-
const
|
|
248
|
+
const terms = taskTerms(task)
|
|
249
|
+
const changed = gitChangedFiles(root)
|
|
250
|
+
const strategy = String(options.strategy || "semantic+graph+git")
|
|
251
|
+
const queryText = [
|
|
252
|
+
task?.title,
|
|
253
|
+
task?.summary,
|
|
254
|
+
...(task?.acceptance || []),
|
|
255
|
+
...(task?.verification || []),
|
|
256
|
+
].filter(Boolean).join(" ")
|
|
257
|
+
|
|
258
|
+
const semantic = await querySemanticIndex(root, queryText, {
|
|
259
|
+
limit: 32,
|
|
260
|
+
maxFiles: options.semanticMaxFiles ?? 6000,
|
|
261
|
+
}).catch(() => null)
|
|
262
|
+
const semanticPaths = semantic?.results?.map((item) => item.path) || []
|
|
263
|
+
|
|
264
|
+
const useGraph = strategy.includes("graph")
|
|
265
|
+
const graph = useGraph
|
|
266
|
+
? await buildRepoGraph(root, { maxFiles: options.maxFiles ?? 2500 })
|
|
267
|
+
: { nodes: [], edges: [], hotspots: [], scannedFiles: 0, truncated: false }
|
|
40
268
|
const nodeMap = new Map(graph.nodes.map((node) => [node.path, node]))
|
|
41
269
|
const incoming = new Map()
|
|
270
|
+
|
|
42
271
|
for (const edge of graph.edges) {
|
|
43
272
|
const list = incoming.get(edge.to) || []
|
|
44
273
|
list.push(edge.from)
|
|
@@ -52,45 +281,93 @@ export async function buildContextManifest(root, task, options = {}) {
|
|
|
52
281
|
}
|
|
53
282
|
|
|
54
283
|
const baseNames = declared.map((file) => path.basename(file, path.extname(file)).toLowerCase())
|
|
55
|
-
const
|
|
56
|
-
|
|
284
|
+
const graphTestCandidates = graph.nodes.map((node) => node.path)
|
|
285
|
+
const semanticTestCandidates = semanticPaths
|
|
286
|
+
const tests = unique([...graphTestCandidates, ...semanticTestCandidates])
|
|
57
287
|
.filter((file) => /(^|\/)(test|tests|__tests__|spec)(\/|$)|\.(test|spec)\./i.test(file))
|
|
58
|
-
.
|
|
59
|
-
|
|
288
|
+
.map((file) => ({
|
|
289
|
+
file,
|
|
290
|
+
score: baseNames.reduce((sum, name) => sum + (name && file.toLowerCase().includes(name) ? 2 : 0), 0) +
|
|
291
|
+
terms.reduce((sum, term) => sum + (file.toLowerCase().includes(term) ? 1 : 0), 0),
|
|
292
|
+
}))
|
|
293
|
+
.filter((item) => item.score > 0 || semanticPaths.includes(item.file))
|
|
294
|
+
.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file))
|
|
295
|
+
.slice(0, 24)
|
|
296
|
+
.map((item) => item.file)
|
|
60
297
|
|
|
61
|
-
const instructions =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
298
|
+
const instructions = await nearbyInstructions(root, declared)
|
|
299
|
+
const ranked = semantic?.results?.length
|
|
300
|
+
? semantic.results.map((item) => ({
|
|
301
|
+
path: item.path,
|
|
302
|
+
score: item.score,
|
|
303
|
+
matches: item.reasons || [],
|
|
304
|
+
symbolHits: item.definitions || [],
|
|
305
|
+
evidenceLevel: semantic.evidenceLevel,
|
|
306
|
+
}))
|
|
307
|
+
: useGraph
|
|
308
|
+
? await rankedReferences(root, graph.nodes, terms, declared, changed)
|
|
309
|
+
: []
|
|
310
|
+
const changedRelevant = changed
|
|
311
|
+
.filter((file) =>
|
|
312
|
+
declared.includes(file) ||
|
|
313
|
+
semanticPaths.includes(file) ||
|
|
314
|
+
graph.nodes.some((node) => node.path === file),
|
|
315
|
+
)
|
|
316
|
+
.slice(0, 30)
|
|
317
|
+
|
|
318
|
+
const priority = unique([
|
|
319
|
+
...declared,
|
|
320
|
+
...tests,
|
|
321
|
+
...related.slice(0, 20),
|
|
322
|
+
...ranked.map((item) => item.path),
|
|
323
|
+
...changedRelevant,
|
|
324
|
+
...instructions,
|
|
325
|
+
])
|
|
65
326
|
|
|
66
|
-
const candidates = unique([...declared, ...related.slice(0, 30), ...tests, ...instructions])
|
|
67
327
|
const files = []
|
|
68
|
-
for (const file of
|
|
328
|
+
for (const file of priority) {
|
|
69
329
|
const info = await fileInfo(root, file)
|
|
70
330
|
if (info) files.push(info)
|
|
71
331
|
}
|
|
72
332
|
|
|
73
333
|
let remaining = budget
|
|
74
334
|
const excerpts = []
|
|
75
|
-
for (const file of
|
|
335
|
+
for (const file of priority) {
|
|
76
336
|
if (remaining <= 0) break
|
|
77
|
-
const
|
|
78
|
-
const
|
|
337
|
+
const isDeclared = declared.includes(file)
|
|
338
|
+
const isTest = tests.includes(file)
|
|
339
|
+
const isInstruction = instructions.includes(file)
|
|
340
|
+
const desired = isDeclared ? 6_000 : isTest ? 4_000 : isInstruction ? 3_000 : 2_500
|
|
341
|
+
const perFile = Math.min(desired, remaining)
|
|
342
|
+
const item = await excerpt(root, file, perFile, terms)
|
|
79
343
|
if (!item) continue
|
|
80
344
|
remaining -= item.text.length
|
|
81
|
-
excerpts.push(
|
|
345
|
+
excerpts.push({
|
|
346
|
+
...item,
|
|
347
|
+
role: isDeclared ? "declared" : isTest ? "test" : isInstruction ? "instruction" : "reference",
|
|
348
|
+
})
|
|
82
349
|
}
|
|
83
350
|
|
|
84
351
|
return {
|
|
85
|
-
schemaVersion:
|
|
352
|
+
schemaVersion: 4,
|
|
86
353
|
task: task?.id || null,
|
|
354
|
+
strategy,
|
|
355
|
+
queryTerms: terms,
|
|
87
356
|
declared,
|
|
88
357
|
related: unique(related).filter((file) => !declared.includes(file)).slice(0, 30),
|
|
89
358
|
tests,
|
|
90
359
|
instructions,
|
|
360
|
+
changed: changedRelevant,
|
|
361
|
+
rankedReferences: ranked,
|
|
91
362
|
files,
|
|
92
363
|
excerpts,
|
|
364
|
+
semantic: semantic ? {
|
|
365
|
+
evidenceLevel: semantic.evidenceLevel,
|
|
366
|
+
resultCount: semantic.results.length,
|
|
367
|
+
stats: semantic.stats,
|
|
368
|
+
} : null,
|
|
93
369
|
graph: {
|
|
370
|
+
enabled: useGraph,
|
|
94
371
|
scannedFiles: graph.scannedFiles,
|
|
95
372
|
truncated: graph.truncated,
|
|
96
373
|
hotspots: graph.hotspots.slice(0, 12),
|
|
@@ -99,3 +376,4 @@ export async function buildContextManifest(root, task, options = {}) {
|
|
|
99
376
|
used: budget - remaining,
|
|
100
377
|
}
|
|
101
378
|
}
|
|
379
|
+
|
package/lib/control-center.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs"
|
|
|
2
2
|
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"
|
|
3
3
|
import path from "node:path"
|
|
4
4
|
import { readLearningState } from "./learning-engine.mjs"
|
|
5
|
+
import { readRuntimeEvents } from "./runtime-events.mjs"
|
|
5
6
|
|
|
6
7
|
function escapeHtml(value) {
|
|
7
8
|
return String(value ?? "")
|
|
@@ -22,10 +23,11 @@ async function collectWork(root) {
|
|
|
22
23
|
for (const entry of entries) {
|
|
23
24
|
if (!entry.isDirectory()) continue
|
|
24
25
|
const dir = path.join(workRoot, entry.name)
|
|
25
|
-
const [state, plan, evidence] = await Promise.all([
|
|
26
|
+
const [state, plan, evidence, events] = await Promise.all([
|
|
26
27
|
readJson(path.join(dir, "STATE.json"), {}),
|
|
27
28
|
readJson(path.join(dir, "PLAN.json"), {}),
|
|
28
29
|
readJson(path.join(dir, "EVIDENCE.json"), {}),
|
|
30
|
+
readRuntimeEvents(path.join(dir, "EVENTS.jsonl"), { limit: 30 }),
|
|
29
31
|
])
|
|
30
32
|
items.push({
|
|
31
33
|
slug: entry.name,
|
|
@@ -39,7 +41,8 @@ async function collectWork(root) {
|
|
|
39
41
|
risk: task.risk || "medium",
|
|
40
42
|
state: state.tasks?.[task.id] || {},
|
|
41
43
|
})),
|
|
42
|
-
receipts:
|
|
44
|
+
receipts: evidence.receipts || [],
|
|
45
|
+
recentEvents: events,
|
|
43
46
|
updatedAt: state.updatedAt || null,
|
|
44
47
|
})
|
|
45
48
|
}
|
|
@@ -92,12 +95,15 @@ h1{margin:0;font-size:30px}.muted{color:#9da9bd}.grid{display:grid;grid-template
|
|
|
92
95
|
.task{display:grid;grid-template-columns:1fr auto;gap:8px;border-top:1px solid #243049;padding:9px 0}.ok{color:#7ee787}.warn{color:#f2cc60}.bad{color:#ff7b72}
|
|
93
96
|
table{width:100%;border-collapse:collapse;margin-top:10px}th,td{text-align:left;padding:8px;border-bottom:1px solid #26324a;font-size:13px}
|
|
94
97
|
pre{white-space:pre-wrap;word-break:break-word;background:#0a0f1c;padding:10px;border-radius:10px}.section{margin-top:28px}
|
|
98
|
+
button{margin-top:10px;border:1px solid #3b4966;border-radius:8px;background:#18233a;color:#e7ecf5;padding:7px 10px;cursor:pointer}button:disabled{opacity:.55;cursor:wait}
|
|
99
|
+
details{margin-top:10px;border-top:1px solid #243049;padding-top:8px}summary{cursor:pointer;color:#c9d4e7}
|
|
95
100
|
</style>
|
|
96
101
|
</head>
|
|
97
102
|
<body><div class="shell">
|
|
98
103
|
<div class="top"><div><h1>UES Control Center</h1><div class="muted" id="root"></div></div><div class="muted" id="generated"></div></div>
|
|
99
104
|
<div class="section"><h2>Long-horizon work</h2><div class="grid" id="work"></div></div>
|
|
100
105
|
<div class="section"><h2>Learning loop</h2><div class="grid" id="learning"></div></div>
|
|
106
|
+
<div class="section"><h2>Recent runtime events</h2><div class="card"><table><thead><tr><th>Work</th><th>Event</th><th>Task</th><th>Time</th></tr></thead><tbody id="events"></tbody></table></div></div>
|
|
101
107
|
<div class="section"><h2>Recent evaluations</h2><div class="card"><table><thead><tr><th>Suite</th><th>Model</th><th>Baseline</th><th>UES</th></tr></thead><tbody id="evals"></tbody></table></div></div>
|
|
102
108
|
</div>
|
|
103
109
|
<script>
|
|
@@ -111,11 +117,22 @@ function render(next){
|
|
|
111
117
|
work.innerHTML="";
|
|
112
118
|
for(const item of data.work){
|
|
113
119
|
const tasks=item.tasks.map(t=>'<div class="task"><span>'+esc(t.id)+' · '+esc(t.title||'')+'</span><span class="pill">'+esc(t.state.status||'pending')+'</span></div>').join('');
|
|
114
|
-
|
|
120
|
+
const recover=(item.tasks||[]).some(t=>t.state?.status==="running")?'<button data-recover="'+esc(item.slug)+'">Recover stale</button>':'';
|
|
121
|
+
const receipts=(item.receipts||[]).length
|
|
122
|
+
? '<details><summary>Inspect receipts ('+item.receipts.length+')</summary><pre>'+esc(JSON.stringify(item.receipts,null,2))+'</pre></details>'
|
|
123
|
+
: '';
|
|
124
|
+
work.insertAdjacentHTML("beforeend",'<div class="card"><div><span class="pill">'+esc(item.status)+'</span></div><h3>'+esc(item.slug)+'</h3><div class="muted">'+esc(item.goal)+'</div><p>'+esc(item.nextAction)+'</p>'+tasks+'<div class="muted">receipts: '+item.receipts.length+' · blockers: '+item.blockers.length+'</div>'+recover+receipts+'</div>');
|
|
115
125
|
}
|
|
116
126
|
if(!data.work.length) work.innerHTML='<div class="card muted">No .ues-work items found.</div>';
|
|
117
127
|
const learning=document.querySelector("#learning");
|
|
118
128
|
learning.innerHTML='<div class="card"><h3>Accepted lessons</h3><strong>'+data.learning.accepted.length+'</strong></div><div class="card"><h3>Proposals</h3><strong>'+data.learning.proposals.length+'</strong></div>';
|
|
129
|
+
const eventBody=document.querySelector("#events");
|
|
130
|
+
eventBody.innerHTML="";
|
|
131
|
+
for(const item of data.work){
|
|
132
|
+
for(const event of (item.recentEvents||[]).slice(-10).reverse()){
|
|
133
|
+
eventBody.insertAdjacentHTML("beforeend",'<tr><td>'+esc(item.slug)+'</td><td>'+esc(event.type)+'</td><td>'+esc(event.task||"—")+'</td><td>'+esc(event.at||"")+'</td></tr>');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
119
136
|
const tbody=document.querySelector("#evals");
|
|
120
137
|
tbody.innerHTML="";
|
|
121
138
|
for(const row of data.evals){
|
|
@@ -123,6 +140,22 @@ function render(next){
|
|
|
123
140
|
tbody.insertAdjacentHTML("beforeend",'<tr><td>'+esc(row.suite)+'</td><td>'+esc(row.model)+'</td><td>'+fmt(row.summary.baseline)+'</td><td>'+fmt(row.summary.ues)+'</td></tr>');
|
|
124
141
|
}
|
|
125
142
|
}
|
|
143
|
+
document.addEventListener("click",async(event)=>{
|
|
144
|
+
const button=event.target.closest?.("[data-recover]");
|
|
145
|
+
if(!button) return;
|
|
146
|
+
button.disabled=true;
|
|
147
|
+
try{
|
|
148
|
+
const response=await fetch("/api/recover",{
|
|
149
|
+
method:"POST",
|
|
150
|
+
headers:{"content-type":"application/json"},
|
|
151
|
+
body:JSON.stringify({slug:button.dataset.recover})
|
|
152
|
+
});
|
|
153
|
+
if(!response.ok) throw new Error(await response.text());
|
|
154
|
+
const refreshed=await fetch("/data.json",{cache:"no-store"});
|
|
155
|
+
if(refreshed.ok) render(await refreshed.json());
|
|
156
|
+
}catch(error){ alert(String(error?.message||error)); }
|
|
157
|
+
finally{ button.disabled=false; }
|
|
158
|
+
});
|
|
126
159
|
render(data);
|
|
127
160
|
if(location.protocol==="http:" || location.protocol==="https:"){
|
|
128
161
|
setInterval(async()=>{
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function evalModeOrder(requestedMode, taskIndex = 0, trial = 1) {
|
|
2
|
+
if (requestedMode === "baseline" || requestedMode === "ues") return [requestedMode]
|
|
3
|
+
if (requestedMode !== "both") throw new Error("requestedMode must be baseline, ues, or both")
|
|
4
|
+
|
|
5
|
+
const normalizedTaskIndex = Number.isInteger(taskIndex) && taskIndex >= 0 ? taskIndex : 0
|
|
6
|
+
const normalizedTrial = Number.isInteger(trial) && trial > 0 ? trial : 1
|
|
7
|
+
const baselineFirst = (normalizedTaskIndex + normalizedTrial) % 2 === 1
|
|
8
|
+
return baselineFirst ? ["baseline", "ues"] : ["ues", "baseline"]
|
|
9
|
+
}
|
package/lib/eval-telemetry.mjs
CHANGED
|
@@ -19,8 +19,11 @@ function visit(value, callback, path = []) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function usageFromObject(object) {
|
|
22
|
-
const usage =
|
|
23
|
-
|
|
22
|
+
const usage =
|
|
23
|
+
object?.usage && typeof object.usage === "object" ? object.usage :
|
|
24
|
+
object?.tokens && typeof object.tokens === "object" ? object.tokens :
|
|
25
|
+
null
|
|
26
|
+
if (!usage) return null
|
|
24
27
|
|
|
25
28
|
const input =
|
|
26
29
|
asNumber(usage.inputTokens) ||
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto"
|
|
2
|
+
|
|
3
|
+
const KINDS = new Set(["plan-verification", "integration-verification"])
|
|
4
|
+
const VERDICTS = new Set(["PASS", "FAIL", "PARTIAL"])
|
|
5
|
+
|
|
6
|
+
function digest(value) {
|
|
7
|
+
return createHash("sha256").update(String(value || "")).digest("hex")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createGateReceipt(input = {}) {
|
|
11
|
+
const kind = String(input.kind || "")
|
|
12
|
+
const verdict = String(input.verdict || "").toUpperCase()
|
|
13
|
+
const evidence = String(input.evidence || "").trim()
|
|
14
|
+
const report = input.report == null ? null : String(input.report)
|
|
15
|
+
return {
|
|
16
|
+
schemaVersion: 1,
|
|
17
|
+
id: randomUUID(),
|
|
18
|
+
kind,
|
|
19
|
+
slug: String(input.slug || ""),
|
|
20
|
+
verdict,
|
|
21
|
+
verifier: String(input.verifier || ""),
|
|
22
|
+
sessionID: input.sessionID ? String(input.sessionID) : null,
|
|
23
|
+
runId: input.runId ? String(input.runId) : null,
|
|
24
|
+
planHash: input.planHash ? String(input.planHash) : null,
|
|
25
|
+
workspaceFingerprint: input.workspaceFingerprint ? String(input.workspaceFingerprint) : null,
|
|
26
|
+
evidence,
|
|
27
|
+
reportHash: report == null ? null : digest(report),
|
|
28
|
+
createdAt: input.createdAt || new Date().toISOString(),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function validateGateReceipt(receipt) {
|
|
33
|
+
const errors = []
|
|
34
|
+
if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
|
|
35
|
+
return { valid: false, errors: ["receipt must be an object"] }
|
|
36
|
+
}
|
|
37
|
+
if (receipt.schemaVersion !== 1) errors.push("schemaVersion must be 1")
|
|
38
|
+
if (!KINDS.has(receipt.kind)) errors.push("invalid gate receipt kind")
|
|
39
|
+
if (!VERDICTS.has(receipt.verdict)) errors.push("invalid gate receipt verdict")
|
|
40
|
+
if (!String(receipt.id || "").trim()) errors.push("receipt id is required")
|
|
41
|
+
if (!String(receipt.slug || "").trim()) errors.push("slug is required")
|
|
42
|
+
if (!String(receipt.verifier || "").trim()) errors.push("verifier is required")
|
|
43
|
+
if (!String(receipt.evidence || "").trim()) errors.push("evidence is required")
|
|
44
|
+
if (!Number.isFinite(Date.parse(receipt.createdAt || ""))) errors.push("createdAt must be an ISO timestamp")
|
|
45
|
+
if (receipt.kind === "plan-verification" && !String(receipt.planHash || "").trim()) {
|
|
46
|
+
errors.push("plan verification receipt requires planHash")
|
|
47
|
+
}
|
|
48
|
+
if (receipt.kind === "integration-verification" && !String(receipt.workspaceFingerprint || "").trim()) {
|
|
49
|
+
errors.push("integration verification receipt requires workspaceFingerprint")
|
|
50
|
+
}
|
|
51
|
+
return { valid: errors.length === 0, errors }
|
|
52
|
+
}
|