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.
Files changed (68) hide show
  1. package/CHANGELOG.md +112 -3
  2. package/README.md +396 -281
  3. package/bin/ocskill.mjs +382 -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 +78 -160
  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 +784 -37
  33. package/global-config/plugins/ues-router/router.js +175 -23
  34. package/global-config/plugins/ues-router/runtime-guard.js +265 -0
  35. package/global-config/skills/engineering-orchestrator/references/long-horizon.md +6 -4
  36. package/lib/aci.mjs +128 -0
  37. package/lib/benchmark-confidence.mjs +173 -0
  38. package/lib/cli-utils.mjs +41 -0
  39. package/lib/container-sandbox.mjs +102 -0
  40. package/lib/context-manifest.mjs +300 -22
  41. package/lib/control-center.mjs +36 -3
  42. package/lib/eval-ablation.mjs +104 -0
  43. package/lib/eval-order.mjs +9 -0
  44. package/lib/eval-report.mjs +11 -0
  45. package/lib/eval-telemetry.mjs +8 -2
  46. package/lib/gate-receipt.mjs +52 -0
  47. package/lib/installer.mjs +39 -25
  48. package/lib/learning-engine.mjs +236 -38
  49. package/lib/model-policy.mjs +6 -0
  50. package/lib/opencode-compat.mjs +25 -10
  51. package/lib/orchestrator-policy.mjs +195 -21
  52. package/lib/process-runner.mjs +30 -9
  53. package/lib/runtime-events.mjs +31 -0
  54. package/lib/semantic-index.mjs +318 -0
  55. package/lib/task-engine.mjs +557 -28
  56. package/lib/trajectory.mjs +89 -0
  57. package/lib/windows-shim.mjs +227 -0
  58. package/lib/worktree-sandbox.mjs +85 -3
  59. package/package.json +11 -4
  60. package/scripts/check-release-tag.mjs +22 -0
  61. package/scripts/control-center.mjs +25 -0
  62. package/scripts/eval-ablation.mjs +44 -0
  63. package/scripts/eval-live.mjs +39 -58
  64. package/scripts/eval-matrix.mjs +166 -0
  65. package/scripts/smoke-packed-install.mjs +138 -4
  66. package/scripts/smoke-plain-install.mjs +91 -0
  67. package/scripts/validate-live-suite.mjs +3 -3
  68. 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))
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
- 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")
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 (/(database|migration|sql|query|index|transaction|schema changes?)/.test(value)) {
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
 
@@ -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`. `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
+ }