opencode-agent-skill 7.7.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 (206) hide show
  1. package/CHANGELOG.md +163 -0
  2. package/LICENSE +9 -0
  3. package/README.md +581 -0
  4. package/bin/ocskill.mjs +975 -0
  5. package/docs/DETERMINISTIC-TOOLS.md +88 -0
  6. package/docs/ENGINEERING-DESIGN.md +176 -0
  7. package/docs/EVALS.md +136 -0
  8. package/docs/NPM-PUBLISH.md +102 -0
  9. package/docs/OPENCODE-COMPAT.md +109 -0
  10. package/docs/RESEARCH-SOURCES.md +37 -0
  11. package/docs/TRACE-SCHEMA.md +109 -0
  12. package/docs/V7-INTELLIGENCE-RUNTIME.md +166 -0
  13. package/evals/live/fixtures/engineering-bench/package.json +1 -0
  14. package/evals/live/fixtures/engineering-bench/src/api-errors.mjs +3 -0
  15. package/evals/live/fixtures/engineering-bench/src/authz.mjs +3 -0
  16. package/evals/live/fixtures/engineering-bench/src/cache-tags.mjs +3 -0
  17. package/evals/live/fixtures/engineering-bench/src/config.mjs +3 -0
  18. package/evals/live/fixtures/engineering-bench/src/contract-consumer.mjs +6 -0
  19. package/evals/live/fixtures/engineering-bench/src/contract-producer.mjs +3 -0
  20. package/evals/live/fixtures/engineering-bench/src/dedupe.mjs +3 -0
  21. package/evals/live/fixtures/engineering-bench/src/dependency.mjs +3 -0
  22. package/evals/live/fixtures/engineering-bench/src/discount.mjs +3 -0
  23. package/evals/live/fixtures/engineering-bench/src/inventory.mjs +3 -0
  24. package/evals/live/fixtures/engineering-bench/src/migration.mjs +3 -0
  25. package/evals/live/fixtures/engineering-bench/src/money.mjs +3 -0
  26. package/evals/live/fixtures/engineering-bench/src/pagination.mjs +5 -0
  27. package/evals/live/fixtures/engineering-bench/src/path-safe.mjs +5 -0
  28. package/evals/live/fixtures/engineering-bench/src/payment.mjs +5 -0
  29. package/evals/live/fixtures/engineering-bench/src/query-sort.mjs +3 -0
  30. package/evals/live/fixtures/engineering-bench/src/react-state.mjs +8 -0
  31. package/evals/live/fixtures/engineering-bench/src/retry.mjs +3 -0
  32. package/evals/live/fixtures/engineering-bench/src/rn-platform.mjs +3 -0
  33. package/evals/live/fixtures/engineering-bench/src/upload.mjs +3 -0
  34. package/evals/live/fixtures/engineering-bench/src/webhook.mjs +3 -0
  35. package/evals/live/graders/engineering-bench.mjs +239 -0
  36. package/evals/live/tasks.json +126 -0
  37. package/evals/long/fixtures/long-horizon/package.json +1 -0
  38. package/evals/long/fixtures/long-horizon/src/auth.mjs +5 -0
  39. package/evals/long/fixtures/long-horizon/src/checkout.mjs +9 -0
  40. package/evals/long/fixtures/long-horizon/src/inventory.mjs +5 -0
  41. package/evals/long/fixtures/long-horizon/src/money.mjs +3 -0
  42. package/evals/long/fixtures/long-horizon/src/payment.mjs +7 -0
  43. package/evals/long/fixtures/long-horizon/src/product-api.mjs +10 -0
  44. package/evals/long/fixtures/long-horizon/src/product-cache.mjs +3 -0
  45. package/evals/long/fixtures/long-horizon/src/product-service.mjs +6 -0
  46. package/evals/long/fixtures/long-horizon/src/product-state.mjs +8 -0
  47. package/evals/long/fixtures/long-horizon/src/project-api.mjs +9 -0
  48. package/evals/long/fixtures/long-horizon/src/project-service.mjs +7 -0
  49. package/evals/long/fixtures/long-horizon/src/user-consumer.mjs +3 -0
  50. package/evals/long/fixtures/long-horizon/src/user-migration.mjs +7 -0
  51. package/evals/long/fixtures/long-horizon/src/user-serializer.mjs +3 -0
  52. package/evals/long/fixtures/long-horizon/src/user-validation.mjs +3 -0
  53. package/evals/long/graders/long-horizon.mjs +209 -0
  54. package/evals/long/tasks.json +36 -0
  55. package/evals/router-triggers.json +1238 -0
  56. package/evals/routing.json +321 -0
  57. package/global-config/AGENTS.md +212 -0
  58. package/global-config/agents/architect.md +38 -0
  59. package/global-config/agents/codebase-mapper.md +41 -0
  60. package/global-config/agents/critic.md +37 -0
  61. package/global-config/agents/debugger.md +36 -0
  62. package/global-config/agents/executor.md +48 -0
  63. package/global-config/agents/integration-verifier.md +43 -0
  64. package/global-config/agents/plan-checker.md +43 -0
  65. package/global-config/agents/researcher.md +30 -0
  66. package/global-config/agents/reviewer.md +30 -0
  67. package/global-config/agents/verifier.md +33 -0
  68. package/global-config/commands/audit.md +8 -0
  69. package/global-config/commands/critique.md +8 -0
  70. package/global-config/commands/debug.md +8 -0
  71. package/global-config/commands/feature.md +8 -0
  72. package/global-config/commands/fix.md +8 -0
  73. package/global-config/commands/plan.md +8 -0
  74. package/global-config/commands/research.md +8 -0
  75. package/global-config/commands/resume.md +22 -0
  76. package/global-config/commands/review.md +8 -0
  77. package/global-config/commands/run.md +29 -0
  78. package/global-config/commands/verify.md +8 -0
  79. package/global-config/plugins/ues-router/capabilities.js +20 -0
  80. package/global-config/plugins/ues-router/index.js +277 -0
  81. package/global-config/plugins/ues-router/router.js +74 -0
  82. package/global-config/plugins/ues-router/safety.js +16 -0
  83. package/global-config/skills/accessibility/SKILL.md +10 -0
  84. package/global-config/skills/accessibility/references/workflow.md +17 -0
  85. package/global-config/skills/api-contract/SKILL.md +12 -0
  86. package/global-config/skills/api-contract/references/workflow.md +17 -0
  87. package/global-config/skills/auth-security/SKILL.md +12 -0
  88. package/global-config/skills/auth-security/references/workflow.md +15 -0
  89. package/global-config/skills/bug-diagnosis/SKILL.md +28 -0
  90. package/global-config/skills/change-impact-analysis/SKILL.md +23 -0
  91. package/global-config/skills/code-review/SKILL.md +19 -0
  92. package/global-config/skills/context-engineering/SKILL.md +18 -0
  93. package/global-config/skills/context-engineering/references/large-repo.md +16 -0
  94. package/global-config/skills/database-engineering/SKILL.md +12 -0
  95. package/global-config/skills/database-engineering/references/workflow.md +15 -0
  96. package/global-config/skills/dependency-management/SKILL.md +12 -0
  97. package/global-config/skills/dependency-management/references/workflow.md +14 -0
  98. package/global-config/skills/devops-engineering/SKILL.md +10 -0
  99. package/global-config/skills/devops-engineering/references/workflow.md +11 -0
  100. package/global-config/skills/django-engineering/SKILL.md +10 -0
  101. package/global-config/skills/django-engineering/references/workflow.md +11 -0
  102. package/global-config/skills/documentation-engineering/SKILL.md +10 -0
  103. package/global-config/skills/documentation-engineering/references/workflow.md +15 -0
  104. package/global-config/skills/dotnet-engineering/SKILL.md +10 -0
  105. package/global-config/skills/dotnet-engineering/references/workflow.md +11 -0
  106. package/global-config/skills/ecommerce-engineering/SKILL.md +10 -0
  107. package/global-config/skills/ecommerce-engineering/references/workflow.md +17 -0
  108. package/global-config/skills/engineering-orchestrator/SKILL.md +29 -0
  109. package/global-config/skills/engineering-orchestrator/references/delegation.md +22 -0
  110. package/global-config/skills/engineering-orchestrator/references/evaluator-loop.md +18 -0
  111. package/global-config/skills/engineering-orchestrator/references/long-horizon.md +57 -0
  112. package/global-config/skills/engineering-orchestrator/references/model-escalation.md +19 -0
  113. package/global-config/skills/engineering-orchestrator/references/retry-policy.md +12 -0
  114. package/global-config/skills/engineering-orchestrator/references/routing.md +39 -0
  115. package/global-config/skills/engineering-orchestrator/references/verification-matrix.md +18 -0
  116. package/global-config/skills/fastapi-engineering/SKILL.md +10 -0
  117. package/global-config/skills/fastapi-engineering/references/workflow.md +13 -0
  118. package/global-config/skills/file-upload-engineering/SKILL.md +10 -0
  119. package/global-config/skills/file-upload-engineering/references/workflow.md +17 -0
  120. package/global-config/skills/flutter-engineering/SKILL.md +10 -0
  121. package/global-config/skills/flutter-engineering/references/workflow.md +13 -0
  122. package/global-config/skills/git-safety/SKILL.md +10 -0
  123. package/global-config/skills/git-safety/references/workflow.md +15 -0
  124. package/global-config/skills/implementation-engineer/SKILL.md +10 -0
  125. package/global-config/skills/implementation-engineer/references/workflow.md +14 -0
  126. package/global-config/skills/java-spring-engineering/SKILL.md +10 -0
  127. package/global-config/skills/java-spring-engineering/references/workflow.md +13 -0
  128. package/global-config/skills/long-task-state/SKILL.md +28 -0
  129. package/global-config/skills/long-task-state/references/context-ledger.md +27 -0
  130. package/global-config/skills/long-task-state/templates/STATE.md +48 -0
  131. package/global-config/skills/nestjs-engineering/SKILL.md +10 -0
  132. package/global-config/skills/nestjs-engineering/references/workflow.md +11 -0
  133. package/global-config/skills/nextjs-engineering/SKILL.md +12 -0
  134. package/global-config/skills/nextjs-engineering/references/workflow.md +15 -0
  135. package/global-config/skills/nodejs-engineering/SKILL.md +12 -0
  136. package/global-config/skills/nodejs-engineering/references/workflow.md +11 -0
  137. package/global-config/skills/payment-engineering/SKILL.md +12 -0
  138. package/global-config/skills/payment-engineering/references/workflow.md +19 -0
  139. package/global-config/skills/performance-engineering/SKILL.md +10 -0
  140. package/global-config/skills/performance-engineering/references/workflow.md +17 -0
  141. package/global-config/skills/python-engineering/SKILL.md +10 -0
  142. package/global-config/skills/python-engineering/references/workflow.md +11 -0
  143. package/global-config/skills/react-engineering/SKILL.md +14 -0
  144. package/global-config/skills/react-engineering/references/workflow.md +16 -0
  145. package/global-config/skills/react-native-engineering/SKILL.md +14 -0
  146. package/global-config/skills/react-native-engineering/references/workflow.md +16 -0
  147. package/global-config/skills/repo-explorer/SKILL.md +17 -0
  148. package/global-config/skills/research-verification/SKILL.md +18 -0
  149. package/global-config/skills/research-verification/references/source-hierarchy.md +12 -0
  150. package/global-config/skills/rest-api-design/SKILL.md +10 -0
  151. package/global-config/skills/rest-api-design/references/workflow.md +18 -0
  152. package/global-config/skills/software-architect/SKILL.md +10 -0
  153. package/global-config/skills/software-architect/references/workflow.md +18 -0
  154. package/global-config/skills/task-planner/SKILL.md +21 -0
  155. package/global-config/skills/task-planner/references/plan-schema.md +56 -0
  156. package/global-config/skills/test-driven-development/SKILL.md +22 -0
  157. package/global-config/skills/test-driven-development/references/writing-good-tests.md +21 -0
  158. package/global-config/skills/test-verification/SKILL.md +20 -0
  159. package/global-config/skills/ui-ux-engineering/SKILL.md +10 -0
  160. package/global-config/skills/ui-ux-engineering/references/workflow.md +17 -0
  161. package/global-config/skills/web-security-review/SKILL.md +10 -0
  162. package/global-config/skills/web-security-review/references/workflow.md +22 -0
  163. package/lib/context-manifest.mjs +101 -0
  164. package/lib/control-center.mjs +148 -0
  165. package/lib/eval-auth.mjs +21 -0
  166. package/lib/eval-report.mjs +72 -0
  167. package/lib/eval-telemetry.mjs +155 -0
  168. package/lib/evidence-receipt.mjs +50 -0
  169. package/lib/hermes-bridge.mjs +28 -0
  170. package/lib/ids.mjs +21 -0
  171. package/lib/installer.mjs +636 -0
  172. package/lib/learning-engine.mjs +161 -0
  173. package/lib/model-config.mjs +88 -0
  174. package/lib/model-policy.mjs +71 -0
  175. package/lib/opencode-compat.mjs +86 -0
  176. package/lib/orchestrator-policy.mjs +35 -0
  177. package/lib/process-runner.mjs +117 -0
  178. package/lib/repo-graph.mjs +135 -0
  179. package/lib/repo-inspect.mjs +264 -0
  180. package/lib/review-scope.mjs +98 -0
  181. package/lib/router-config.mjs +40 -0
  182. package/lib/task-engine.mjs +777 -0
  183. package/lib/task-graph.mjs +262 -0
  184. package/lib/update-resolver.mjs +43 -0
  185. package/lib/verification-plan.mjs +52 -0
  186. package/lib/version.mjs +47 -0
  187. package/lib/workspace-snapshot.mjs +45 -0
  188. package/lib/worktree-sandbox.mjs +59 -0
  189. package/package.json +69 -0
  190. package/scripts/check-working-tree.mjs +2 -0
  191. package/scripts/collect-evidence.mjs +2 -0
  192. package/scripts/control-center.mjs +37 -0
  193. package/scripts/detect-stack.mjs +2 -0
  194. package/scripts/detect-test-commands.mjs +2 -0
  195. package/scripts/eval-live.mjs +437 -0
  196. package/scripts/eval-report.mjs +49 -0
  197. package/scripts/eval-router.mjs +45 -0
  198. package/scripts/eval-skills.mjs +71 -0
  199. package/scripts/impact-map.mjs +4 -0
  200. package/scripts/install.mjs +61 -0
  201. package/scripts/repo-map.mjs +2 -0
  202. package/scripts/smoke-packed-install.mjs +326 -0
  203. package/scripts/syntax-check.mjs +35 -0
  204. package/scripts/uninstall.mjs +21 -0
  205. package/scripts/validate-live-suite.mjs +65 -0
  206. package/scripts/validate.mjs +133 -0
