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.
Files changed (63) hide show
  1. package/CHANGELOG.md +63 -3
  2. package/README.md +675 -581
  3. package/bin/ocskill.mjs +358 -156
  4. package/docs/DETERMINISTIC-TOOLS.md +25 -8
  5. package/docs/ENGINEERING-DESIGN.md +31 -13
  6. package/docs/EVALS.md +34 -12
  7. package/docs/NPM-PUBLISH.md +6 -6
  8. package/docs/OPENCODE-COMPAT.md +11 -8
  9. package/docs/TRACE-SCHEMA.md +15 -2
  10. package/docs/V8-INTELLIGENCE-RELIABILITY.md +206 -0
  11. package/docs/V9-SPEED-INTELLIGENCE.md +102 -0
  12. package/evals/live/tasks.json +6 -6
  13. package/evals/polyglot/fixtures/polyglot-bench/api/generated/client.ts +2 -0
  14. package/evals/polyglot/fixtures/polyglot-bench/api/openapi.json +25 -0
  15. package/evals/polyglot/fixtures/polyglot-bench/db/migrations/20260920_add_order_key.sql +1 -0
  16. package/evals/polyglot/fixtures/polyglot-bench/dotnet/OrderService.cs +8 -0
  17. package/evals/polyglot/fixtures/polyglot-bench/java/PriceService.java +5 -0
  18. package/evals/polyglot/fixtures/polyglot-bench/monorepo/package.json +6 -0
  19. package/evals/polyglot/fixtures/polyglot-bench/monorepo/packages/api/package.json +4 -0
  20. package/evals/polyglot/fixtures/polyglot-bench/monorepo/packages/web/package.json +7 -0
  21. package/evals/polyglot/fixtures/polyglot-bench/monorepo/pnpm-lock.yaml +5 -0
  22. package/evals/polyglot/fixtures/polyglot-bench/next/app/api/products/route.ts +7 -0
  23. package/evals/polyglot/fixtures/polyglot-bench/python/tenant_auth.py +4 -0
  24. package/evals/polyglot/fixtures/polyglot-bench/react-native/keyboard.ts +3 -0
  25. package/evals/polyglot/graders/polyglot-bench.mjs +101 -0
  26. package/evals/polyglot/tasks.json +54 -0
  27. package/global-config/AGENTS.md +10 -7
  28. package/global-config/agents/integration-verifier.md +1 -1
  29. package/global-config/agents/plan-checker.md +1 -1
  30. package/global-config/commands/run.md +9 -5
  31. package/global-config/plugins/ues-router/capabilities.js +4 -0
  32. package/global-config/plugins/ues-router/index.js +414 -21
  33. package/global-config/plugins/ues-router/router.js +140 -23
  34. package/global-config/skills/engineering-orchestrator/references/long-horizon.md +6 -4
  35. package/lib/aci.mjs +128 -0
  36. package/lib/benchmark-confidence.mjs +135 -0
  37. package/lib/cli-utils.mjs +41 -0
  38. package/lib/container-sandbox.mjs +102 -0
  39. package/lib/context-manifest.mjs +300 -22
  40. package/lib/control-center.mjs +36 -3
  41. package/lib/eval-order.mjs +9 -0
  42. package/lib/eval-telemetry.mjs +5 -2
  43. package/lib/gate-receipt.mjs +52 -0
  44. package/lib/installer.mjs +39 -25
  45. package/lib/learning-engine.mjs +236 -38
  46. package/lib/opencode-compat.mjs +25 -10
  47. package/lib/orchestrator-policy.mjs +101 -20
  48. package/lib/process-runner.mjs +30 -9
  49. package/lib/runtime-events.mjs +31 -0
  50. package/lib/semantic-index.mjs +318 -0
  51. package/lib/task-engine.mjs +416 -24
  52. package/lib/trajectory.mjs +89 -0
  53. package/lib/windows-shim.mjs +227 -0
  54. package/lib/worktree-sandbox.mjs +85 -3
  55. package/package.json +10 -4
  56. package/scripts/check-release-tag.mjs +22 -0
  57. package/scripts/control-center.mjs +25 -0
  58. package/scripts/eval-live.mjs +39 -58
  59. package/scripts/eval-matrix.mjs +155 -0
  60. package/scripts/smoke-packed-install.mjs +138 -4
  61. package/scripts/smoke-plain-install.mjs +91 -0
  62. package/scripts/validate-live-suite.mjs +3 -3
  63. 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
- export function routeSkills(text, maxSkills = 4) {
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 limit = Number.isInteger(maxSkills) ? Math.max(1, Math.min(maxSkills, 6)) : 4
17
- const routed = []
18
- const risky = /(migration|schema|database|sql|auth|permission|security|payment|webhook|public api|contract|dependency|deploy|ci|production|rollback)/
19
- const nonTrivial = value.length > 220 || risky.test(value) || /(implement|feature|refactor|fix|debug|investigate|review|audit|bug|regression|failing|failure|error|exception|broken)/.test(value)
56
+ const domains = []
57
+ for (const [domain, pattern] of DOMAIN_PATTERNS) {
58
+ if (pattern.test(value)) add(domains, domain)
59
+ }
20
60
 
21
- if (nonTrivial) add(routed, "ues-engineering-orchestrator")
22
- 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)/.test(value)
23
- if (longHorizon) {
24
- add(routed, "ues-engineering-orchestrator")
25
- add(routed, "ues-long-task-state")
26
- add(routed, "ues-task-planner")
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 (/(database|migration|sql|query|index|transaction|schema changes?)/.test(value)) {
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 (/(auth|authorization|authentication|permission|role|tenant|idor|token|session)/.test(value)) {
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 (/(payment|checkout|webhook|refund|idempotenc)/.test(value)) {
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 (/(api contract|openapi|response schema|request schema|breaking api|public api)/.test(value)) {
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 (/(docker|github actions|ci\/cd|pipeline|deploy|kubernetes|container)/.test(value)) add(routed, "ues-devops-engineering")
52
- if (/(performance|slow|latency|memory leak|n\+1|bundle size)/.test(value)) add(routed, "ues-performance-engineering")
53
- if (/(accessibility|accessible|a11y|screen reader|keyboard navigation|aria|focus management|focus handling)/.test(value)) add(routed, "ues-accessibility")
54
- if (/(upload|file upload|multipart|object storage)/.test(value)) add(routed, "ues-file-upload-engineering")
55
- if (/(ecommerce|marketplace|inventory|cart|catalog|order)/.test(value)) add(routed, "ues-ecommerce-engineering")
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`. `work start` refuses to run until an independent plan checker PASS is recorded with `work approve-plan`.
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** — finalization requires a recorded `verify-integration --verdict PASS`. The engine fingerprints the Git workspace at PASS time and rejects finalization if the workspace changes afterward.
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, persist that gate with `ocskill work approve-plan`.
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
+ }