opencode-agent-skill 14.2.0-beta.1 → 15.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 (55) hide show
  1. package/README.md +51 -18
  2. package/docs/PI-COMPAT.md +167 -3
  3. package/docs/V15-MANAGED-RUNTIME.md +128 -0
  4. package/global-config/agents/architect.md +63 -1
  5. package/global-config/agents/executor.md +7 -0
  6. package/global-config/agents/plan-checker.md +8 -1
  7. package/global-config/agents/verifier.md +3 -3
  8. package/global-config/skills/database-engineering/references/workflow.md +15 -0
  9. package/global-config/skills/engineering-orchestrator/references/long-horizon.md +21 -11
  10. package/lib/activity-deadline.mjs +70 -0
  11. package/lib/adaptive-context-budget.mjs +25 -5
  12. package/lib/affected-tests.mjs +1 -1
  13. package/lib/capability-fabric.mjs +40 -0
  14. package/lib/code-intelligence/edit-anchor.mjs +102 -0
  15. package/lib/code-intelligence/index.mjs +95 -0
  16. package/lib/code-intelligence/lsp-provider.mjs +189 -0
  17. package/lib/completion-auditor.mjs +102 -0
  18. package/lib/document-ingestion.mjs +60 -0
  19. package/lib/evidence-store.mjs +92 -8
  20. package/lib/execution-contract.mjs +579 -0
  21. package/lib/fast-verification-gate.mjs +69 -0
  22. package/lib/leaf-runtime-optimizer.mjs +76 -0
  23. package/lib/mcp-health.mjs +144 -0
  24. package/lib/mcp-tool-policy.mjs +19 -0
  25. package/lib/memory-engine.mjs +28 -0
  26. package/lib/pi-child-invocation.mjs +65 -0
  27. package/lib/pi-rpc-pool.mjs +61 -9
  28. package/lib/plan-salvage.mjs +124 -0
  29. package/lib/planning-speed-policy.mjs +131 -0
  30. package/lib/prompt-cache.mjs +26 -12
  31. package/lib/repo-graph.mjs +1 -1
  32. package/lib/repo-inspect.mjs +1 -0
  33. package/lib/reversible-context.mjs +49 -0
  34. package/lib/runtime-artifacts.mjs +42 -0
  35. package/lib/runtime-events.mjs +33 -1
  36. package/lib/safety.mjs +1 -0
  37. package/lib/semantic-index.mjs +1 -1
  38. package/lib/service-manager.mjs +526 -0
  39. package/lib/session-display.mjs +49 -0
  40. package/lib/task-engine.mjs +6 -10
  41. package/lib/task-graph.mjs +87 -0
  42. package/lib/task-policy.mjs +210 -8
  43. package/lib/trajectory.mjs +111 -1
  44. package/lib/turbo-fast-path.mjs +44 -0
  45. package/lib/workspace-fingerprint.mjs +2 -11
  46. package/lib/workspace-root.mjs +49 -0
  47. package/lib/worktree-sandbox.mjs +234 -11
  48. package/package.json +9 -6
  49. package/pi/extensions/ues-child-runtime.ts +300 -2
  50. package/pi/extensions/ues.ts +1887 -160
  51. package/scripts/check-release-consistency.mjs +25 -2
  52. package/scripts/check-source-integrity.mjs +532 -3
  53. package/scripts/eval-pi.mjs +76 -36
  54. package/scripts/smoke-pi-extension.mjs +1 -1
  55. package/pi/prompts/ues-run.md +0 -17
@@ -2,6 +2,93 @@ import path from "node:path"
2
2
 
3
3
  const VALID_RISKS = new Set(["low", "medium", "high", "critical"])
4
4
 