@@ -0,0 +1,161 @@
1
+ import { existsSync } from "node:fs"
2
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"
3
+ import { createHash } from "node:crypto"
4
+ import path from "node:path"
5
+
6
+ const LEARNING_DIR = ".ues-learning"
7
+ const STATE_FILE = "LEARNINGS.json"
8
+
9
+ function idFor(type, key) {
10
+ return createHash("sha256").update(type + ":" + key).digest("hex").slice(0, 16)
11
+ }
12
+
13
+ async function walkJson(dir, limit = 500) {
14
+ const files = []
15
+ async function visit(current) {
16
+ if (files.length >= limit) return
17
+ const entries = await readdir(current, { withFileTypes: true }).catch(() => [])
18
+ for (const entry of entries) {
19
+ if (files.length >= limit) break
20
+ const full = path.join(current, entry.name)
21
+ if (entry.isDirectory()) await visit(full)
22
+ else if (entry.isFile() && entry.name.endsWith(".json")) files.push(full)
23
+ }
24
+ }
25
+ await visit(dir)
26
+ return files
27
+ }
28
+
29
+ function proposal(type, key, title, evidence, recommendation) {
30
+ return { id: idFor(type, key), type, key, title, evidence, recommendation, status: "proposed" }
31
+ }
32
+
33
+ export async function analyzeEvalTraces(evalDir) {
34
+ const files = await walkJson(path.resolve(evalDir))
35
+ const results = []
36
+ for (const file of files) {
37
+ try {
38
+ const payload = JSON.parse(await readFile(file, "utf8"))
39
+ for (const item of payload.results || []) results.push({ ...item, source: file })
40
+ } catch {}
41
+ }
42
+
43
+ const counters = new Map()
44
+ const bump = (key, item) => {
45
+ const value = counters.get(key) || { count: 0, tasks: new Set(), modes: new Set() }
46
+ value.count += 1
47
+ if (item.task) value.tasks.add(item.task)
48
+ if (item.mode) value.modes.add(item.mode)
49
+ counters.set(key, value)
50
+ }
51
+
52
+ for (const item of results) {
53
+ if (item.timedOut) bump("hard-timeout", item)
54
+ if (item.idleTimedOut) bump("idle-timeout", item)
55
+ if (Number(item.agentExit) !== 0) bump("agent-exit", item)
56
+ if (Number(item.graderExit) !== 0) bump("grader-failure", item)
57
+ if (item.orchestration?.required && !item.orchestration?.valid) bump("orchestration-failure", item)
58
+ if ((item.telemetry?.parseErrors || 0) > 0) bump("telemetry-parse-errors", item)
59
+ }
60
+
61
+ const proposals = []
62
+ for (const [key, value] of counters) {
63
+ if (value.count < 1) continue
64
+ const details = {
65
+ count: value.count,
66
+ tasks: [...value.tasks].sort(),
67
+ modes: [...value.modes].sort(),
68
+ samples: results.length,
69
+ }
70
+ const recommendations = {
71
+ "hard-timeout": "Inspect task decomposition and model/runtime latency; shorten tasks or raise a justified hard timeout.",
72
+ "idle-timeout": "Inspect tool/session deadlocks and add progress-producing boundaries or recovery.",
73
+ "agent-exit": "Classify process/provider failures before retrying; avoid blind model escalation.",
74
+ "grader-failure": "Mine failing task contracts for missing domain guidance or context selection, then add a regression before changing skills.",
75
+ "orchestration-failure": "Strengthen durable workflow instructions/gates or runtime support; do not count direct patches as UES success.",
76
+ "telemetry-parse-errors": "Update telemetry parsing against the observed OpenCode event shape with a fixture-backed test.",
77
+ }
78
+ proposals.push(proposal(
79
+ "eval-pattern",
80
+ key,
81
+ "Recurring evaluation pattern: " + key,
82
+ details,
83
+ recommendations[key] || "Review the repeated pattern and add a regression before promoting a reusable lesson.",
84
+ ))
85
+ }
86
+
87
+ return {
88
+ schemaVersion: 1,
89
+ analyzedAt: new Date().toISOString(),
90
+ files: files.length,
91
+ results: results.length,
92
+ proposals: proposals.sort((a, b) => b.evidence.count - a.evidence.count || a.key.localeCompare(b.key)),
93
+ }
94
+ }
95
+
96
+ export function learningFile(root) {
97
+ return path.join(path.resolve(root), LEARNING_DIR, STATE_FILE)
98
+ }
99
+
100
+ export async function readLearningState(root) {
101
+ const file = learningFile(root)
102
+ if (!existsSync(file)) return { schemaVersion: 1, updatedAt: null, proposals: [], accepted: [] }
103
+ try {
104
+ const parsed = JSON.parse(await readFile(file, "utf8"))
105
+ return {
106
+ schemaVersion: 1,
107
+ updatedAt: parsed.updatedAt || null,
108
+ proposals: Array.isArray(parsed.proposals) ? parsed.proposals : [],
109
+ accepted: Array.isArray(parsed.accepted) ? parsed.accepted : [],
110
+ }
111
+ } catch {
112
+ return { schemaVersion: 1, updatedAt: null, proposals: [], accepted: [], invalid: true }
113
+ }
114
+ }
115
+
116
+ export async function saveLearningAnalysis(root, analysis) {
117
+ const dir = path.dirname(learningFile(root))
118
+ await mkdir(dir, { recursive: true })
119
+ const current = await readLearningState(root)
120
+ const acceptedIDs = new Set(current.accepted.map((item) => item.id))
121
+ const proposals = analysis.proposals.filter((item) => !acceptedIDs.has(item.id))
122
+ const next = {
123
+ schemaVersion: 1,
124
+ updatedAt: new Date().toISOString(),
125
+ proposals,
126
+ accepted: current.accepted,
127
+ }
128
+ await writeFile(learningFile(root), JSON.stringify(next, null, 2) + "\n", "utf8")
129
+ return next
130
+ }
131
+
132
+ export async function acceptLearning(root, id) {
133
+ const current = await readLearningState(root)
134
+ const item = current.proposals.find((entry) => entry.id === id)
135
+ if (!item) throw new Error("unknown learning proposal: " + id)
136
+ const accepted = { ...item, status: "accepted", acceptedAt: new Date().toISOString() }
137
+ const next = {
138
+ schemaVersion: 1,
139
+ updatedAt: accepted.acceptedAt,
140
+ proposals: current.proposals.filter((entry) => entry.id !== id),
141
+ accepted: [...current.accepted.filter((entry) => entry.id !== id), accepted],
142
+ }
143
+ await mkdir(path.dirname(learningFile(root)), { recursive: true })
144
+ await writeFile(learningFile(root), JSON.stringify(next, null, 2) + "\n", "utf8")
145
+ return accepted
146
+ }
147
+
148
+ export async function relevantAcceptedLearnings(root, text, limit = 5) {
149
+ const state = await readLearningState(root)
150
+ const terms = new Set(String(text || "").toLowerCase().split(/[^a-z0-9_-]+/).filter((term) => term.length >= 4))
151
+ return state.accepted
152
+ .map((item) => {
153
+ const haystack = JSON.stringify(item).toLowerCase()
154
+ const score = [...terms].reduce((sum, term) => sum + (haystack.includes(term) ? 1 : 0), 0)
155
+ return { item, score }
156
+ })
157
+ .filter((entry) => entry.score > 0)
158
+ .sort((a, b) => b.score - a.score)
159
+ .slice(0, limit)
160
+ .map((entry) => entry.item)
161
+ }
@@ -0,0 +1,88 @@
1
+ import { existsSync } from "node:fs"
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises"
3
+ import path from "node:path"
4
+ import { defaultModelPolicy, resolveModel } from "./model-policy.mjs"
5
+
6
+ const TIERS = new Set(["light", "standard", "heavy"])
7
+
8
+ function validModelID(value) {
9
+ return typeof value === "string" && /^[^\s/]+\/[^\s#]+(?:#[^\s]+)?$/.test(value)
10
+ }
11
+
12
+ function normalize(policy) {
13
+ const base = defaultModelPolicy()
14
+ const input = policy && typeof policy === "object" ? policy : {}
15
+ const tiers = { ...base.tiers }
16
+ for (const tier of TIERS) {
17
+ const value = input.tiers?.[tier]
18
+ if (value === null || validModelID(value)) tiers[tier] = value
19
+ }
20
+
21
+ const roleTiers = { ...base.roleTiers }
22
+ for (const [role, tier] of Object.entries(input.roleTiers || {})) {
23
+ if (TIERS.has(tier)) roleTiers[role] = tier
24
+ }
25
+
26
+ return {
27
+ schemaVersion: 1,
28
+ enabled: input.enabled === true,
29
+ maxEscalations: Number.isInteger(input.maxEscalations)
30
+ ? Math.max(0, Math.min(input.maxEscalations, 2))
31
+ : base.maxEscalations,
32
+ tiers,
33
+ roleTiers,
34
+ }
35
+ }
36
+
37
+ export function modelPolicyFile(configDir) {
38
+ return path.join(path.resolve(configDir), ".ues", "model-policy.json")
39
+ }
40
+
41
+ export async function readModelPolicy(configDir) {
42
+ const file = modelPolicyFile(configDir)
43
+ if (!existsSync(file)) return { ...defaultModelPolicy(), file }
44
+ try {
45
+ const parsed = JSON.parse(await readFile(file, "utf8"))
46
+ return { ...normalize(parsed), file }
47
+ } catch {
48
+ return { ...defaultModelPolicy(), file, invalid: true }
49
+ }
50
+ }
51
+
52
+ export async function writeModelPolicy(configDir, patch = {}) {
53
+ const current = await readModelPolicy(configDir)
54
+ const merged = normalize({
55
+ ...current,
56
+ ...patch,
57
+ tiers: { ...current.tiers, ...(patch.tiers || {}) },
58
+ roleTiers: { ...current.roleTiers, ...(patch.roleTiers || {}) },
59
+ })
60
+ const file = modelPolicyFile(configDir)
61
+ await mkdir(path.dirname(file), { recursive: true })
62
+ await writeFile(file, JSON.stringify(merged, null, 2) + "\n", "utf8")
63
+ return { ...merged, file }
64
+ }
65
+
66
+ export function validateModelID(value) {
67
+ return validModelID(value)
68
+ }
69
+
70
+ export function applyConfiguredModel(source, role, policy) {
71
+ if (!policy?.enabled) return source
72
+ const resolved = resolveModel(role, 1, policy)
73
+ if (!resolved.model) return source
74
+
75
+ const lines = String(source).split(/\r?\n/)
76
+ const start = lines.indexOf("---")
77
+ const end = lines.indexOf("---", start + 1)
78
+ if (start < 0 || end < 0) return source
79
+
80
+ const existing = lines.findIndex((line, index) => index > start && index < end && /^model:\s*/.test(line))
81
+ if (existing >= 0) {
82
+ lines[existing] = "model: " + resolved.model
83
+ } else {
84
+ const mode = lines.findIndex((line, index) => index > start && index < end && /^mode:\s*/.test(line))
85
+ lines.splice(mode >= 0 ? mode + 1 : start + 1, 0, "model: " + resolved.model)
86
+ }
87
+ return lines.join("\n")
88
+ }
@@ -0,0 +1,71 @@
1
+ const ROLE_TIERS = {
2
+ "codebase-mapper": "standard",
3
+ architect: "heavy",
4
+ "plan-checker": "heavy",
5
+ executor: "standard",
6
+ debugger: "standard",
7
+ researcher: "standard",
8
+ reviewer: "standard",
9
+ critic: "heavy",
10
+ verifier: "standard",
11
+ "integration-verifier": "heavy",
12
+ }
13
+
14
+ const ORDER = ["light", "standard", "heavy"]
15
+
16
+ function tierIndex(value) {
17
+ const index = ORDER.indexOf(value)
18
+ return index < 0 ? 1 : index
19
+ }
20
+
21
+ export function defaultTier(role) {
22
+ return ROLE_TIERS[role] || "standard"
23
+ }
24
+
25
+ export function resolveTier(role, attempt = 1, options = {}) {
26
+ const base = options.baseTier || defaultTier(role)
27
+ const index = Math.max(0, ORDER.indexOf(base))
28
+ const maxEscalations = Number.isInteger(options.maxEscalations) ? Math.max(0, options.maxEscalations) : 2
29
+ const escalation = Math.min(Math.max(0, Number(attempt || 1) - 1), maxEscalations)
30
+ return ORDER[Math.min(ORDER.length - 1, index + escalation)]
31
+ }
32
+
33
+ export function resolveModel(role, attempt, config = {}) {
34
+ const tier = resolveTier(role, attempt, {
35
+ baseTier: config.roleTiers?.[role],
36
+ maxEscalations: config.maxEscalations,
37
+ })
38
+ const model = config.tiers?.[tier] || null
39
+ return { role, attempt: Number(attempt || 1), tier, model }
40
+ }
41
+
42
+ export function defaultModelPolicy() {
43
+ return {
44
+ schemaVersion: 1,
45
+ enabled: false,
46
+ maxEscalations: 2,
47
+ tiers: { light: null, standard: null, heavy: null },
48
+ roleTiers: { ...ROLE_TIERS },
49
+ }
50
+ }
51
+
52
+
53
+ export function resolveAdaptiveModel(role, attempt, taskPolicy = {}, config = {}) {
54
+ const configuredBase = config.roleTiers?.[role] || defaultTier(role)
55
+ const policyBase = taskPolicy.modelTier || configuredBase
56
+ const baseTier = ORDER[Math.max(tierIndex(configuredBase), tierIndex(policyBase))]
57
+ const resolved = resolveModel(role, attempt, {
58
+ ...config,
59
+ roleTiers: { ...(config.roleTiers || {}), [role]: baseTier },
60
+ })
61
+ return {
62
+ ...resolved,
63
+ policy: {
64
+ mode: taskPolicy.mode || null,
65
+ risk: taskPolicy.risk || null,
66
+ score: Number(taskPolicy.score || 0),
67
+ maxAttempts: Number(taskPolicy.maxAttempts || 0) || null,
68
+ contextBudget: Number(taskPolicy.contextBudget || 0) || null,
69
+ },
70
+ }
71
+ }
@@ -0,0 +1,86 @@
1
+ import { spawnSync } from "node:child_process"
2
+
3
+ export function parseOpenCodeMajor(value) {
4
+ const match = String(value || "").match(/(\d+)\.(\d+)\.(\d+)/)
5
+ return match ? Number(match[1]) : null
6
+ }
7
+
8
+ export function detectOpenCodeMajor() {
9
+ const forced = process.env.UES_OPENCODE_MAJOR
10
+ if (forced && /^\d+$/.test(forced)) return Number(forced)
11
+
12
+ const candidates = process.platform === "win32"
13
+ ? [
14
+ () => spawnSync(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", "opencode --version"], { encoding: "utf8" }),
15
+ () => spawnSync("opencode", ["--version"], { encoding: "utf8" }),
16
+ ]
17
+ : [() => spawnSync("opencode", ["--version"], { encoding: "utf8" })]
18
+
19
+ for (const run of candidates) {
20
+ const result = run()
21
+ if (result.status !== 0) continue
22
+ const major = parseOpenCodeMajor(result.stdout || result.stderr)
23
+ if (major !== null) return major
24
+ }
25
+ return 1
26
+ }
27
+
28
+ export function buildOpenCodeRunArgs({ major = 1, model, workspace, variant = null, prompt }) {
29
+ const args = ["run"]
30
+ if (Number(major) >= 2) args.push("--standalone")
31
+ args.push(
32
+ "--format",
33
+ "json",
34
+ "--auto",
35
+ "--agent",
36
+ "build",
37
+ "--model",
38
+ model,
39
+ "--dir",
40
+ workspace,
41
+ )
42
+ if (variant) args.push("--variant", variant)
43
+ args.push(prompt)
44
+ return args
45
+ }
46
+
47
+ export function adaptAgentForOpenCode(source, major) {
48
+ if (major < 2) return source
49
+
50
+ const lines = String(source).split(/\r?\n/)
51
+ const start = lines.findIndex((line) => /^permission:\s*$/.test(line))
52
+ if (start < 0) return source
53
+
54
+ const mappings = {
55
+ bash: "shell",
56
+ task: "subagent",
57
+ write: "edit",
58
+ }
59
+ const rules = []
60
+ const seen = new Set()
61
+ let end = start + 1
62
+
63
+ for (; end < lines.length; end += 1) {
64
+ const match = lines[end].match(/^\s{2}([A-Za-z0-9_-]+):\s*(allow|ask|deny)\s*$/)
65
+ if (!match) break
66
+ const action = mappings[match[1]] || match[1]
67
+ const effect = match[2]
68
+ const key = action + ":" + effect
69
+ if (seen.has(key)) continue
70
+ seen.add(key)
71
+ rules.push({ action, effect })
72
+ }
73
+
74
+ if (rules.length === 0) return source
75
+
76
+ const replacement = ["permissions:"]
77
+ for (const rule of rules) {
78
+ replacement.push(
79
+ " - action: " + rule.action,
80
+ ' resource: "*"',
81
+ " effect: " + rule.effect,
82
+ )
83
+ }
84
+
85
+ return [...lines.slice(0, start), ...replacement, ...lines.slice(end)].join("\n")
86
+ }
@@ -0,0 +1,35 @@
1
+ const HIGH_RISK = /(auth|security|permission|payment|migration|schema|database|production|deploy|public api|breaking|secret|credential)/i
2
+ const LONG = /(whole repo|whole repository|entire repo|entire project|long[- ]running|multi[- ]file|cross[- ]module|resume|migration|refactor all)/i
3
+
4
+ export function classifyEngineeringTask(text, facts = {}) {
5
+ const value = String(text || "")
6
+ let score = 0
7
+ if (value.length > 250) score += 1
8
+ if (value.length > 700) score += 1
9
+ if (HIGH_RISK.test(value)) score += 2
10
+ if (LONG.test(value)) score += 2
11
+ if (Number(facts.changedFiles || 0) > 5) score += 1
12
+ if (Number(facts.changedFiles || 0) > 12) score += 1
13
+ if (facts.hasMigration || facts.hasPublicContract) score += 2
14
+
15
+ const risk = HIGH_RISK.test(value) || facts.hasMigration || facts.hasPublicContract
16
+ ? "high"
17
+ : score >= 3 ? "medium" : "low"
18
+
19
+ const mode = score >= 4 ? "long-horizon" : score >= 2 ? "standard" : "inline"
20
+ const modelTier = risk === "high" || score >= 4 ? "heavy" : score >= 2 ? "standard" : "light"
21
+ const maxAttempts = risk === "high" ? 2 : 3
22
+ const contextBudget = mode === "long-horizon" ? 48_000 : mode === "standard" ? 32_000 : 16_000
23
+
24
+ return {
25
+ schemaVersion: 1,
26
+ score,
27
+ risk,
28
+ mode,
29
+ modelTier,
30
+ maxAttempts,
31
+ contextBudget,
32
+ requirePlanCheck: mode === "long-horizon" || risk === "high",
33
+ requireIntegrationVerification: mode !== "inline" || risk === "high",
34
+ }
35
+ }
@@ -0,0 +1,117 @@
1
+ import { spawn, spawnSync } from "node:child_process"
2
+
3
+ function appendBounded(current, chunk, limit) {
4
+ const next = current + String(chunk || "")
5
+ if (next.length <= limit) return next
6
+ return next.slice(next.length - limit)
7
+ }
8
+
9
+ function killTree(child) {
10
+ if (!child?.pid) return
11
+ if (process.platform === "win32") {
12
+ spawnSync("taskkill", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore" })
13
+ return
14
+ }
15
+ try {
16
+ process.kill(-child.pid, "SIGTERM")
17
+ } catch {
18
+ try { child.kill("SIGTERM") } catch {}
19
+ }
20
+ }
21
+
22
+ export function runProcess(executable, args = [], options = {}) {
23
+ const startedAt = Date.now()
24
+ const heartbeatMs = Math.max(0, Number(options.heartbeatMs ?? 30_000))
25
+ const timeoutMs = Math.max(0, Number(options.timeoutMs ?? 0))
26
+ const idleTimeoutMs = Math.max(0, Number(options.idleTimeoutMs ?? 0))
27
+ const maxBuffer = Math.max(1024, Number(options.maxBuffer ?? 4 * 1024 * 1024))
28
+
29
+ return new Promise((resolve) => {
30
+ let stdout = ""
31
+ let stderr = ""
32
+ let lastOutputAt = Date.now()
33
+ let timedOut = false
34
+ let idleTimedOut = false
35
+ let aborted = false
36
+ let finished = false
37
+
38
+ const child = spawn(executable, args, {
39
+ cwd: options.cwd,
40
+ env: options.env,
41
+ windowsHide: true,
42
+ detached: process.platform !== "win32",
43
+ stdio: ["ignore", "pipe", "pipe"],
44
+ })
45
+
46
+ const cleanup = () => {
47
+ clearInterval(heartbeatTimer)
48
+ clearInterval(watchdogTimer)
49
+ if (options.signal) options.signal.removeEventListener("abort", onAbort)
50
+ }
51
+
52
+ const finish = (status, signal, spawnError = null) => {
53
+ if (finished) return
54
+ finished = true
55
+ cleanup()
56
+ resolve({
57
+ status: Number.isInteger(status) ? status : (spawnError || signal || timedOut || idleTimedOut || aborted) ? 1 : 0,
58
+ signal: signal || null,
59
+ stdout,
60
+ stderr: spawnError ? appendBounded(stderr, String(spawnError.message || spawnError), maxBuffer) : stderr,
61
+ durationMs: Date.now() - startedAt,
62
+ timedOut,
63
+ idleTimedOut,
64
+ aborted,
65
+ })
66
+ }
67
+
68
+ child.stdout?.on("data", (chunk) => {
69
+ lastOutputAt = Date.now()
70
+ stdout = appendBounded(stdout, chunk, maxBuffer)
71
+ options.onStdout?.(String(chunk))
72
+ })
73
+ child.stderr?.on("data", (chunk) => {
74
+ lastOutputAt = Date.now()
75
+ stderr = appendBounded(stderr, chunk, maxBuffer)
76
+ options.onStderr?.(String(chunk))
77
+ })
78
+
79
+ child.on("error", (error) => finish(1, null, error))
80
+ child.on("close", (code, signal) => finish(code ?? 1, signal))
81
+
82
+ const heartbeatTimer = heartbeatMs > 0
83
+ ? setInterval(() => {
84
+ options.onHeartbeat?.({
85
+ pid: child.pid,
86
+ elapsedMs: Date.now() - startedAt,
87
+ idleMs: Date.now() - lastOutputAt,
88
+ })
89
+ }, heartbeatMs)
90
+ : null
91
+
92
+ const watchdogTimer = (timeoutMs > 0 || idleTimeoutMs > 0)
93
+ ? setInterval(() => {
94
+ const now = Date.now()
95
+ if (timeoutMs > 0 && now - startedAt >= timeoutMs) {
96
+ timedOut = true
97
+ killTree(child)
98
+ return
99
+ }
100
+ if (idleTimeoutMs > 0 && now - lastOutputAt >= idleTimeoutMs) {
101
+ idleTimedOut = true
102
+ killTree(child)
103
+ }
104
+ }, 500)
105
+ : null
106
+
107
+ function onAbort() {
108
+ aborted = true
109
+ killTree(child)
110
+ }
111
+
112
+ if (options.signal) {
113
+ if (options.signal.aborted) onAbort()
114
+ else options.signal.addEventListener("abort", onAbort, { once: true })
115
+ }
116
+ })
117
+ }
@@ -0,0 +1,135 @@
1
+ import { readFile, readdir, stat } from "node:fs/promises"
2
+ import path from "node:path"
3
+
4
+ const SKIP = new Set([
5
+ ".git", "node_modules", ".next", "dist", "build", "coverage", ".venv", "venv",
6
+ "Pods", "DerivedData", ".gradle", ".idea", ".cache", ".turbo", "target", "bin", "obj",
7
+ ".ues-work",
8
+ ])
9
+
10
+ const SOURCE_EXTENSIONS = new Set([
11
+ ".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx", ".py", ".java", ".kt", ".kts",
12
+ ".cs", ".go", ".rs", ".rb", ".php", ".vue", ".svelte",
13
+ ])
14
+
15
+ async function walk(root, maxFiles = 2500, maxDepth = 8) {
16
+ const files = []
17
+ async function visit(dir, depth) {
18
+ if (depth > maxDepth || files.length >= maxFiles) return
19
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => [])
20
+ for (const entry of entries) {
21
+ if (files.length >= maxFiles) break
22
+ if (SKIP.has(entry.name)) continue
23
+ const full = path.join(dir, entry.name)
24
+ if (entry.isDirectory()) await visit(full, depth + 1)
25
+ else if (entry.isFile() && SOURCE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full)
26
+ }
27
+ }
28
+ await visit(root, 0)
29
+ return files
30
+ }
31
+
32
+ function rel(root, file) {
33
+ return path.relative(root, file).replaceAll("\\", "/")
34
+ }
35
+
36
+ function importsFor(source, ext) {
37
+ const imports = []
38
+ const push = (value) => {
39
+ const clean = String(value || "").trim()
40
+ if (clean && !imports.includes(clean)) imports.push(clean)
41
+ }
42
+
43
+ if ([".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx", ".vue", ".svelte"].includes(ext)) {
44
+ for (const match of source.matchAll(/\b(?:import|export)\s+(?:[^"'\n]+?\s+from\s+)?["']([^"']+)["']/g)) push(match[1])
45
+ for (const match of source.matchAll(/\brequire\(\s*["']([^"']+)["']\s*\)/g)) push(match[1])
46
+ for (const match of source.matchAll(/\bimport\(\s*["']([^"']+)["']\s*\)/g)) push(match[1])
47
+ } else if (ext === ".py") {
48
+ for (const match of source.matchAll(/^\s*from\s+([A-Za-z0-9_\.]+)\s+import\s+/gm)) push(match[1])
49
+ for (const match of source.matchAll(/^\s*import\s+([A-Za-z0-9_\.]+)/gm)) push(match[1])
50
+ } else if ([".java", ".kt", ".kts"].includes(ext)) {
51
+ for (const match of source.matchAll(/^\s*import\s+([A-Za-z0-9_.*]+)/gm)) push(match[1])
52
+ } else if (ext === ".cs") {
53
+ for (const match of source.matchAll(/^\s*using\s+([A-Za-z0-9_.]+)/gm)) push(match[1])
54
+ } else if (ext === ".go") {
55
+ for (const match of source.matchAll(/import\s+(?:\([^)]*?["']([^"']+)["']|["']([^"']+)["'])/gs)) push(match[1] || match[2])
56
+ } else if (ext === ".rs") {
57
+ for (const match of source.matchAll(/^\s*use\s+([^;]+);/gm)) push(match[1])
58
+ }
59
+ return imports
60
+ }
61
+
62
+ function resolveRelativeImport(root, importer, specifier, fileSet) {
63
+ if (!specifier.startsWith(".")) return null
64
+ const base = path.resolve(path.dirname(importer), specifier)
65
+ const candidates = [
66
+ base,
67
+ ...[...SOURCE_EXTENSIONS].map((ext) => base + ext),
68
+ ...[...SOURCE_EXTENSIONS].map((ext) => path.join(base, "index" + ext)),
69
+ ]
70
+ for (const candidate of candidates) {
71
+ const relative = rel(root, candidate)
72
+ if (fileSet.has(relative)) return relative
73
+ }
74
+ return null
75
+ }
76
+
77
+ export async function buildRepoGraph(root = process.cwd(), options = {}) {
78
+ root = path.resolve(root)
79
+ const maxFiles = options.maxFiles ?? 2500
80
+ const files = await walk(root, maxFiles, options.maxDepth ?? 8)
81
+ const fileSet = new Set(files.map((file) => rel(root, file)))
82
+ const nodes = []
83
+ const edges = []
84
+ const external = new Map()
85
+
86
+ for (const file of files) {
87
+ const info = await stat(file).catch(() => null)
88
+ if (!info || info.size > 768 * 1024) continue
89
+ const source = await readFile(file, "utf8").catch(() => "")
90
+ const relative = rel(root, file)
91
+ const ext = path.extname(file).toLowerCase()
92
+ const imports = importsFor(source, ext)
93
+ const local = []
94
+
95
+ for (const specifier of imports) {
96
+ const resolved = resolveRelativeImport(root, file, specifier, fileSet)
97
+ if (resolved) {
98
+ local.push(resolved)
99
+ edges.push({ from: relative, to: resolved, kind: "local-import" })
100
+ } else {
101
+ const parts = specifier.split("/")
102
+ const key = parts[0].startsWith("@") ? parts.slice(0, 2).join("/") : parts[0]
103
+ external.set(key, (external.get(key) || 0) + 1)
104
+ }
105
+ }
106
+
107
+ nodes.push({ path: relative, imports: imports.length, localImports: [...new Set(local)].sort() })
108
+ }
109
+
110
+ const incoming = new Map()
111
+ for (const edge of edges) incoming.set(edge.to, (incoming.get(edge.to) || 0) + 1)
112
+ const hotspots = nodes
113
+ .map((node) => ({
114
+ path: node.path,
115
+ incoming: incoming.get(node.path) || 0,
116
+ outgoing: node.localImports.length,
117
+ score: (incoming.get(node.path) || 0) + node.localImports.length,
118
+ }))
119
+ .sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
120
+ .slice(0, 30)
121
+
122
+ return {
123
+ schemaVersion: 1,
124
+ root,
125
+ scannedFiles: files.length,
126
+ truncated: files.length >= maxFiles,
127
+ nodes,
128
+ edges,
129
+ hotspots,
130
+ externalImports: [...external.entries()]
131
+ .map(([name, count]) => ({ name, count }))
132
+ .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name))
133
+ .slice(0, 50),
134
+ }
135
+ }