opencode-agent-skill 14.4.0 → 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 (44) hide show
  1. package/README.md +33 -15
  2. package/docs/PI-COMPAT.md +151 -2
  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 +17 -0
  14. package/lib/completion-auditor.mjs +25 -5
  15. package/lib/evidence-store.mjs +92 -8
  16. package/lib/execution-contract.mjs +579 -0
  17. package/lib/leaf-runtime-optimizer.mjs +76 -0
  18. package/lib/pi-child-invocation.mjs +65 -0
  19. package/lib/pi-rpc-pool.mjs +61 -9
  20. package/lib/plan-salvage.mjs +124 -0
  21. package/lib/planning-speed-policy.mjs +131 -0
  22. package/lib/repo-graph.mjs +1 -1
  23. package/lib/repo-inspect.mjs +1 -0
  24. package/lib/runtime-artifacts.mjs +42 -0
  25. package/lib/runtime-events.mjs +33 -1
  26. package/lib/safety.mjs +1 -0
  27. package/lib/semantic-index.mjs +1 -1
  28. package/lib/service-manager.mjs +526 -0
  29. package/lib/session-display.mjs +49 -0
  30. package/lib/task-graph.mjs +87 -0
  31. package/lib/task-policy.mjs +150 -4
  32. package/lib/trajectory.mjs +111 -1
  33. package/lib/turbo-fast-path.mjs +44 -0
  34. package/lib/workspace-fingerprint.mjs +2 -11
  35. package/lib/workspace-root.mjs +49 -0
  36. package/lib/worktree-sandbox.mjs +234 -11
  37. package/package.json +8 -5
  38. package/pi/extensions/ues-child-runtime.ts +145 -2
  39. package/pi/extensions/ues.ts +1760 -160
  40. package/scripts/check-release-consistency.mjs +25 -2
  41. package/scripts/check-source-integrity.mjs +471 -4
  42. package/scripts/eval-pi.mjs +76 -36
  43. package/scripts/smoke-pi-extension.mjs +1 -1
  44. package/pi/prompts/ues-run.md +0 -17
@@ -2,12 +2,32 @@ 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
7
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
8
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
9
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
10
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
+
11
31
  function referencedSourceFiles(value) {
12
32
  const files = []
13
33
  const seen = new Set()
@@ -47,6 +67,107 @@ function riskTextFor(value) {
47
67
  .replace(/chỉ\s+đọc[^.\n]*/gi, "")
48
68
  }
49
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
+
50
171
  function signal(name, matched, weight) {
51
172
  return matched ? { name, weight } : null
52
173
  }
@@ -185,6 +306,17 @@ export function shouldRunDedicatedDiagnosis(taskPolicy = {}, attempt = 1) {
185
306
  return recoveryPolicyForAttempt(taskPolicy, normalizedAttempt).requireDiagnosis === true
186
307
  }
187
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
+
188
320
  return taskPolicy.executionProfile !== "fast" || taskPolicy.risk !== "low"
189
321
  }
190
322
 
@@ -192,7 +324,8 @@ export function classifyEngineeringTask(text, facts = {}) {
192
324
  const value = String(text || "")
193
325
  const riskText = riskTextFor(value)
194
326
  const sensitiveDomain = SENSITIVE_DOMAIN.test(value)
195
- const singleFileBounded = singleFileBoundedTask(value, facts)
327
+ const readOnly = readOnlyTask(value, facts)
328
+ const singleFileBounded = readOnly ? false : singleFileBoundedTask(value, facts)
196
329
  const rawSensitiveMutation = HIGH_RISK_MUTATION.test(riskText)
197
330
  const sensitiveMutation =
198
331
  rawSensitiveMutation &&
@@ -223,6 +356,7 @@ export function classifyEngineeringTask(text, facts = {}) {
223
356
  signal("declared-high-risk", declaredHighRisk, 2),
224
357
  signal("explicit-long-horizon", explicitLongHorizon, 2),
225
358
  signal("debugging", DEBUG.test(value), 1),
359
+ signal("read-only", readOnly, -1),
226
360
  signal("single-file-bounded", singleFileBounded, -2),
227
361
  signal("public-contract", CONTRACT.test(value) || facts.hasPublicContract, 2),
228
362
  signal("data-migration", dataMigration, 2),
@@ -234,6 +368,7 @@ export function classifyEngineeringTask(text, facts = {}) {
234
368
 
235
369
  const score = Math.max(0, signals.reduce((sum, item) => sum + item.weight, 0))
236
370
  const highRisk = declaredHighRisk || sensitiveMutation || Boolean(facts.hasMigration) || Boolean(facts.hasPublicContract)
371
+ const diagnosisEvidence = facts.failureEvidence === true || CONCRETE_DIAGNOSIS.test(value)
237
372
  const risk = highRisk ? "high" : singleFileBounded ? "low" : score >= 3 ? "medium" : "low"
238
373
  const mode =
239
374
  explicitLongHorizon
@@ -270,11 +405,22 @@ export function classifyEngineeringTask(text, facts = {}) {
270
405
  maxAttempts,
271
406
  contextBudget: profile.contextBudget,
272
407
  maxSkills: profile.maxSkills,
273
- requirePlanCheck: profile.durableState || risk === "high",
274
- requireIntegrationVerification: mode !== "inline" || risk === "high",
408
+ requirePlanCheck: readOnly ? false : profile.durableState || risk === "high",
409
+ requireIntegrationVerification: readOnly ? false : mode !== "inline" || risk === "high",
275
410
  requireFreshEvidence: true,
411
+ requireBehavioralReceipt: !readOnly,
412
+ readOnly,
276
413
  singleFileBounded,
277
- profile,
414
+ diagnosisEvidence,
415
+ profile: readOnly
416
+ ? {
417
+ ...profile,
418
+ worktree: "off",
419
+ verification: "command-evidence",
420
+ fullCI: false,
421
+ containerVerification: "off",
422
+ }
423
+ : profile,
278
424
  domains: [...new Set(domains)],
279
425
  recovery: {
280
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
+ }