opencode-agent-skill 7.7.0 → 10.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 +112 -3
- package/README.md +396 -281
- package/bin/ocskill.mjs +382 -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 +78 -160
- 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 +784 -37
- package/global-config/plugins/ues-router/router.js +175 -23
- package/global-config/plugins/ues-router/runtime-guard.js +265 -0
- package/global-config/skills/engineering-orchestrator/references/long-horizon.md +6 -4
- package/lib/aci.mjs +128 -0
- package/lib/benchmark-confidence.mjs +173 -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-ablation.mjs +104 -0
- package/lib/eval-order.mjs +9 -0
- package/lib/eval-report.mjs +11 -0
- package/lib/eval-telemetry.mjs +8 -2
- package/lib/gate-receipt.mjs +52 -0
- package/lib/installer.mjs +39 -25
- package/lib/learning-engine.mjs +236 -38
- package/lib/model-policy.mjs +6 -0
- package/lib/opencode-compat.mjs +25 -10
- package/lib/orchestrator-policy.mjs +195 -21
- 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 +557 -28
- package/lib/trajectory.mjs +89 -0
- package/lib/windows-shim.mjs +227 -0
- package/lib/worktree-sandbox.mjs +85 -3
- package/package.json +11 -4
- package/scripts/check-release-tag.mjs +22 -0
- package/scripts/control-center.mjs +25 -0
- package/scripts/eval-ablation.mjs +44 -0
- package/scripts/eval-live.mjs +39 -58
- package/scripts/eval-matrix.mjs +166 -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))
|
|
72
|
+
}
|
|
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)),
|
|
27
99
|
}
|
|
28
|
-
|
|
29
|
-
if (/(latest|current docs|documentation|release notes|version compatibility|dependency|package version|api changed)/.test(value)) add(routed, "ues-research-verification")
|
|
100
|
+
}
|
|
30
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
|
+
}
|
|
113
|
+
|
|
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")
|
|
34
133
|
|
|
35
|
-
if (
|
|
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
|
|
|
@@ -72,3 +189,38 @@ export function routeSkills(text, maxSkills = 4) {
|
|
|
72
189
|
|
|
73
190
|
return routed.slice(0, limit)
|
|
74
191
|
}
|
|
192
|
+
|
|
193
|
+
export function routeSkillsForPolicy(text, policy = {}, maxSkills = 4, facts = {}) {
|
|
194
|
+
const limit = Number.isInteger(maxSkills) ? Math.max(1, Math.min(maxSkills, 6)) : 4
|
|
195
|
+
const routed = routeSkills(text, 6, facts)
|
|
196
|
+
const fast =
|
|
197
|
+
policy?.executionProfile === "fast" &&
|
|
198
|
+
policy?.risk !== "high" &&
|
|
199
|
+
policy?.mode !== "long-horizon"
|
|
200
|
+
|
|
201
|
+
if (!fast) return routed.slice(0, limit)
|
|
202
|
+
|
|
203
|
+
const eligible = routed.filter((id) =>
|
|
204
|
+
![
|
|
205
|
+
"ues-engineering-orchestrator",
|
|
206
|
+
"ues-change-impact-analysis",
|
|
207
|
+
"ues-task-planner",
|
|
208
|
+
"ues-long-task-state",
|
|
209
|
+
].includes(id),
|
|
210
|
+
)
|
|
211
|
+
const selected = [
|
|
212
|
+
...eligible.filter((id) => !PROCESS_SKILLS.has(id)),
|
|
213
|
+
...eligible.filter((id) => PROCESS_SKILLS.has(id)),
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
const value = String(text || "").toLowerCase()
|
|
217
|
+
if (/(review|audit|kiểm tra code|đánh giá)/.test(value)) add(selected, "ues-code-review")
|
|
218
|
+
if (/(verify|verification|test|tests|kiểm thử|xác minh)/.test(value)) add(selected, "ues-test-verification")
|
|
219
|
+
|
|
220
|
+
if (selected.length === 0 && /(code|repository|repo|project|function|class|endpoint|hàm|lớp|dự án)/.test(value)) {
|
|
221
|
+
add(selected, "ues-repo-explorer")
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return selected.slice(0, limit)
|
|
225
|
+
}
|
|
226
|
+
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
|
|
3
|
+
const EXPLORATION_TOOL = /(?:^|[._-])(read|grep|glob|repo[-_.]?graph|semantic[-_.]?search|aci[-_.]?search)(?:$|[._-])/i
|
|
4
|
+
const VERIFY_OR_WRITE_TOOL = /(?:^|[._-])(edit|write|patch|bash|shell|verify|test|apply)(?:$|[._-])/i
|
|
5
|
+
|
|
6
|
+
function canonical(value) {
|
|
7
|
+
if (Array.isArray(value)) return value.map(canonical)
|
|
8
|
+
if (!value || typeof value !== "object") return value
|
|
9
|
+
return Object.fromEntries(
|
|
10
|
+
Object.keys(value).sort().map((key) => [key, canonical(value[key])]),
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function stableRuntimeHash(value) {
|
|
15
|
+
const text = typeof value === "string" ? value : JSON.stringify(canonical(value))
|
|
16
|
+
return createHash("sha256").update(text || "").digest("hex")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isExplorationTool(tool) {
|
|
20
|
+
return EXPLORATION_TOOL.test(String(tool || ""))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function budgetToolResult(tool, result, options = {}) {
|
|
24
|
+
const name = String(tool || "")
|
|
25
|
+
const aggressive = /grep|glob|repo[-_.]?graph|semantic[-_.]?search/i.test(name)
|
|
26
|
+
const maxChars = Math.max(2_000, Number(options.maxChars || (aggressive ? 12_000 : 24_000)))
|
|
27
|
+
const maxLines = Math.max(40, Number(options.maxLines || (aggressive ? 160 : 360)))
|
|
28
|
+
|
|
29
|
+
if (!aggressive && !/read/i.test(name)) return result
|
|
30
|
+
|
|
31
|
+
const original = typeof result === "string" ? result : String(result?.output || "")
|
|
32
|
+
const lines = original.split(/\r?\n/)
|
|
33
|
+
if (original.length <= maxChars && lines.length <= maxLines) return result
|
|
34
|
+
|
|
35
|
+
const digest = stableRuntimeHash(original)
|
|
36
|
+
const headLineCount = Math.max(1, Math.floor(maxLines * 0.7))
|
|
37
|
+
const tailLineCount = Math.max(1, maxLines - headLineCount)
|
|
38
|
+
let bounded = [
|
|
39
|
+
...lines.slice(0, headLineCount),
|
|
40
|
+
`...[UES tool output truncated: ${lines.length} lines / ${original.length} chars, sha256=${digest.slice(0, 16)}]...`,
|
|
41
|
+
...lines.slice(-tailLineCount),
|
|
42
|
+
].join("\n")
|
|
43
|
+
|
|
44
|
+
if (bounded.length > maxChars) {
|
|
45
|
+
const marker = `\n...[UES char budget applied; sha256=${digest.slice(0, 16)}]...\n`
|
|
46
|
+
const room = Math.max(0, maxChars - marker.length)
|
|
47
|
+
const head = Math.floor(room * 0.7)
|
|
48
|
+
bounded = bounded.slice(0, head) + marker + bounded.slice(-(room - head))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (typeof result === "string") return bounded
|
|
52
|
+
return {
|
|
53
|
+
...result,
|
|
54
|
+
output: bounded,
|
|
55
|
+
metadata: {
|
|
56
|
+
...(result?.metadata || {}),
|
|
57
|
+
uesTruncated: true,
|
|
58
|
+
uesOriginalChars: original.length,
|
|
59
|
+
uesOriginalLines: lines.length,
|
|
60
|
+
uesOutputDigest: digest,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function classifyProviderFailure(error = {}) {
|
|
66
|
+
const status = Number(error?.status ?? error?.cause?.status)
|
|
67
|
+
const type = String(error?.type || error?.code || "")
|
|
68
|
+
const message = String(error?.message || error || "")
|
|
69
|
+
const value = `${type} ${message}`.toLowerCase()
|
|
70
|
+
|
|
71
|
+
if (status === 401 || status === 403 || /auth|unauthori[sz]ed|invalid api key|credential/.test(value)) return "AUTH"
|
|
72
|
+
if (/context.{0,20}(large|length|window|overflow)|too many tokens|max(?:imum)? context/.test(value)) return "CONTEXT_TOO_LARGE"
|
|
73
|
+
if (status === 429 || /rate.?limit|too many requests/.test(value)) return "RATE_LIMIT"
|
|
74
|
+
if (/quota|credit|billing|insufficient balance/.test(value)) return "QUOTA"
|
|
75
|
+
if (/no token|no output|empty response|empty completion|returned no content|no content|stalled|no-progress/.test(value)) return "NO_TOKEN"
|
|
76
|
+
if (/timeout|timed out|deadline|abort(?:ed)?/.test(value)) return "TIMEOUT"
|
|
77
|
+
if (Number.isFinite(status) && status >= 500 && status < 600) return "PROVIDER_5XX"
|
|
78
|
+
if (/provider|upstream|gateway|service unavailable/.test(value)) return "PROVIDER_5XX"
|
|
79
|
+
return "OTHER"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function progressWatchdogDecision(snapshot = {}, now = Date.now(), stallMs = 60_000) {
|
|
83
|
+
const limit = Math.max(30_000, Math.min(Number(stallMs || 60_000), 5 * 60_000))
|
|
84
|
+
const lastProgressAt = Number(snapshot.lastProgressAt || 0)
|
|
85
|
+
const activeToolCalls = Math.max(0, Number(snapshot.activeToolCalls || 0))
|
|
86
|
+
const idleMs = Math.max(0, Number(now) - lastProgressAt)
|
|
87
|
+
|
|
88
|
+
if (activeToolCalls > 0) {
|
|
89
|
+
return { stalled: false, idleMs, limitMs: limit, reason: "tool-active" }
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
stalled: idleMs >= limit,
|
|
93
|
+
idleMs,
|
|
94
|
+
limitMs: limit,
|
|
95
|
+
reason: idleMs >= limit ? "no-progress" : "within-grace",
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function providerRecoveryPlan(kind, physicalAttempt = 1, options = {}) {
|
|
100
|
+
const attempt = Math.max(1, Number(physicalAttempt || 1))
|
|
101
|
+
const hasEscalationModel = Boolean(options.hasEscalationModel)
|
|
102
|
+
|
|
103
|
+
if (kind === "AUTH") return { action: "fail-fast", retry: false, reason: "authentication failure requires user/config repair" }
|
|
104
|
+
if (kind === "CONTEXT_TOO_LARGE") return { action: "compact-context", retry: false, reason: "reduce/compact context instead of repeating the same request" }
|
|
105
|
+
if (kind === "QUOTA") {
|
|
106
|
+
return hasEscalationModel
|
|
107
|
+
? { action: "fresh-session-escalated-model", retry: true, reason: "quota exhausted on current provider/model" }
|
|
108
|
+
: { action: "fail-retryable", retry: false, reason: "quota exhausted and no configured fallback model exists" }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (["NO_TOKEN", "TIMEOUT", "RATE_LIMIT", "PROVIDER_5XX"].includes(kind)) {
|
|
112
|
+
if (attempt <= 1) {
|
|
113
|
+
return { action: "fresh-session-same-model", retry: true, reason: "retry transport/provider stall once with fresh session state" }
|
|
114
|
+
}
|
|
115
|
+
if (hasEscalationModel) {
|
|
116
|
+
return { action: "fresh-session-escalated-model", retry: true, reason: "repeated provider failure triggers configured model/provider escalation" }
|
|
117
|
+
}
|
|
118
|
+
return { action: "fail-retryable", retry: false, reason: "repeated provider failure without configured fallback" }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { action: "fail-task", retry: false, reason: "non-provider failure should be diagnosed by the task recovery policy" }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function createSessionState(now = Date.now()) {
|
|
125
|
+
return {
|
|
126
|
+
signatures: new Map(),
|
|
127
|
+
lastWorkspaceSignal: null,
|
|
128
|
+
lastEvidenceKey: null,
|
|
129
|
+
noProgressCalls: 0,
|
|
130
|
+
loopBlocked: false,
|
|
131
|
+
lastProgressAt: now,
|
|
132
|
+
activeCalls: new Set(),
|
|
133
|
+
compactionAt: null,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function createRuntimeGuard(options = {}) {
|
|
138
|
+
const duplicateLimit = Math.max(2, Number(options.duplicateLimit || 3))
|
|
139
|
+
const loopLimit = Math.max(4, Number(options.loopLimit || 6))
|
|
140
|
+
const sessions = new Map()
|
|
141
|
+
|
|
142
|
+
function stateFor(sessionID, at = Date.now()) {
|
|
143
|
+
const key = String(sessionID || "global")
|
|
144
|
+
if (!sessions.has(key)) sessions.set(key, createSessionState(at))
|
|
145
|
+
return sessions.get(key)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function resetForWorkspace(state, workspaceSignal) {
|
|
149
|
+
if (state.lastWorkspaceSignal === null || state.lastWorkspaceSignal === workspaceSignal) return
|
|
150
|
+
state.signatures.clear()
|
|
151
|
+
state.noProgressCalls = 0
|
|
152
|
+
state.loopBlocked = false
|
|
153
|
+
state.lastEvidenceKey = null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
before(input = {}) {
|
|
158
|
+
const at = Number(input.now || Date.now())
|
|
159
|
+
const state = stateFor(input.sessionID, at)
|
|
160
|
+
const workspaceSignal = String(input.workspaceSignal || "")
|
|
161
|
+
resetForWorkspace(state, workspaceSignal)
|
|
162
|
+
state.lastWorkspaceSignal = workspaceSignal
|
|
163
|
+
|
|
164
|
+
const tool = String(input.tool || "")
|
|
165
|
+
if (isExplorationTool(tool)) {
|
|
166
|
+
const signature = stableRuntimeHash({ tool, input: input.input || {}, cwd: input.cwd || "", workspaceSignal })
|
|
167
|
+
const seen = state.signatures.get(signature) || { count: 0, lastResultHash: null }
|
|
168
|
+
seen.count += 1
|
|
169
|
+
state.signatures.set(signature, seen)
|
|
170
|
+
|
|
171
|
+
if (state.loopBlocked) {
|
|
172
|
+
return {
|
|
173
|
+
blocked: true,
|
|
174
|
+
code: "UES_LOOP_DETECTED",
|
|
175
|
+
message: `UES loop guard blocked repeated exploration after ${state.noProgressCalls} no-progress calls. Choose a deterministic next action: edit the scoped target, run focused verification, inspect a direct caller/failing stack, change hypothesis, or escalate recovery.`,
|
|
176
|
+
signature,
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (seen.count > duplicateLimit) {
|
|
180
|
+
return {
|
|
181
|
+
blocked: true,
|
|
182
|
+
code: "UES_DUPLICATE_TOOL",
|
|
183
|
+
message: `UES duplicate-tool guard blocked ${tool}: equivalent arguments were already executed ${seen.count - 1} times with no workspace change. Use the previous evidence or change the query/scope before retrying.`,
|
|
184
|
+
signature,
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
state.lastProgressAt = at
|
|
190
|
+
if (input.callID) state.activeCalls.add(String(input.callID))
|
|
191
|
+
return { blocked: false }
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
after(input = {}) {
|
|
195
|
+
const at = Number(input.now || Date.now())
|
|
196
|
+
const state = stateFor(input.sessionID, at)
|
|
197
|
+
const workspaceSignal = String(input.workspaceSignal || "")
|
|
198
|
+
const tool = String(input.tool || "")
|
|
199
|
+
if (input.callID) state.activeCalls.delete(String(input.callID))
|
|
200
|
+
resetForWorkspace(state, workspaceSignal)
|
|
201
|
+
|
|
202
|
+
if (input.status === "completed" && isExplorationTool(tool)) {
|
|
203
|
+
const resultHash = stableRuntimeHash(input.result || "")
|
|
204
|
+
const evidenceKey = stableRuntimeHash({ workspaceSignal, resultHash })
|
|
205
|
+
if (state.lastEvidenceKey === evidenceKey) {
|
|
206
|
+
state.noProgressCalls += 1
|
|
207
|
+
} else {
|
|
208
|
+
state.lastEvidenceKey = evidenceKey
|
|
209
|
+
state.noProgressCalls = 0
|
|
210
|
+
}
|
|
211
|
+
if (state.noProgressCalls >= loopLimit) state.loopBlocked = true
|
|
212
|
+
|
|
213
|
+
const signature = stableRuntimeHash({ tool, input: input.input || {}, cwd: input.cwd || "", workspaceSignal })
|
|
214
|
+
const seen = state.signatures.get(signature)
|
|
215
|
+
if (seen) seen.lastResultHash = resultHash
|
|
216
|
+
} else if (
|
|
217
|
+
input.status === "completed" &&
|
|
218
|
+
(VERIFY_OR_WRITE_TOOL.test(tool) || state.lastWorkspaceSignal !== workspaceSignal)
|
|
219
|
+
) {
|
|
220
|
+
state.noProgressCalls = 0
|
|
221
|
+
state.loopBlocked = false
|
|
222
|
+
state.lastEvidenceKey = null
|
|
223
|
+
state.signatures.clear()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
state.lastWorkspaceSignal = workspaceSignal
|
|
227
|
+
state.lastProgressAt = at
|
|
228
|
+
return {
|
|
229
|
+
loopBlocked: state.loopBlocked,
|
|
230
|
+
noProgressCalls: state.noProgressCalls,
|
|
231
|
+
lastProgressAt: state.lastProgressAt,
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
touch(sessionID, at = Date.now()) {
|
|
236
|
+
const state = stateFor(sessionID, at)
|
|
237
|
+
state.lastProgressAt = Number(at)
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
compacted(sessionID, at = Date.now()) {
|
|
241
|
+
const state = stateFor(sessionID, at)
|
|
242
|
+
state.compactionAt = Number(at)
|
|
243
|
+
state.lastProgressAt = Number(at)
|
|
244
|
+
state.signatures.clear()
|
|
245
|
+
state.noProgressCalls = 0
|
|
246
|
+
state.loopBlocked = false
|
|
247
|
+
state.lastEvidenceKey = null
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
snapshot(sessionID) {
|
|
251
|
+
const state = stateFor(sessionID)
|
|
252
|
+
return {
|
|
253
|
+
noProgressCalls: state.noProgressCalls,
|
|
254
|
+
loopBlocked: state.loopBlocked,
|
|
255
|
+
lastProgressAt: state.lastProgressAt,
|
|
256
|
+
activeToolCalls: state.activeCalls.size,
|
|
257
|
+
compactionAt: state.compactionAt,
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
clear(sessionID) {
|
|
262
|
+
sessions.delete(String(sessionID || "global"))
|
|
263
|
+
},
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -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
|
+
}
|