5
+ function firstDefined(...values) {
6
+ return values.find((value) => value !== undefined && value !== null)
7
+ }
8
+
9
+ function normalizeStringList(value, aliases = []) {
10
+ const sources = [value, ...aliases].filter((source) => source !== undefined && source !== null)
11
+ if (!sources.length) return undefined
12
+
13
+ for (const source of sources) {
14
+ const items = Array.isArray(source) ? source : [source]
15
+ const normalized = items
16
+ .map((item) => {
17
+ if (typeof item === "string") return item.trim()
18
+ if (!item || typeof item !== "object" || Array.isArray(item)) return ""
19
+ return String(
20
+ firstDefined(
21
+ item.criterion,
22
+ item.criteria,
23
+ item.check,
24
+ item.description,
25
+ item.expected,
26
+ item.outcome,
27
+ item.text,
28
+ item.command,
29
+ ) || "",
30
+ ).trim()
31
+ })
32
+ .filter(Boolean)
33
+ if (normalized.length) return normalized
34
+ }
35
+
36
+ return []
37
+ }
38
+
39
+ function normalizeRisk(value, fallback = "medium") {
40
+ const raw = String(value ?? "").trim()
41
+ if (!raw) return { risk: fallback, riskNotes: "" }
42
+ const lower = raw.toLowerCase()
43
+ if (VALID_RISKS.has(lower)) return { risk: lower, riskNotes: "" }
44
+
45
+ const token = lower.match(/\b(low|medium|high|critical)\b/)?.[1]
46
+ if (token && VALID_RISKS.has(token)) {
47
+ return { risk: token, riskNotes: raw }
48
+ }
49
+
50
+ // Unknown prose in a risk field is never downgraded. Preserve it as notes
51
+ // and choose a conservative level so weak-model formatting errors cannot
52
+ // silently weaken verification requirements.
53
+ return { risk: "high", riskNotes: raw }
54
+ }
55
+
56
+ export function normalizePlanForValidation(plan) {
57
+ if (!plan || typeof plan !== "object" || Array.isArray(plan)) return plan
58
+ const tasks = Array.isArray(plan.tasks)
59
+ ? plan.tasks.map((task) => {
60
+ if (!task || typeof task !== "object" || Array.isArray(task)) return task
61
+
62
+ const acceptance = normalizeStringList(
63
+ task.acceptance,
64
+ [task.acceptanceCriteria, task.criteria, task.successCriteria],
65
+ )
66
+ const verification = normalizeStringList(
67
+ task.verification,
68
+ [task.verificationChecks, task.checks, task.verify, task.validation],
69
+ )
70
+ const riskSource = firstDefined(task.risk, task.riskLevel, task.severity)
71
+ const normalizedRisk = normalizeRisk(riskSource, "medium")
72
+ const dependencies = firstDefined(task.dependsOn, task.dependencies)
73
+
74
+ return {
75
+ ...task,
76
+ ...(dependencies !== undefined ? { dependsOn: Array.isArray(dependencies) ? dependencies : [dependencies].filter(Boolean) } : {}),
77
+ ...(acceptance !== undefined ? { acceptance } : {}),
78
+ ...(verification !== undefined ? { verification } : {}),
79
+ risk: normalizedRisk.risk,
80
+ ...(normalizedRisk.riskNotes && !task.riskNotes ? { riskNotes: normalizedRisk.riskNotes } : {}),
81
+ }
82
+ })
83
+ : plan.tasks
84
+
85
+ return {
86
+ ...plan,
87
+ schemaVersion: Number(plan.schemaVersion || 1),
88
+ tasks,
89
+ }
90
+ }
91
+
5
92
  function unsafeRepoPath(value) {
6
93
  const raw = String(value || "").replaceAll("\\", "/")
7
94
  if (!raw) return false
@@ -2,8 +2,62 @@ const SENSITIVE_DOMAIN = /(\bauth\b|authorization|authentication|security|permis
2
2
  const HIGH_RISK_MUTATION = /((?:fix|change|modify|update|alter|migrate|drop|truncate|delete|remove|rotate|deploy|publish|push|sửa|thay đổi|cập nhật|xóa|xoá|di trú|chuyển đổi|triển khai).{0,64}(?:\bauth\b|authorization|authentication|security|permission|payment(?: handling| flow)?|schema|database|production|public api|secret|credential|phân quyền|bảo mật|thanh toán|cơ sở dữ liệu|api công khai|bí mật|thông tin xác thực)|(?:\bauth\b|authorization|authentication|security|permission|payment(?: handling| flow)?|schema|database|production|public api|secret|credential|phân quyền|bảo mật|thanh toán|cơ sở dữ liệu|api công khai|bí mật|thông tin xác thực).{0,64}(?:fix|change|modify|update|alter|migrate|drop|truncate|delete|remove|rotate|deploy|publish|push|sửa|thay đổi|cập nhật|xóa|xoá|di trú|chuyển đổi|triển khai)|database migration|schema migration|migrate database|migrate schema|drop table|truncate table|deploy(?:ment)?\s+(?:to\s+)?production|production\s+deploy(?:ment)?|rotate\s+(?:secret|credential)|breaking\s+(?:change\s+to\s+)?(?:public\s+)?api|npm publish|git push|force push|reset --hard|git clean)/i
3
3
  const LONG = /(whole repo|whole repository|whole project|entire repo|entire project|large refactor|major refactor|long[- ](?:running|horizon)|durable state|dependency graph|integration verification|resume|refactor all|multi[- ]step migration|migration across|toàn bộ repo|toàn bộ repository|toàn bộ dự án|toàn bộ project|refactor lớn|tác vụ dài|nhiều file|nhiều module|tiếp tục công việc|refactor toàn bộ|xác minh tích hợp|kiểm tra tích hợp|chia (?:công việc|task|tác vụ).*(?:dependency|phụ thuộc))/i
4
4
  const DEBUG = /(fix|bug|debug|crash|regression|failure|error|broken|sửa lỗi|lỗi|điều tra lỗi|không chạy)/i
5
+ const CONCRETE_DIAGNOSIS = /(\bdebug\b|diagnos|reproduce|stack\s*trace|exception|\bcrash(?:es|ed|ing)?\b|failing\s+test|tests?\s+(?:currently\s+)?fail(?:ing|ed)?|failure\s+(?:affects|occurs|happens|when|on)|error\s*[:=]|expected.{0,48}actual|actual.{0,48}expected|exit\s+code\s*[1-9]|\b(?:ECONN\w*|SQLSTATE|TypeError|ReferenceError|AssertionError)\b|điều\s+tra\s+lỗi|tái\s+hiện\s+lỗi|lỗi\s*[:=]|test.{0,32}(?:thất\s+bại|fail))/i
5
6
  const CONTRACT = /(public api|api contract|openapi|response schema|request schema|breaking api|hợp đồng api|api công khai)/i
6
7
  const DATA = /(database|sql|migration|schema|transaction|index|cơ sở dữ liệu|dữ liệu|migrate)/i
8
+ const BROAD_FILE_SCOPE = /(across\s+(?:multiple|several|many)\s+(?:files|modules|packages)|multiple\s+(?:files|modules|packages)|several\s+(?:files|modules|packages)|many\s+(?:files|modules|packages)|nhiều\s+(?:file|tệp|module)|toàn bộ\s+(?:repo|repository|project|dự án))/i
9
+ const CRITICAL_LOCAL_MUTATION = /(drop\s+table|truncate\s+table|reset\s+--hard|git\s+clean|force\s+push|npm\s+publish|deploy(?:ment)?\s+(?:to\s+)?production|production\s+deploy(?:ment)?|rotate\s+(?:secret|credential)|(?:secret|credential|token).{0,48}(?:rotate|replace|revoke|delete)|(?:authorization|authentication|permission|phân quyền|xác thực).{0,64}(?:change|modify|update|bypass|remove|delete|sửa|thay đổi)|(?:database|sql|schema).{0,64}(?:migration|migrate|drop|truncate|alter)|(?:migration|migrate).{0,64}(?:database|sql|schema)|payment.{0,64}(?:charge|capture|refund|webhook|checkout)|(?:charge|capture|refund|webhook|checkout).{0,64}payment|breaking\s+(?:change\s+to\s+)?(?:public\s+)?api)/i
10
+ const EXPLICIT_READ_ONLY = /(?:\bread[- ]?only\b|\bdo not\s+(?:edit|modify|change|write|create|delete|remove)\b|\bwithout\s+(?:editing|modifying|changing|writing|creating|deleting|removing)\b|\bno\s+(?:source|file|code)?\s*(?:edits?|changes?|writes?)\b|không\s+(?:sửa|chỉnh sửa|thay đổi|ghi|tạo|xóa|xoá)\b|chỉ\s+(?:đọc|kiểm tra|rà soát)\b)/i
11
+ const READ_ONLY_MODE = /(?:\bread[- ]?only\b|chế\s+độ\s+chỉ\s+đọc|chế\s+độ\s+read[- ]?only)/i
12
+ const MUTATION_INTENT = /(?:\b(?:fix|repair|implement|add|edit|modify|change|write|create|delete|remove|update|refactor|migrate|rename)\b|(?:sửa|chỉnh sửa|thay đổi|ghi|tạo|xóa|xoá|cập nhật|refactor|di trú|đổi tên))/i
13
+ const SOURCE_FILE_RE = /(?:^|[\s("'\x60])((?:[A-Za-z]:)?(?:[./\\\w@-]+[\\/])*[\w@.-]+\.(?:[cm]?[jt]sx?|py|go|rs|java|kt|kts|cs|rb|php|swift|dart|vue|svelte))(?=$|[\s"'\x60),:;.])/gi
14
+
15
+ const COMMAND_ONLY_READ_ONLY = /(?:chỉ\s+(?:chạy|thực hiện)(?:\s+đúng)?|only\s+(?:run|execute)|run\s+only|execute\s+only|exactly\s+(?:run|execute))/i
16
+
17
+ const AUTO_ENGINEERING_ACTION = /(?:\b(?:fix|repair|debug|implement|add|edit|modify|change|update|refactor|migrate|review|audit|verify|test|build|run|inspect|scan|check|analy[sz]e|optimi[sz]e|cleanup|clean up|commit|push|continue|resume)\b|(?:sửa|chỉnh sửa|xử lý|điều tra|thêm|tạo|cập nhật|thay đổi|refactor|di trú|rà soát|kiểm tra|xác minh|chạy|test|build|xem|phân tích|tối ưu|dọn|commit|push|tiếp tục))/i
18
+ const AUTO_ENGINEERING_TARGET = /(?:\b(?:repo(?:sitory)?|project|code|source|file|module|package|branch|commit|git|npm|pnpm|yarn|node|typescript|javascript|python|java|kotlin|swift|react|expo|android|ios|api|backend|frontend|mobile|web|database|db|sql|prisma|test|e2e|ci|build|lint|typecheck|runtime|server|client|bug|error|failure|exception)\b|(?:dự án|mã nguồn|tệp|nhánh|cơ sở dữ liệu|dữ liệu|kiểm thử|lỗi|ứng dụng|giao diện|máy chủ))/i
19
+ const AUTO_INFORMATIONAL_ONLY = /(?:^|\b)(?:explain|what\s+is|what\s+are|how\s+does|why\s+does|compare|difference\s+between|giải\s+thích|là\s+gì|hoạt\s+động\s+(?:ra\s+sao|thế\s+nào)|tại\s+sao|so\s+sánh)(?:\b|\s)/i
20
+ const AUTO_NON_TASK_CHAT = /^(?:xin\s*chào|chào|hello|hi|hey|cảm\s*ơn|cam\s*on|thanks?|ok(?:ay)?|được|ừ|uh|vâng|vang)[!?.\s]*$/i
21
+ const AUTO_ACTIVE_CONTINUATION = /^(?:continue|resume|keep\s+going|go\s+on|tiếp\s+tục|làm\s+tiếp|lam\s+tiep|sửa\s+tiếp|sua\s+tiep|kiểm\s+tra\s+tiếp|kiem\s+tra\s+tiep)(?:\b|[.!?,;:\s])/i
22
+
23
+ const SAFE_READ_ONLY_GIT_CHECKS = [
24
+ { id: "git-status-short", label: "git status --short", match: /^git\s+status\s+--short\b/i, args: ["status", "--short"] },
25
+ { id: "git-branch-current", label: "git branch --show-current", match: /^git\s+branch\s+--show-current\b/i, args: ["branch", "--show-current"] },
26
+ { id: "git-rev-parse-head", label: "git rev-parse HEAD", match: /^git\s+rev-parse\s+head\b/i, args: ["rev-parse", "HEAD"] },
27
+ { id: "git-rev-parse-root", label: "git rev-parse --show-toplevel", match: /^git\s+rev-parse\s+--show-toplevel\b/i, args: ["rev-parse", "--show-toplevel"] },
28
+ { id: "git-status", label: "git status", match: /^git\s+status\b/i, args: ["status"] },
29
+ ]
30
+
31
+ function referencedSourceFiles(value) {
32
+ const files = []
33
+ const seen = new Set()
34
+ SOURCE_FILE_RE.lastIndex = 0
35
+ let match
36
+ while ((match = SOURCE_FILE_RE.exec(String(value || "")))) {
37
+ const normalized = String(match[1] || "").replaceAll("\\", "/").toLowerCase()
38
+ if (!normalized || seen.has(normalized)) continue
39
+ seen.add(normalized)
40
+ files.push(normalized)
41
+ }
42
+ return files
43
+ }
44
+
45
+ function singleFileBoundedTask(value, facts = {}) {
46
+ const declaredFiles = Array.isArray(facts.declaredFiles) ? facts.declaredFiles.map(String).filter(Boolean) : []
47
+ const changedFileCount = Array.isArray(facts.changedFiles)
48
+ ? facts.changedFiles.length
49
+ : Number.isFinite(Number(facts.changedFiles)) ? Number(facts.changedFiles) : 0
50
+ const referencedFiles = referencedSourceFiles(value)
51
+ const counts = [declaredFiles.length, changedFileCount, referencedFiles.length].filter((count) => count > 0)
52
+ if (!counts.length || counts.some((count) => count > 1)) return false
53
+ if (BROAD_FILE_SCOPE.test(String(value || "")) || LONG.test(String(value || ""))) return false
54
+ return counts.some((count) => count === 1) && (
55
+ declaredFiles.length === 1 ||
56
+ changedFileCount === 1 ||
57
+ /\b(?:fix|repair|change|update|modify|rename|implement|correct|sửa|chỉnh sửa|thay đổi|cập nhật)\b/i.test(String(value || ""))
58
+ )
59
+ }
60
+
7
61
 
8
62
  function riskTextFor(value) {
9
63
  return String(value || "")
@@ -13,6 +67,107 @@ function riskTextFor(value) {
13
67
  .replace(/chỉ\s+đọc[^.\n]*/gi, "")
14
68
  }
15
69
 
70
+ export function automaticUesContinuation(value, facts = {}) {
71
+ const text = String(value || "").trim()
72
+ if (!facts.activeRun) return { forward: false, reason: "no-active-run" }
73
+ if (!text || text.startsWith("/")) return { forward: false, reason: "not-continuation" }
74
+ if (!AUTO_ACTIVE_CONTINUATION.test(text)) return { forward: false, reason: "not-continuation" }
75
+ return { forward: true, reason: "active-run-continuation" }
76
+ }
77
+
78
+ export function automaticUesAdmission(value, facts = {}) {
79
+ const text = String(value || "").trim()
80
+ const native = (reason, policy = null) => ({
81
+ admit: false,
82
+ route: "native",
83
+ confidence: "low",
84
+ reason,
85
+ policy,
86
+ })
87
+
88
+ if (!text) return native("empty")
89
+ if (text.startsWith("/")) return native("explicit-command")
90
+ if (AUTO_NON_TASK_CHAT.test(text)) return native("casual-chat")
91
+ if (facts.inGitWorkspace === false) return native("not-git-workspace")
92
+
93
+ const policy = classifyEngineeringTask(text, facts)
94
+ const action = AUTO_ENGINEERING_ACTION.test(text)
95
+ const target = AUTO_ENGINEERING_TARGET.test(text)
96
+ const informationalOnly =
97
+ AUTO_INFORMATIONAL_ONLY.test(text) &&
98
+ !/(?:\b(?:fix|repair|debug|implement|edit|modify|change|update|refactor|migrate|test|build|run|commit|push)\b|(?:sửa|chỉnh sửa|xử lý|thay đổi|cập nhật|refactor|chạy|test|build|commit|push))/i.test(text)
99
+ const longStructured =
100
+ target &&
101
+ (
102
+ text.length >= 700 ||
103
+ policy.mode === "long-horizon" ||
104
+ /(?:^|\n)\s*(?:phase\s+\d+|mục\s+tiêu|requirements?|acceptance criteria|final report)\b/i.test(text)
105
+ )
106
+ const readOnlyEngineering =
107
+ policy.readOnly === true &&
108
+ /(?:repo(?:sitory)?|project|code|source|git|file|test|dự án|mã nguồn|tệp|kiểm tra repository)/i.test(text)
109
+
110
+ if (informationalOnly && !longStructured && !readOnlyEngineering) {
111
+ return native("informational-question", policy)
112
+ }
113
+
114
+ const admit = longStructured || (action && target) || readOnlyEngineering
115
+ if (!admit) return native("not-confident-engineering-task", policy)
116
+
117
+ const route = policy.risk === "high" ? "guarded" : "auto"
118
+ return {
119
+ admit: true,
120
+ route,
121
+ confidence: longStructured || readOnlyEngineering ? "high" : "medium",
122
+ reason:
123
+ route === "guarded"
124
+ ? "high-risk-engineering-task"
125
+ : longStructured
126
+ ? "long-structured-engineering-task"
127
+ : readOnlyEngineering
128
+ ? "read-only-engineering-task"
129
+ : "engineering-action-target",
130
+ policy,
131
+ }
132
+ }
133
+
134
+ export function deterministicReadOnlyGitCommands(value) {
135
+ const text = String(value || "")
136
+ if (!COMMAND_ONLY_READ_ONLY.test(text)) return []
137
+
138
+ const mentions = []
139
+ const lower = text.toLowerCase()
140
+ const gitRe = /\bgit\s+/g
141
+ let hit
142
+ while ((hit = gitRe.exec(lower))) {
143
+ const tail = text.slice(hit.index)
144
+ const matched = SAFE_READ_ONLY_GIT_CHECKS.find((item) => item.match.test(tail))
145
+ if (!matched) return []
146
+ mentions.push({ index: hit.index, id: matched.id, label: matched.label, command: "git", args: [...matched.args] })
147
+ }
148
+
149
+ if (!mentions.length) return []
150
+ const seen = new Set()
151
+ return mentions
152
+ .sort((a, b) => a.index - b.index)
153
+ .filter((item) => {
154
+ if (seen.has(item.id)) return false
155
+ seen.add(item.id)
156
+ return true
157
+ })
158
+ .map(({ id, label, command, args }) => ({ id, label, command, args }))
159
+ }
160
+
161
+ function readOnlyTask(value, facts = {}) {
162
+ if (facts.readOnly === true) return true
163
+ const text = String(value || "")
164
+ if (READ_ONLY_MODE.test(text)) return true
165
+ if (!EXPLICIT_READ_ONLY.test(text)) return false
166
+ // Negative file-scope constraints ("fix X, but do not modify Y") must not
167
+ // convert a mutating engineering task into a read-only inspection.
168
+ return !MUTATION_INTENT.test(riskTextFor(text))
169
+ }
170
+
16
171
  function signal(name, matched, weight) {
17
172
  return matched ? { name, weight } : null
18
173
  }
@@ -151,6 +306,17 @@ export function shouldRunDedicatedDiagnosis(taskPolicy = {}, attempt = 1) {
151
306
  return recoveryPolicyForAttempt(taskPolicy, normalizedAttempt).requireDiagnosis === true
152
307
  }
153
308
 
309
+ // Long-horizon work already has an architect + independent plan gate. A generic
310
+ // word such as "fix" must not force a redundant debugger pass before planning.
311
+ // Keep first-pass diagnosis when the request contains concrete failure evidence.
312
+ if (
313
+ taskPolicy.mode === "long-horizon" &&
314
+ taskPolicy.requirePlanCheck === true &&
315
+ taskPolicy.diagnosisEvidence !== true
316
+ ) {
317
+ return false
318
+ }
319
+
154
320
  return taskPolicy.executionProfile !== "fast" || taskPolicy.risk !== "low"
155
321
  }
156
322
 
@@ -158,7 +324,19 @@ export function classifyEngineeringTask(text, facts = {}) {
158
324
  const value = String(text || "")
159
325
  const riskText = riskTextFor(value)
160
326
  const sensitiveDomain = SENSITIVE_DOMAIN.test(value)
161
- const sensitiveMutation = HIGH_RISK_MUTATION.test(riskText)
327
+ const readOnly = readOnlyTask(value, facts)
328
+ const singleFileBounded = readOnly ? false : singleFileBoundedTask(value, facts)
329
+ const rawSensitiveMutation = HIGH_RISK_MUTATION.test(riskText)
330
+ const sensitiveMutation =
331
+ rawSensitiveMutation &&
332
+ (!singleFileBounded || CRITICAL_LOCAL_MUTATION.test(riskText))
333
+ const dataMigration =
334
+ Boolean(facts.hasMigration) ||
335
+ (
336
+ !singleFileBounded &&
337
+ DATA.test(riskText) &&
338
+ /migration|schema|migrate|di trú|chuyển đổi/i.test(riskText)
339
+ )
162
340
  const declaredHighRisk =
163
341
  ["high", "critical"].includes(String(facts.risk || "").toLowerCase()) ||
164
342
  /\b(?:risk|rủi ro)\s*[:=\/-]?\s*(?:high|critical|cao|nghiêm trọng)\b/i.test(value) ||
@@ -178,18 +356,30 @@ export function classifyEngineeringTask(text, facts = {}) {
178
356
  signal("declared-high-risk", declaredHighRisk, 2),
179
357
  signal("explicit-long-horizon", explicitLongHorizon, 2),
180
358
  signal("debugging", DEBUG.test(value), 1),
359
+ signal("read-only", readOnly, -1),
360
+ signal("single-file-bounded", singleFileBounded, -2),
181
361
  signal("public-contract", CONTRACT.test(value) || facts.hasPublicContract, 2),
182
- signal("data-migration", (DATA.test(riskText) && /migration|schema|migrate|di trú|chuyển đổi/i.test(riskText)) || facts.hasMigration, 2),
362
+ signal("data-migration", dataMigration, 2),
183
363
  signal("many-changed-files", Number(facts.changedFiles || 0) > 5, 1),
184
364
  signal("very-many-changed-files", Number(facts.changedFiles || 0) > 12, 1),
185
365
  signal("large-repository", Number(facts.repoFiles || 0) > 1500, 1),
186
366
  signal("monorepo", facts.monorepo === true, 1),
187
367
  ].filter(Boolean)
188
368
 
189
- const score = signals.reduce((sum, item) => sum + item.weight, 0)
369
+ const score = Math.max(0, signals.reduce((sum, item) => sum + item.weight, 0))
190
370
  const highRisk = declaredHighRisk || sensitiveMutation || Boolean(facts.hasMigration) || Boolean(facts.hasPublicContract)
191
- const risk = highRisk ? "high" : score >= 3 ? "medium" : "low"
192
- const mode = explicitLongHorizon || score >= 4 ? "long-horizon" : score >= 2 ? "standard" : "inline"
371
+ const diagnosisEvidence = facts.failureEvidence === true || CONCRETE_DIAGNOSIS.test(value)
372
+ const risk = highRisk ? "high" : singleFileBounded ? "low" : score >= 3 ? "medium" : "low"
373
+ const mode =
374
+ explicitLongHorizon
375
+ ? "long-horizon"
376
+ : singleFileBounded && !highRisk
377
+ ? "inline"
378
+ : score >= 4
379
+ ? "long-horizon"
380
+ : score >= 2
381
+ ? "standard"
382
+ : "inline"
193
383
  const modelTier = risk === "high" || mode === "long-horizon" ? "heavy" : score >= 2 ? "standard" : "light"
194
384
  const maxAttempts = risk === "high" ? 2 : 3
195
385
  const profile = profileFor(mode, risk)
@@ -215,10 +405,22 @@ export function classifyEngineeringTask(text, facts = {}) {
215
405
  maxAttempts,
216
406
  contextBudget: profile.contextBudget,
217
407
  maxSkills: profile.maxSkills,
218
- requirePlanCheck: profile.durableState || risk === "high",
219
- requireIntegrationVerification: mode !== "inline" || risk === "high",
408
+ requirePlanCheck: readOnly ? false : profile.durableState || risk === "high",
409
+ requireIntegrationVerification: readOnly ? false : mode !== "inline" || risk === "high",
220
410
  requireFreshEvidence: true,
221
- profile,
411
+ requireBehavioralReceipt: !readOnly,
412
+ readOnly,
413
+ singleFileBounded,
414
+ diagnosisEvidence,
415
+ profile: readOnly
416
+ ? {
417
+ ...profile,
418
+ worktree: "off",
419
+ verification: "command-evidence",
420
+ fullCI: false,
421
+ containerVerification: "off",
422
+ }
423
+ : profile,
222
424
  domains: [...new Set(domains)],
223
425
  recovery: {
224
426
  escalateAfterFailure: true,
@@ -1,9 +1,115 @@
1
1
  import { createHash, randomUUID } from "node:crypto"
2
- import { mkdir, readFile, writeFile } from "node:fs/promises"
2
+ import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"
3
3
  import path from "node:path"
4
4
 
5
5
  const TRACE_DIR = ".ues-traces"
6
6
  const MAX_EVENT_BYTES = 64 * 1024
7
+ const DEFAULT_MAX_TRACE_FILE_BYTES = 4 * 1024 * 1024
8
+ const DEFAULT_MAX_TRACE_TOTAL_BYTES = 24 * 1024 * 1024
9
+ const DEFAULT_MAX_TRACE_FILES = 32
10
+ const DEFAULT_MAX_TRACE_AGE_MS = 7 * 86_400_000
11
+
12
+ function positiveInt(value, fallback, min, max) {
13
+ const parsed = Number(value)
14
+ if (!Number.isFinite(parsed)) return fallback
15
+ return Math.max(min, Math.min(max, Math.trunc(parsed)))
16
+ }
17
+
18
+ function traceLimits(options = {}) {
19
+ return {
20
+ maxFileBytes: positiveInt(
21
+ options.maxFileBytes ?? process.env.UES_TRACE_MAX_FILE_BYTES,
22
+ DEFAULT_MAX_TRACE_FILE_BYTES,
23
+ 256 * 1024,
24
+ 64 * 1024 * 1024,
25
+ ),
26
+ maxTotalBytes: positiveInt(
27
+ options.maxTotalBytes ?? process.env.UES_TRACE_MAX_TOTAL_BYTES,
28
+ DEFAULT_MAX_TRACE_TOTAL_BYTES,
29
+ 1024 * 1024,
30
+ 512 * 1024 * 1024,
31
+ ),
32
+ maxFiles: positiveInt(
33
+ options.maxFiles ?? process.env.UES_TRACE_MAX_FILES,
34
+ DEFAULT_MAX_TRACE_FILES,
35
+ 4,
36
+ 256,
37
+ ),
38
+ maxAgeMs: positiveInt(
39
+ options.maxAgeMs ?? process.env.UES_TRACE_MAX_AGE_MS,
40
+ DEFAULT_MAX_TRACE_AGE_MS,
41
+ 60_000,
42
+ 90 * 86_400_000,
43
+ ),
44
+ }
45
+ }
46
+
47
+ async function compactTraceFile(file, maxFileBytes) {
48
+ const info = await stat(file).catch(() => null)
49
+ if (!info?.isFile() || info.size < maxFileBytes) return false
50
+
51
+ const source = await readFile(file)
52
+ const keepBytes = Math.max(128 * 1024, Math.floor(maxFileBytes / 2))
53
+ let kept = source.subarray(Math.max(0, source.length - keepBytes))
54
+ const newline = kept.indexOf(0x0a)
55
+ if (newline >= 0 && source.length > keepBytes) kept = kept.subarray(newline + 1)
56
+ const marker = Buffer.from(JSON.stringify({
57
+ schemaVersion: 1,
58
+ type: "trace.compacted",
59
+ at: new Date().toISOString(),
60
+ payload: { previousBytes: source.length, retainedBytes: kept.length },
61
+ }) + "\n")
62
+ await writeFile(file, Buffer.concat([marker, kept]))
63
+ return true
64
+ }
65
+
66
+ export async function pruneTrajectoryStore(root = process.cwd(), options = {}) {
67
+ root = path.resolve(root)
68
+ const dir = path.join(root, TRACE_DIR)
69
+ const limits = traceLimits(options)
70
+ const protect = options.protectTraceID ? cleanID(options.protectTraceID) + ".jsonl" : null
71
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => [])
72
+ const rows = []
73
+ for (const entry of entries) {
74
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue
75
+ const file = path.join(dir, entry.name)
76
+ const info = await stat(file).catch(() => null)
77
+ if (!info?.isFile()) continue
78
+ rows.push({ name: entry.name, file, bytes: info.size, mtimeMs: info.mtimeMs })
79
+ }
80
+
81
+ rows.sort((a, b) => b.mtimeMs - a.mtimeMs)
82
+ let retainedBytes = 0
83
+ let retainedFiles = 0
84
+ const removed = []
85
+ const now = Date.now()
86
+
87
+ for (const row of rows) {
88
+ if (row.name === protect) {
89
+ retainedBytes += row.bytes
90
+ retainedFiles += 1
91
+ continue
92
+ }
93
+ const expired = limits.maxAgeMs > 0 && now - row.mtimeMs > limits.maxAgeMs
94
+ const overFiles = retainedFiles >= limits.maxFiles
95
+ const overBytes = retainedBytes + row.bytes > limits.maxTotalBytes
96
+ if (expired || overFiles || overBytes) {
97
+ await rm(row.file, { force: true }).catch(() => {})
98
+ removed.push(row.name)
99
+ continue
100
+ }
101
+ retainedBytes += row.bytes
102
+ retainedFiles += 1
103
+ }
104
+
105
+ return {
106
+ removed,
107
+ removedCount: removed.length,
108
+ retainedFiles,
109
+ retainedBytes,
110
+ limits,
111
+ }
112
+ }
7
113
 
8
114
  function cleanID(value) {
9
115
  const text = String(value || "").trim()
@@ -49,8 +155,12 @@ export function trajectoryFile(root, traceID) {
49
155
  }
50
156
 
51
157
  export async function appendTrajectoryEvent(root, traceID, type, payload = {}) {
158
+ root = path.resolve(root)
52
159
  const file = trajectoryFile(root, traceID)
53
160
  await mkdir(path.dirname(file), { recursive: true })
161
+ const limits = traceLimits()
162
+ await compactTraceFile(file, limits.maxFileBytes).catch(() => false)
163
+ await pruneTrajectoryStore(root, { ...limits, protectTraceID: traceID }).catch(() => null)
54
164
  const scrubbed = scrubTrajectoryValue(payload)
55
165
  const rawPayload = JSON.stringify(scrubbed)
56
166
  const bounded = Buffer.byteLength(rawPayload, "utf8") <= MAX_EVENT_BYTES
@@ -0,0 +1,44 @@
1
+ const FAST_ROLES = new Set(["executor", "verifier"])
2
+
3
+ export function turboFastPathDecision(taskPolicy = {}, options = {}) {
4
+ const role = String(options.role || "")
5
+ const attempt = Math.max(1, Number(options.attempt || 1))
6
+ const browserRequested = options.browserRequested === true
7
+ const visualRequired = options.visualRequired === true
8
+ const reasons = []
9
+
10
+ if (attempt !== 1) reasons.push("recovery-attempt")
11
+ if (taskPolicy.executionProfile !== "fast") reasons.push("not-fast-profile")
12
+ if (taskPolicy.singleFileBounded !== true) reasons.push("not-single-file-bounded")
13
+ if (String(taskPolicy.risk || "").toLowerCase() !== "low") reasons.push("risk-not-low")
14
+ if (taskPolicy.requireIntegrationVerification === true) reasons.push("integration-required")
15
+ if (!FAST_ROLES.has(role)) reasons.push("role-not-fast-lane")
16
+ if (browserRequested) reasons.push("browser-required")
17
+ if (visualRequired) reasons.push("visual-required")
18
+
19
+ const eligible = reasons.length === 0
20
+ return {
21
+ schemaVersion: 1,
22
+ eligible,
23
+ strategy: eligible ? "single-model-deterministic-first" : "standard",
24
+ deterministicFirst: eligible,
25
+ verifierOnDemand: eligible,
26
+ failClosed: true,
27
+ maxModelLanes: eligible ? 1 : null,
28
+ reasons,
29
+ }
30
+ }
31
+
32
+ export function turboFastTimeoutBudget(options = {}) {
33
+ const number = (value, fallback, min, max) => {
34
+ const parsed = Number(value)
35
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback
36
+ return Math.max(min, Math.min(max, Math.trunc(parsed)))
37
+ }
38
+ return {
39
+ hardTimeoutMs: number(options.hardTimeoutMs, 180_000, 30_000, 10 * 60_000),
40
+ idleTimeoutMs: number(options.idleTimeoutMs, 60_000, 20_000, 5 * 60_000),
41
+ postToolErrorIdleTimeoutMs: number(options.postToolErrorIdleTimeoutMs, 30_000, 5_000, 2 * 60_000),
42
+ verificationTimeoutSec: number(options.verificationTimeoutSec, 90, 30, 300),
43
+ }
44
+ }
@@ -2,20 +2,11 @@ import { createHash } from "node:crypto"
2
2
  import { lstatSync, readFileSync, readlinkSync } from "node:fs"
3
3
  import { spawnSync } from "node:child_process"
4
4
  import path from "node:path"
5
+ import { UES_RUNTIME_DIRS } from "./runtime-artifacts.mjs"
5
6
 
6
- const RUNTIME_DIRS = [
7
- ".ues-cache",
8
- ".ues-traces",
9
- ".ues-work",
10
- ".ues-learning",
11
- ".ues-dashboard",
12
- ".ues-sandboxes",
13
- ".ues-memory",
14
- ".ues-evals",
15
- ]
16
7
  const RUNTIME_PATHSPECS = [
17
8
  ".",
18
- ...RUNTIME_DIRS.map((dir) => `:(exclude)${dir}/**`),
9
+ ...UES_RUNTIME_DIRS.map((dir) => `:(exclude)${dir}/**`),
19
10
  ]
20
11
 
21
12
  let nonGitNonce = 0
@@ -0,0 +1,49 @@
1
+ import { spawnSync } from "node:child_process"
2
+ import path from "node:path"
3
+
4
+ function runGit(cwd, args) {
5
+ return spawnSync("git", ["-C", cwd, ...args], {
6
+ encoding: "utf8",
7
+ windowsHide: true,
8
+ shell: false,
9
+ })
10
+ }
11
+
12
+ export function resolveGitWorkspaceRoot(start = process.cwd()) {
13
+ const requested = path.resolve(String(start || process.cwd()))
14
+ const inside = runGit(requested, ["rev-parse", "--is-inside-work-tree"])
15
+ if (inside.status !== 0 || String(inside.stdout || "").trim() !== "true") {
16
+ return {
17
+ ok: false,
18
+ requested,
19
+ root: null,
20
+ error: String(inside.stderr || inside.stdout || "not a git worktree").trim(),
21
+ }
22
+ }
23
+
24
+ const top = runGit(requested, ["rev-parse", "--show-toplevel"])
25
+ if (top.status !== 0) {
26
+ return {
27
+ ok: false,
28
+ requested,
29
+ root: null,
30
+ error: String(top.stderr || top.stdout || "could not resolve git root").trim(),
31
+ }
32
+ }
33
+
34
+ const root = path.resolve(String(top.stdout || "").trim())
35
+ return { ok: true, requested, root, error: null }
36
+ }
37
+
38
+ export function requireGitWorkspaceRoot(start = process.cwd(), purpose = "UES") {
39
+ const resolved = resolveGitWorkspaceRoot(start)
40
+ if (resolved.ok) return resolved.root
41
+
42
+ const detail = resolved.error ? ": " + resolved.error : ""
43
+ throw new Error(
44
+ purpose +
45
+ " requires Pi to be opened inside a Git repository. Refusing to create UES runtime artifacts in " +
46
+ resolved.requested +
47
+ detail,
48
+ )
49
+ }