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
|
@@ -11,50 +11,167 @@ const PROCESS_SKILLS = new Set([
|
|
|
11
11
|
"ues-repo-explorer",
|
|
12
12
|
])
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
const DOMAIN_PATTERNS = [
|
|
15
|
+
["react-native", /(react native|expo|android|ios|gradle|xcode|metro)/],
|
|
16
|
+
["nextjs", /(next\.js|nextjs|app router|server component)/],
|
|
17
|
+
["react", /(\breact\b|\bhook\b|useeffect|usestate|\bcomponent\b)/],
|
|
18
|
+
["fastapi", /(fastapi|pydantic|uvicorn)/],
|
|
19
|
+
["django", /(django|django rest|drf)/],
|
|
20
|
+
["python", /(python|pytest|pip|poetry|pyproject)/],
|
|
21
|
+
["nestjs", /(nestjs|nest\.js)/],
|
|
22
|
+
["nodejs", /(node\.js|nodejs|express|npm|pnpm|yarn)/],
|
|
23
|
+
["dotnet", /(asp\.net|\.net|dotnet|nuget|c#)/],
|
|
24
|
+
["java-spring", /(spring boot|spring framework|maven|gradle java|\bjava\b)/],
|
|
25
|
+
["flutter", /(flutter|dart)/],
|
|
26
|
+
["database", /(database|migration|sql|query|index|transaction|schema changes?|cơ sở dữ liệu|truy vấn|chỉ mục|giao dịch|migrate dữ liệu)/],
|
|
27
|
+
["auth-security", /(auth|authorization|authentication|permission|role|tenant|idor|token|session|xác thực|phân quyền|quyền|vai trò)/],
|
|
28
|
+
["payment", /(payment|checkout|webhook|refund|idempotenc|thanh toán|hoàn tiền)/],
|
|
29
|
+
["api-contract", /(api contract|openapi|response schema|request schema|breaking api|public api|hợp đồng api|api công khai)/],
|
|
30
|
+
["devops", /(docker|github actions|ci\/cd|pipeline|deploy|kubernetes|container|triển khai|đường ống ci)/],
|
|
31
|
+
["performance", /(performance|slow|latency|memory leak|n\+1|bundle size|hiệu năng|chậm|rò rỉ bộ nhớ)/],
|
|
32
|
+
["accessibility", /(accessibility|accessible|a11y|screen reader|keyboard navigation|aria|focus management|focus handling)/],
|
|
33
|
+
["file-upload", /(upload|file upload|multipart|object storage)/],
|
|
34
|
+
["ecommerce", /(ecommerce|marketplace|inventory|cart|catalog|order)/],
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
const STACK_TO_DOMAIN = new Map([
|
|
38
|
+
["react-native", "react-native"],
|
|
39
|
+
["react", "react"],
|
|
40
|
+
["nextjs", "nextjs"],
|
|
41
|
+
["nestjs", "nestjs"],
|
|
42
|
+
["express", "nodejs"],
|
|
43
|
+
["fastify", "nodejs"],
|
|
44
|
+
["node", "nodejs"],
|
|
45
|
+
["python", "python"],
|
|
46
|
+
["django", "django"],
|
|
47
|
+
["java-maven", "java-spring"],
|
|
48
|
+
["java-gradle", "java-spring"],
|
|
49
|
+
["kotlin-gradle", "java-spring"],
|
|
50
|
+
["dotnet", "dotnet"],
|
|
51
|
+
["flutter", "flutter"],
|
|
52
|
+
])
|
|
53
|
+
|
|
54
|
+
export function classifyIntent(text, facts = {}) {
|
|
15
55
|
const value = String(text || "").toLowerCase()
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
56
|
+
const domains = []
|
|
57
|
+
for (const [domain, pattern] of DOMAIN_PATTERNS) {
|
|
58
|
+
if (pattern.test(value)) add(domains, domain)
|
|
59
|
+
}
|
|
20
60
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
61
|
+
const repoStacks = [...new Set(
|
|
62
|
+
(facts.repoStacks || facts.stacks || [])
|
|
63
|
+
.map((item) => String(item || "").toLowerCase())
|
|
64
|
+
.filter(Boolean),
|
|
65
|
+
)]
|
|
66
|
+
const explicitFramework = domains.some((domain) => [
|
|
67
|
+
"react-native", "nextjs", "react", "fastapi", "django", "python", "nestjs",
|
|
68
|
+
"nodejs", "dotnet", "java-spring", "flutter",
|
|
69
|
+
].includes(domain))
|
|
70
|
+
if (!explicitFramework) {
|
|
71
|
+
for (const stack of repoStacks) add(domains, STACK_TO_DOMAIN.get(stack))
|
|
27
72
|
}
|
|
28
|
-
if (/(\bfix\b|\bbug\b|crash|regression|failing|failure|error|exception|broken|\bdebug\b)/.test(value)) add(routed, "ues-bug-diagnosis")
|
|
29
|
-
if (/(latest|current docs|documentation|release notes|version compatibility|dependency|package version|api changed)/.test(value)) add(routed, "ues-research-verification")
|
|
30
73
|
|
|
74
|
+
const actions = []
|
|
75
|
+
if (/(\bfix\b|\bbug\b|crash|regression|failing|failure|error|exception|broken|\bdebug\b|sửa lỗi|lỗi|không chạy|bị hỏng|điều tra lỗi)/.test(value)) add(actions, "debug")
|
|
76
|
+
if (/(implement|feature|add|build|create|triển khai tính năng|thêm|xây dựng)/.test(value)) add(actions, "implement")
|
|
77
|
+
if (/(review|audit|kiểm tra code|đánh giá)/.test(value)) add(actions, "review")
|
|
78
|
+
if (/(investigate|analy[sz]e|profile|optimi[sz]e|điều tra|phân tích|tối ưu)/.test(value)) add(actions, "investigate")
|
|
79
|
+
if (/(refactor|cleanup|restructure|refactor toàn bộ)/.test(value)) add(actions, "refactor")
|
|
80
|
+
if (/(latest|current docs|documentation|release notes|version compatibility|dependency|package version|api changed|tài liệu mới nhất|phiên bản mới|tương thích phiên bản|package mới)/.test(value)) add(actions, "research")
|
|
81
|
+
|
|
82
|
+
const risky = /(migration|schema|database|sql|auth|permission|security|payment|webhook|public api|contract|dependency|deploy|ci|production|rollback|cơ sở dữ liệu|phân quyền|xác thực|bảo mật|thanh toán|triển khai|phụ thuộc)/.test(value)
|
|
83
|
+
const longHorizon = value.length > 700 || /(large task|big task|long[- ]running|multi[- ]file|cross[- ]module|whole (?:repo|repository|project)|entire (?:repo|repository|project)|full refactor|refactor all|migrate all|resume this work|toàn bộ (?:repo|repository|dự án)|nhiều file|nhiều module|refactor toàn bộ|tiếp tục công việc)/.test(value)
|
|
84
|
+
const nonTrivial = value.length > 220 || risky || actions.length > 0
|
|
85
|
+
|
|
86
|
+
const feedbackDomains = [...new Set((facts.feedbackDomains || []).filter((item) => domains.includes(item)))]
|
|
87
|
+
const confidence = Math.min(1, 0.35 + domains.length * 0.08 + actions.length * 0.08 + (repoStacks.length ? 0.08 : 0) + (feedbackDomains.length ? 0.08 : 0))
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
schemaVersion: 1,
|
|
91
|
+
actions,
|
|
92
|
+
domains,
|
|
93
|
+
repoStacks,
|
|
94
|
+
feedbackDomains,
|
|
95
|
+
nonTrivial,
|
|
96
|
+
risk: risky ? "high" : nonTrivial ? "medium" : "low",
|
|
97
|
+
scope: longHorizon ? "long-horizon" : nonTrivial ? "standard" : "inline",
|
|
98
|
+
confidence: Number(confidence.toFixed(2)),
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function addDomainSkills(routed, value, intent) {
|
|
31
103
|
if (/(react native|expo|android|ios|gradle|xcode|metro)/.test(value)) add(routed, "ues-react-native-engineering")
|
|
32
104
|
else if (/(next\.js|nextjs|app router|server component)/.test(value)) add(routed, "ues-nextjs-engineering")
|
|
33
105
|
else if (/(\breact\b|\bhook\b|useeffect|usestate|\bcomponent\b)/.test(value)) add(routed, "ues-react-engineering")
|
|
106
|
+
else {
|
|
107
|
+
for (const domain of intent.domains) {
|
|
108
|
+
if (domain === "react-native") add(routed, "ues-react-native-engineering")
|
|
109
|
+
else if (domain === "nextjs") add(routed, "ues-nextjs-engineering")
|
|
110
|
+
else if (domain === "react") add(routed, "ues-react-engineering")
|
|
111
|
+
}
|
|
112
|
+
}
|
|
34
113
|
|
|
35
|
-
if (/(
|
|
114
|
+
if (/(fastapi|pydantic|uvicorn)/.test(value)) add(routed, "ues-fastapi-engineering")
|
|
115
|
+
else if (/(django|django rest|drf)/.test(value)) add(routed, "ues-django-engineering")
|
|
116
|
+
else if (/(python|pytest|pip|poetry|pyproject)/.test(value)) add(routed, "ues-python-engineering")
|
|
117
|
+
else {
|
|
118
|
+
if (intent.domains.includes("fastapi")) add(routed, "ues-fastapi-engineering")
|
|
119
|
+
else if (intent.domains.includes("django")) add(routed, "ues-django-engineering")
|
|
120
|
+
else if (intent.domains.includes("python")) add(routed, "ues-python-engineering")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (/(nestjs|nest\.js)/.test(value)) add(routed, "ues-nestjs-engineering")
|
|
124
|
+
else if (/(node\.js|nodejs|express|npm|pnpm|yarn)/.test(value)) add(routed, "ues-nodejs-engineering")
|
|
125
|
+
else {
|
|
126
|
+
if (intent.domains.includes("nestjs")) add(routed, "ues-nestjs-engineering")
|
|
127
|
+
else if (intent.domains.includes("nodejs")) add(routed, "ues-nodejs-engineering")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (/(asp\.net|\.net|dotnet|nuget|c#)/.test(value) || intent.domains.includes("dotnet")) add(routed, "ues-dotnet-engineering")
|
|
131
|
+
if (/(spring boot|spring framework|maven|gradle java|\bjava\b)/.test(value) || intent.domains.includes("java-spring")) add(routed, "ues-java-spring-engineering")
|
|
132
|
+
if (/(flutter|dart)/.test(value) || intent.domains.includes("flutter")) add(routed, "ues-flutter-engineering")
|
|
133
|
+
|
|
134
|
+
if (intent.domains.includes("database")) {
|
|
36
135
|
add(routed, "ues-database-engineering")
|
|
37
136
|
add(routed, "ues-change-impact-analysis")
|
|
38
137
|
}
|
|
39
|
-
if (
|
|
138
|
+
if (intent.domains.includes("auth-security")) {
|
|
40
139
|
add(routed, "ues-auth-security")
|
|
41
140
|
add(routed, "ues-change-impact-analysis")
|
|
42
141
|
}
|
|
43
|
-
if (
|
|
142
|
+
if (intent.domains.includes("payment")) {
|
|
44
143
|
add(routed, "ues-payment-engineering")
|
|
45
144
|
add(routed, "ues-change-impact-analysis")
|
|
46
145
|
}
|
|
47
|
-
if (
|
|
146
|
+
if (intent.domains.includes("api-contract")) {
|
|
48
147
|
add(routed, "ues-api-contract")
|
|
49
148
|
add(routed, "ues-change-impact-analysis")
|
|
50
149
|
}
|
|
51
|
-
if (
|
|
52
|
-
if (
|
|
53
|
-
if (
|
|
54
|
-
if (
|
|
55
|
-
if (
|
|
150
|
+
if (intent.domains.includes("devops")) add(routed, "ues-devops-engineering")
|
|
151
|
+
if (intent.domains.includes("performance")) add(routed, "ues-performance-engineering")
|
|
152
|
+
if (intent.domains.includes("accessibility")) add(routed, "ues-accessibility")
|
|
153
|
+
if (intent.domains.includes("file-upload")) add(routed, "ues-file-upload-engineering")
|
|
154
|
+
if (intent.domains.includes("ecommerce")) add(routed, "ues-ecommerce-engineering")
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function routeSkills(text, maxSkills = 4, facts = {}) {
|
|
158
|
+
const value = String(text || "").toLowerCase()
|
|
159
|
+
const limit = Number.isInteger(maxSkills) ? Math.max(1, Math.min(maxSkills, 6)) : 4
|
|
160
|
+
const routed = []
|
|
161
|
+
const intent = classifyIntent(value, facts)
|
|
162
|
+
|
|
163
|
+
if (intent.nonTrivial) add(routed, "ues-engineering-orchestrator")
|
|
164
|
+
if (intent.scope === "long-horizon") {
|
|
165
|
+
add(routed, "ues-engineering-orchestrator")
|
|
166
|
+
add(routed, "ues-long-task-state")
|
|
167
|
+
add(routed, "ues-task-planner")
|
|
168
|
+
}
|
|
169
|
+
if (intent.actions.includes("debug")) add(routed, "ues-bug-diagnosis")
|
|
170
|
+
if (intent.actions.includes("research")) add(routed, "ues-research-verification")
|
|
171
|
+
|
|
172
|
+
addDomainSkills(routed, value, intent)
|
|
56
173
|
|
|
57
|
-
if (routed.length === 0 && /(code|repository|repo|project|function|class|endpoint|test)/.test(value)) {
|
|
174
|
+
if (routed.length === 0 && /(code|repository|repo|project|function|class|endpoint|test|dự án|hàm|lớp|kiểm thử)/.test(value)) {
|
|
58
175
|
add(routed, "ues-repo-explorer")
|
|
59
176
|
}
|
|
60
177
|
|
|
@@ -12,6 +12,7 @@ When the user explicitly chooses the long-horizon workflow (for example with `/u
|
|
|
12
12
|
PLAN.json
|
|
13
13
|
STATE.json
|
|
14
14
|
EVIDENCE.json
|
|
15
|
+
EVENTS.jsonl
|
|
15
16
|
tasks/
|
|
16
17
|
reports/
|
|
17
18
|
```
|
|
@@ -22,9 +23,9 @@ The directory is git-ignored execution state, not hidden reasoning. Store requir
|
|
|
22
23
|
|
|
23
24
|
The V6 engine enforces three boundaries:
|
|
24
25
|
|
|
25
|
-
1. **Plan gate** — `work plan` leaves the item in `awaiting-plan-approval`.
|
|
26
|
+
1. **Plan gate** — `work plan` leaves the item in `awaiting-plan-approval`. Long/high-risk plans require a structured `plan-verification` receipt bound to the current plan hash before `work approve-plan` succeeds.
|
|
26
27
|
2. **Concurrent state gate** — all mutable `STATE.json` and `EVIDENCE.json` operations use a per-work-item lock plus atomic file replacement, preventing safe-wave executors from losing each other's state.
|
|
27
|
-
3. **Integration gate** —
|
|
28
|
+
3. **Integration gate** — long/high-risk PASS requires a structured `integration-verification` receipt bound to the current workspace fingerprint. Finalization rejects any later workspace change.
|
|
28
29
|
|
|
29
30
|
## Pipeline
|
|
30
31
|
|
|
@@ -32,15 +33,16 @@ The V6 engine enforces three boundaries:
|
|
|
32
33
|
2. Write observable acceptance criteria into `SPEC.md`.
|
|
33
34
|
3. Initialize state with `ocskill work init`.
|
|
34
35
|
4. Create `PLAN.json` following the plan schema and import it with `ocskill work plan`.
|
|
35
|
-
5. Run `ues-plan-checker`. If it returns PASS,
|
|
36
|
+
5. Run `ues-plan-checker`. If it returns PASS, create `work gate-receipt <slug> plan` and persist approval with `work approve-plan --receipt-file ...`.
|
|
36
37
|
6. Use `ocskill task-graph` to compute dependency-safe waves.
|
|
37
38
|
7. For each ready task:
|
|
38
39
|
- on OpenCode V2 prefer `ues.dispatch_task`, which performs `work start`, creates a fresh `ues-executor` session, selects the configured attempt-based model tier, prompts it with a bounded context pack and waits for completion;
|
|
39
40
|
- inspect the child diff and verification;
|
|
41
|
+
- record at least one successful `work verify-command` receipt for long/high-risk work;
|
|
40
42
|
- persist `work complete --evidence ...` or `work fail --reason ...`.
|
|
41
43
|
8. On failure, re-diagnose rather than stacking patches. A later `ues.dispatch_task` attempt can escalate from standard to heavy when configured.
|
|
42
44
|
9. After all tasks complete, run `ues-integration-verifier`.
|
|
43
|
-
10. Persist the verifier's actual result with `ocskill work verify-integration
|
|
45
|
+
10. Persist the verifier's actual result with a structured integration receipt plus `ocskill work verify-integration --receipt-file ...`.
|
|
44
46
|
11. Only a recorded PASS with an unchanged workspace can be finalized.
|
|
45
47
|
|
|
46
48
|
## Parallelism
|
package/lib/aci.mjs
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { buildSemanticIndex, querySemanticIndex } from "./semantic-index.mjs"
|
|
4
|
+
|
|
5
|
+
function safeFull(root, relative) {
|
|
6
|
+
const base = path.resolve(root)
|
|
7
|
+
const full = path.resolve(base, relative)
|
|
8
|
+
if (full !== base && !full.startsWith(base + path.sep)) {
|
|
9
|
+
throw new Error("path escapes repository root")
|
|
10
|
+
}
|
|
11
|
+
return full
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function aciSearch(root, query, options = {}) {
|
|
15
|
+
const result = await querySemanticIndex(root, query, {
|
|
16
|
+
limit: options.limit ?? 20,
|
|
17
|
+
rebuild: options.rebuild === true,
|
|
18
|
+
})
|
|
19
|
+
return {
|
|
20
|
+
...result,
|
|
21
|
+
contract: {
|
|
22
|
+
evidenceOnly: true,
|
|
23
|
+
note: "Results are deterministic syntax-aware lexical evidence, not a claim of semantic correctness.",
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function aciReferences(root, symbol, options = {}) {
|
|
29
|
+
const name = String(symbol || "").trim()
|
|
30
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$.-]*$/.test(name)) {
|
|
31
|
+
throw new Error("reference symbol must be a concrete identifier")
|
|
32
|
+
}
|
|
33
|
+
const built = await buildSemanticIndex(root, { rebuild: options.rebuild === true })
|
|
34
|
+
const results = []
|
|
35
|
+
for (const [file, entry] of Object.entries(built.index.files)) {
|
|
36
|
+
const exact = Number(entry.identifiers?.[name] || 0)
|
|
37
|
+
const folded = exact || Object.entries(entry.identifiers || {}).find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1] || 0
|
|
38
|
+
const definitions = (entry.symbols || []).filter((item) => String(item.name).toLowerCase() === name.toLowerCase())
|
|
39
|
+
if (!folded && !definitions.length) continue
|
|
40
|
+
results.push({
|
|
41
|
+
path: file,
|
|
42
|
+
references: Number(folded || 0),
|
|
43
|
+
definitions: definitions.slice(0, 12),
|
|
44
|
+
evidence: definitions.length ? "definition+lexical-reference" : "lexical-reference",
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
results.sort((a, b) => (b.definitions.length - a.definitions.length) || b.references - a.references || a.path.localeCompare(b.path))
|
|
48
|
+
return {
|
|
49
|
+
schemaVersion: 1,
|
|
50
|
+
symbol: name,
|
|
51
|
+
evidenceLevel: "syntax-aware-lexical",
|
|
52
|
+
results: results.slice(0, Math.max(1, Math.min(Number(options.limit || 40), 200))),
|
|
53
|
+
stats: built.stats,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function aciView(root, relative, options = {}) {
|
|
58
|
+
root = path.resolve(root)
|
|
59
|
+
const full = safeFull(root, relative)
|
|
60
|
+
const info = await stat(full).catch(() => null)
|
|
61
|
+
if (!info?.isFile()) throw new Error("file does not exist: " + relative)
|
|
62
|
+
const maxBytes = Math.max(4 * 1024, Math.min(Number(options.maxBytes || 256 * 1024), 1024 * 1024))
|
|
63
|
+
if (info.size > maxBytes) throw new Error("file exceeds bounded viewer limit; request a smaller generated artifact or use repository-native tooling")
|
|
64
|
+
const source = await readFile(full, "utf8")
|
|
65
|
+
if (source.includes("\0")) throw new Error("binary file is not supported by bounded viewer")
|
|
66
|
+
const lines = source.split(/\r?\n/)
|
|
67
|
+
const center = Math.max(1, Number(options.line || 1))
|
|
68
|
+
const count = Math.max(1, Math.min(Number(options.lines || 120), 240))
|
|
69
|
+
const start = Math.max(1, Math.min(
|
|
70
|
+
Number(options.startLine || 0) || (center - Math.floor(count / 3)),
|
|
71
|
+
Math.max(1, lines.length),
|
|
72
|
+
))
|
|
73
|
+
const end = Math.min(lines.length, start + count - 1)
|
|
74
|
+
const width = String(end).length
|
|
75
|
+
return {
|
|
76
|
+
schemaVersion: 1,
|
|
77
|
+
path: path.relative(root, full).replaceAll("\\", "/"),
|
|
78
|
+
startLine: start,
|
|
79
|
+
endLine: end,
|
|
80
|
+
totalLines: lines.length,
|
|
81
|
+
truncated: start > 1 || end < lines.length,
|
|
82
|
+
text: lines.slice(start - 1, end)
|
|
83
|
+
.map((line, index) => String(start + index).padStart(width, " ") + " | " + line)
|
|
84
|
+
.join("\n"),
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function aciTextSearch(root, query, options = {}) {
|
|
89
|
+
const value = String(query || "").trim()
|
|
90
|
+
if (value.length < 2) throw new Error("search query must contain at least two characters")
|
|
91
|
+
const candidateLimit = Math.max(1, Math.min(Number(options.candidateLimit || 500), 5000))
|
|
92
|
+
const resultLimit = Math.max(1, Math.min(Number(options.limit || 80), 200))
|
|
93
|
+
const semantic = await querySemanticIndex(root, value, { limit: Math.min(candidateLimit, 100) })
|
|
94
|
+
const built = await buildSemanticIndex(root)
|
|
95
|
+
const allIndexed = Object.entries(built.index.files)
|
|
96
|
+
.filter(([, entry]) => !entry?.skipped)
|
|
97
|
+
.map(([file]) => file)
|
|
98
|
+
const candidates = [...new Set([
|
|
99
|
+
...semantic.results.map((item) => item.path),
|
|
100
|
+
...allIndexed,
|
|
101
|
+
])].slice(0, candidateLimit)
|
|
102
|
+
const matches = []
|
|
103
|
+
const needle = options.caseSensitive ? value : value.toLowerCase()
|
|
104
|
+
for (const file of candidates) {
|
|
105
|
+
const full = safeFull(root, file)
|
|
106
|
+
const source = await readFile(full, "utf8").catch(() => "")
|
|
107
|
+
if (!source) continue
|
|
108
|
+
const lines = source.split(/\r?\n/)
|
|
109
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
110
|
+
const haystack = options.caseSensitive ? lines[index] : lines[index].toLowerCase()
|
|
111
|
+
if (!haystack.includes(needle)) continue
|
|
112
|
+
matches.push({ path: file, line: index + 1, preview: lines[index].trim().slice(0, 300) })
|
|
113
|
+
if (matches.length >= resultLimit) break
|
|
114
|
+
}
|
|
115
|
+
if (matches.length >= resultLimit) break
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
schemaVersion: 1,
|
|
119
|
+
query: value,
|
|
120
|
+
evidenceLevel: "exact-text",
|
|
121
|
+
matches,
|
|
122
|
+
candidatesScanned: candidates.length,
|
|
123
|
+
candidatePool: allIndexed.length,
|
|
124
|
+
candidateTruncated: candidates.length < allIndexed.length,
|
|
125
|
+
resultsTruncated: matches.length >= resultLimit,
|
|
126
|
+
semanticStats: built.stats,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
function asBool(value) {
|
|
2
|
+
return value === true
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function pairKey(item) {
|
|
6
|
+
return [item.suite || "", item.task || "", item.trial || 1].join("::")
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function exactSignPValue(a, b) {
|
|
10
|
+
const n = a + b
|
|
11
|
+
if (n <= 0) return 1
|
|
12
|
+
const k = Math.min(a, b)
|
|
13
|
+
let probability = 2 ** (-n)
|
|
14
|
+
let cumulative = probability
|
|
15
|
+
for (let i = 1; i <= k; i += 1) {
|
|
16
|
+
probability *= (n - i + 1) / i
|
|
17
|
+
cumulative += probability
|
|
18
|
+
}
|
|
19
|
+
return Math.min(1, cumulative * 2)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function mean(values) {
|
|
23
|
+
const usable = values.filter((value) => Number.isFinite(value))
|
|
24
|
+
return usable.length ? usable.reduce((sum, value) => sum + value, 0) / usable.length : null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function pairedBenchmarkConfidence(results, options = {}) {
|
|
28
|
+
const byKey = new Map()
|
|
29
|
+
for (const item of results || []) {
|
|
30
|
+
if (!["baseline", "ues"].includes(item?.mode)) continue
|
|
31
|
+
const key = pairKey(item)
|
|
32
|
+
const pair = byKey.get(key) || {}
|
|
33
|
+
pair[item.mode] = item
|
|
34
|
+
byKey.set(key, pair)
|
|
35
|
+
}
|
|
36
|
+
const pairs = [...byKey.entries()]
|
|
37
|
+
.filter(([, pair]) => pair.baseline && pair.ues)
|
|
38
|
+
.map(([key, pair]) => ({ key, ...pair }))
|
|
39
|
+
|
|
40
|
+
let bothPass = 0
|
|
41
|
+
let bothFail = 0
|
|
42
|
+
let baselineOnly = 0
|
|
43
|
+
let uesOnly = 0
|
|
44
|
+
for (const pair of pairs) {
|
|
45
|
+
const b = asBool(pair.baseline.passed)
|
|
46
|
+
const u = asBool(pair.ues.passed)
|
|
47
|
+
if (b && u) bothPass += 1
|
|
48
|
+
else if (!b && !u) bothFail += 1
|
|
49
|
+
else if (b) baselineOnly += 1
|
|
50
|
+
else uesOnly += 1
|
|
51
|
+
}
|
|
52
|
+
const total = pairs.length
|
|
53
|
+
const baselinePassed = bothPass + baselineOnly
|
|
54
|
+
const uesPassed = bothPass + uesOnly
|
|
55
|
+
const baselinePassRate = total ? baselinePassed / total : 0
|
|
56
|
+
const uesPassRate = total ? uesPassed / total : 0
|
|
57
|
+
const delta = uesPassRate - baselinePassRate
|
|
58
|
+
const pValue = exactSignPValue(uesOnly, baselineOnly)
|
|
59
|
+
|
|
60
|
+
const suites = {}
|
|
61
|
+
for (const pair of pairs) {
|
|
62
|
+
const suite = pair.baseline.suite || pair.ues.suite || "unknown"
|
|
63
|
+
suites[suite] ??= { total: 0, baselinePassed: 0, uesPassed: 0 }
|
|
64
|
+
suites[suite].total += 1
|
|
65
|
+
if (pair.baseline.passed) suites[suite].baselinePassed += 1
|
|
66
|
+
if (pair.ues.passed) suites[suite].uesPassed += 1
|
|
67
|
+
}
|
|
68
|
+
for (const item of Object.values(suites)) {
|
|
69
|
+
item.baselinePassRate = item.total ? item.baselinePassed / item.total : 0
|
|
70
|
+
item.uesPassRate = item.total ? item.uesPassed / item.total : 0
|
|
71
|
+
item.delta = item.uesPassRate - item.baselinePassRate
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const baselineDurations = pairs.map((pair) => Number(pair.baseline.durationMs))
|
|
75
|
+
const uesDurations = pairs.map((pair) => Number(pair.ues.durationMs))
|
|
76
|
+
const baselineDuration = mean(baselineDurations)
|
|
77
|
+
const uesDuration = mean(uesDurations)
|
|
78
|
+
const durationRatio = baselineDuration && uesDuration ? uesDuration / baselineDuration : null
|
|
79
|
+
|
|
80
|
+
const baselineCosts = pairs.map((pair) => Number(pair.baseline.telemetry?.cost)).filter((value) => value > 0)
|
|
81
|
+
const uesCosts = pairs.map((pair) => Number(pair.ues.telemetry?.cost)).filter((value) => value > 0)
|
|
82
|
+
const baselineCost = mean(baselineCosts)
|
|
83
|
+
const uesCost = mean(uesCosts)
|
|
84
|
+
const costRatio = baselineCost && uesCost ? uesCost / baselineCost : null
|
|
85
|
+
|
|
86
|
+
const minPairs = Math.max(1, Number(options.minPairs || 20))
|
|
87
|
+
const alpha = Math.min(0.5, Math.max(0.0001, Number(options.alpha || 0.05)))
|
|
88
|
+
const minDelta = Math.max(0, Number(options.minDelta || 0))
|
|
89
|
+
const suiteRegressionTolerance = Math.max(0, Number(options.suiteRegressionTolerance || 0))
|
|
90
|
+
const maxDurationRatio = Math.max(1, Number(options.maxDurationRatio || 1.75))
|
|
91
|
+
const noSuiteRegression = Object.values(suites).every((item) => item.delta >= -suiteRegressionTolerance)
|
|
92
|
+
const speedAcceptable = durationRatio == null || durationRatio <= maxDurationRatio
|
|
93
|
+
|
|
94
|
+
const checks = {
|
|
95
|
+
pairedCoverage: total >= minPairs,
|
|
96
|
+
positiveDelta: delta > minDelta,
|
|
97
|
+
moreWinsThanLosses: uesOnly > baselineOnly,
|
|
98
|
+
statisticallySupported: pValue <= alpha,
|
|
99
|
+
noSuiteRegression,
|
|
100
|
+
speedAcceptable,
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
schemaVersion: 1,
|
|
104
|
+
kind: "ues-paired-benchmark-confidence",
|
|
105
|
+
pairs: total,
|
|
106
|
+
bothPass,
|
|
107
|
+
bothFail,
|
|
108
|
+
baselineOnly,
|
|
109
|
+
uesOnly,
|
|
110
|
+
baselinePassed,
|
|
111
|
+
uesPassed,
|
|
112
|
+
baselinePassRate,
|
|
113
|
+
uesPassRate,
|
|
114
|
+
delta,
|
|
115
|
+
pValue,
|
|
116
|
+
alpha,
|
|
117
|
+
minPairs,
|
|
118
|
+
minDelta,
|
|
119
|
+
suiteRegressionTolerance,
|
|
120
|
+
duration: {
|
|
121
|
+
baselineMeanMs: baselineDuration,
|
|
122
|
+
uesMeanMs: uesDuration,
|
|
123
|
+
ratio: durationRatio,
|
|
124
|
+
maxRatio: maxDurationRatio,
|
|
125
|
+
},
|
|
126
|
+
cost: {
|
|
127
|
+
baselineMean: baselineCost,
|
|
128
|
+
uesMean: uesCost,
|
|
129
|
+
ratio: costRatio,
|
|
130
|
+
},
|
|
131
|
+
suites,
|
|
132
|
+
checks,
|
|
133
|
+
promotionEligible: Object.values(checks).every(Boolean),
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
export function clipOutput(value, { head = 8000, tail = 8000 } = {}) {
|
|
5
|
+
const text = String(value ?? "")
|
|
6
|
+
if (text.length <= head + tail) return text
|
|
7
|
+
return text.slice(0, head) + "\n...[truncated]\n" + text.slice(-tail)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function positionalArg(args, index) {
|
|
11
|
+
const value = args[index]
|
|
12
|
+
if (value == null || value === "" || value === "--" || value.startsWith("--")) return null
|
|
13
|
+
return value
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function readJsonFile(file) {
|
|
17
|
+
return JSON.parse(readFileSync(path.resolve(file), "utf8"))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function readTextFile(file) {
|
|
21
|
+
return readFileSync(path.resolve(file), "utf8")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function optionInt(args, name, fallback) {
|
|
25
|
+
const index = args.indexOf(name)
|
|
26
|
+
const raw = index >= 0 ? args[index + 1] : null
|
|
27
|
+
return Number.parseInt(raw || String(fallback), 10)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function optionIntOrUndefined(args, name) {
|
|
31
|
+
return optionInt(args, name, 0) || undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function optionValue(args, name) {
|
|
35
|
+
const index = args.indexOf(name)
|
|
36
|
+
return index >= 0 ? args[index + 1] : null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function errorMessage(error) {
|
|
40
|
+
return error instanceof Error ? error.message : error
|
|
41
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
function probe(engine) {
|
|
5
|
+
const result = spawnSync(engine, ["--version"], { encoding: "utf8", timeout: 5000 })
|
|
6
|
+
return {
|
|
7
|
+
engine,
|
|
8
|
+
available: result.status === 0,
|
|
9
|
+
version: result.status === 0 ? String(result.stdout || result.stderr || "").trim() : null,
|
|
10
|
+
error: result.status === 0 ? null : String(result.stderr || result.stdout || "").trim() || "unavailable",
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function containerSandboxCapability(preferred = null) {
|
|
15
|
+
const names = preferred ? [preferred] : ["docker", "podman"]
|
|
16
|
+
const probes = names.map(probe)
|
|
17
|
+
const selected = probes.find((item) => item.available) || null
|
|
18
|
+
return {
|
|
19
|
+
schemaVersion: 1,
|
|
20
|
+
available: Boolean(selected),
|
|
21
|
+
selected,
|
|
22
|
+
probes,
|
|
23
|
+
guarantees: selected ? [
|
|
24
|
+
"network disabled by default",
|
|
25
|
+
"Linux capabilities dropped",
|
|
26
|
+
"no-new-privileges",
|
|
27
|
+
"read-only container root",
|
|
28
|
+
"bounded pids/memory/cpu",
|
|
29
|
+
"only requested workspace bind-mounted read-write",
|
|
30
|
+
"no host environment secrets forwarded by default",
|
|
31
|
+
] : [],
|
|
32
|
+
limitation: "This isolates deterministic verification commands, not the OpenCode model session itself.",
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function validImage(image) {
|
|
37
|
+
return /^[A-Za-z0-9][A-Za-z0-9._/:@-]{0,240}$/.test(String(image || ""))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function buildContainerSandboxArgs(root, image, command, commandArgs = [], options = {}) {
|
|
41
|
+
if (!validImage(image)) throw new Error("container image is required and must be a concrete image reference")
|
|
42
|
+
if (!command) throw new Error("sandbox command is required")
|
|
43
|
+
root = path.resolve(root)
|
|
44
|
+
const memory = String(options.memory || "2g")
|
|
45
|
+
const cpus = String(options.cpus || "2")
|
|
46
|
+
const pids = String(options.pids || "256")
|
|
47
|
+
const args = [
|
|
48
|
+
"run", "--rm", "--init",
|
|
49
|
+
"--network", options.network === "bridge" ? "bridge" : "none",
|
|
50
|
+
"--cap-drop", "ALL",
|
|
51
|
+
"--security-opt", "no-new-privileges",
|
|
52
|
+
"--read-only",
|
|
53
|
+
"--pids-limit", pids,
|
|
54
|
+
"--memory", memory,
|
|
55
|
+
"--cpus", cpus,
|
|
56
|
+
"--tmpfs", "/tmp:rw,noexec,nosuid,size=256m",
|
|
57
|
+
"--mount", "type=bind,source=" + root + ",target=/workspace",
|
|
58
|
+
"--workdir", "/workspace",
|
|
59
|
+
"--env", "CI=true",
|
|
60
|
+
]
|
|
61
|
+
if (process.platform !== "win32" && typeof process.getuid === "function" && typeof process.getgid === "function") {
|
|
62
|
+
args.push("--user", process.getuid() + ":" + process.getgid())
|
|
63
|
+
}
|
|
64
|
+
args.push(image, command, ...commandArgs)
|
|
65
|
+
return args
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function runContainerSandbox(root, input = {}) {
|
|
69
|
+
const capability = containerSandboxCapability(input.engine || null)
|
|
70
|
+
if (!capability.available) {
|
|
71
|
+
const error = new Error("container sandbox requested but Docker/Podman is unavailable")
|
|
72
|
+
error.capability = capability
|
|
73
|
+
throw error
|
|
74
|
+
}
|
|
75
|
+
const args = buildContainerSandboxArgs(root, input.image, input.command, input.args || [], input)
|
|
76
|
+
const started = Date.now()
|
|
77
|
+
const result = spawnSync(capability.selected.engine, args, {
|
|
78
|
+
cwd: path.resolve(root),
|
|
79
|
+
encoding: "utf8",
|
|
80
|
+
maxBuffer: Math.max(1024 * 1024, Number(input.maxBuffer || 8 * 1024 * 1024)),
|
|
81
|
+
timeout: Math.max(1000, Number(input.timeoutMs || 10 * 60_000)),
|
|
82
|
+
env: {
|
|
83
|
+
SystemRoot: process.env.SystemRoot,
|
|
84
|
+
WINDIR: process.env.WINDIR,
|
|
85
|
+
PATH: process.env.PATH,
|
|
86
|
+
PATHEXT: process.env.PATHEXT,
|
|
87
|
+
HOME: process.env.HOME,
|
|
88
|
+
USERPROFILE: process.env.USERPROFILE,
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
return {
|
|
92
|
+
schemaVersion: 1,
|
|
93
|
+
engine: capability.selected,
|
|
94
|
+
args,
|
|
95
|
+
status: result.status ?? 1,
|
|
96
|
+
signal: result.signal || null,
|
|
97
|
+
stdout: String(result.stdout || ""),
|
|
98
|
+
stderr: String(result.stderr || ""),
|
|
99
|
+
durationMs: Date.now() - started,
|
|
100
|
+
timedOut: Boolean(result.error?.code === "ETIMEDOUT"),
|
|
101
|
+
}
|
|
102
|
+
}
|