opencode-agent-skill 14.4.0 → 15.1.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 (53) hide show
  1. package/README.md +34 -16
  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 +38 -5
  13. package/lib/capability-fabric.mjs +17 -0
  14. package/lib/code-intelligence/index.mjs +3 -2
  15. package/lib/code-intelligence/lsp-provider.mjs +326 -52
  16. package/lib/compaction-resume-guard.mjs +255 -0
  17. package/lib/completion-auditor.mjs +25 -5
  18. package/lib/context-engine-v11.mjs +20 -7
  19. package/lib/evidence-store.mjs +92 -8
  20. package/lib/execution-contract.mjs +592 -0
  21. package/lib/leaf-runtime-optimizer.mjs +76 -0
  22. package/lib/memory-engine.mjs +189 -16
  23. package/lib/performance-fabric.mjs +262 -107
  24. package/lib/pi-child-invocation.mjs +65 -0
  25. package/lib/pi-rpc-pool.mjs +61 -9
  26. package/lib/plan-salvage.mjs +124 -0
  27. package/lib/planning-speed-policy.mjs +131 -0
  28. package/lib/repo-graph.mjs +1 -1
  29. package/lib/repo-inspect.mjs +1 -0
  30. package/lib/runtime-artifacts.mjs +42 -0
  31. package/lib/runtime-events.mjs +33 -1
  32. package/lib/safety.mjs +1 -0
  33. package/lib/semantic-index.mjs +1 -1
  34. package/lib/service-manager.mjs +526 -0
  35. package/lib/session-display.mjs +49 -0
  36. package/lib/subagent-artifacts.mjs +182 -0
  37. package/lib/task-graph.mjs +87 -0
  38. package/lib/task-policy.mjs +150 -4
  39. package/lib/trajectory.mjs +111 -1
  40. package/lib/turbo-fast-path.mjs +44 -0
  41. package/lib/verification-broker.mjs +32 -27
  42. package/lib/workspace-fingerprint.mjs +45 -19
  43. package/lib/workspace-hygiene.mjs +481 -0
  44. package/lib/workspace-root.mjs +49 -0
  45. package/lib/worktree-sandbox.mjs +234 -11
  46. package/package.json +8 -5
  47. package/pi/extensions/ues-child-runtime.ts +170 -4
  48. package/pi/extensions/ues.ts +2063 -188
  49. package/scripts/check-release-consistency.mjs +25 -2
  50. package/scripts/check-source-integrity.mjs +471 -4
  51. package/scripts/eval-pi.mjs +76 -36
  52. package/scripts/smoke-pi-extension.mjs +1 -1
  53. package/pi/prompts/ues-run.md +0 -17
@@ -0,0 +1,182 @@
1
+ import { createHash, randomUUID } from "node:crypto"
2
+ import { existsSync } from "node:fs"
3
+ import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises"
4
+ import path from "node:path"
5
+ import { putEvidence } from "./evidence-store.mjs"
6
+
7
+ const ARTIFACT_DIR = path.join(".ues-work", ".subagents")
8
+ const HANDLE_PATTERN = /^sa-[a-z0-9]+-[a-f0-9]{8}$/
9
+
10
+ function now() {
11
+ return new Date().toISOString()
12
+ }
13
+
14
+ function hash(value) {
15
+ return createHash("sha256").update(String(value || "")).digest("hex").slice(0, 20)
16
+ }
17
+
18
+ function artifactDir(root) {
19
+ return path.join(path.resolve(root), ARTIFACT_DIR)
20
+ }
21
+
22
+ function artifactPath(root, handle) {
23
+ if (!HANDLE_PATTERN.test(String(handle || ""))) throw new Error("invalid subagent handle")
24
+ return path.join(artifactDir(root), handle + ".json")
25
+ }
26
+
27
+ async function atomicJson(file, value) {
28
+ await mkdir(path.dirname(file), { recursive: true })
29
+ const temp = file + "." + process.pid + "." + Date.now() + ".tmp"
30
+ await writeFile(temp, JSON.stringify(value, null, 2) + "\n", "utf8")
31
+ try {
32
+ await rename(temp, file)
33
+ } catch (error) {
34
+ await rm(temp, { force: true }).catch(() => {})
35
+ throw error
36
+ }
37
+ }
38
+
39
+ function publicArtifact(root, artifact) {
40
+ return {
41
+ ...artifact,
42
+ file: path.relative(path.resolve(root), artifactPath(root, artifact.handle)).replaceAll("\\", "/"),
43
+ }
44
+ }
45
+
46
+ export async function createSubagentArtifact(root, input = {}) {
47
+ const handle = "sa-" + Date.now().toString(36) + "-" + randomUUID().replaceAll("-", "").slice(0, 8)
48
+ const task = String(input.task || "")
49
+ const taskEvidence = await putEvidence(root, task, {
50
+ kind: "subagent-task",
51
+ source: handle,
52
+ summary: `Exact delegated task for ${input.agent || "UES child"}`,
53
+ })
54
+ const timestamp = now()
55
+ const artifact = {
56
+ schemaVersion: 1,
57
+ handle,
58
+ status: "running",
59
+ agent: input.agent || null,
60
+ role: input.role || null,
61
+ attempt: Math.max(1, Number(input.attempt || 1)),
62
+ traceID: input.traceID || null,
63
+ model: input.model || null,
64
+ modelTier: input.modelTier || null,
65
+ workspaceFingerprint: input.workspaceFingerprint || null,
66
+ taskHash: hash(task),
67
+ taskPreview: task.replace(/\s+/g, " ").trim().slice(0, 1200),
68
+ taskRef: taskEvidence.ref,
69
+ startedAt: timestamp,
70
+ updatedAt: timestamp,
71
+ completedAt: null,
72
+ result: null,
73
+ resume: {
74
+ state: "running",
75
+ taskRef: taskEvidence.ref,
76
+ outputRef: null,
77
+ instruction: "Inspect this handle for status. Do not infer completion until status and verification evidence say so.",
78
+ },
79
+ }
80
+ await atomicJson(artifactPath(root, handle), artifact)
81
+ return publicArtifact(root, artifact)
82
+ }
83
+
84
+ export async function readSubagentArtifact(root, handle) {
85
+ const file = artifactPath(root, handle)
86
+ const parsed = JSON.parse(await readFile(file, "utf8"))
87
+ return publicArtifact(root, parsed)
88
+ }
89
+
90
+ export async function finalizeSubagentArtifact(root, handle, result = {}) {
91
+ const current = await readSubagentArtifact(root, handle)
92
+ const output = String(result.output || "")
93
+ const outputEvidence = output
94
+ ? await putEvidence(root, output, {
95
+ kind: "subagent-output",
96
+ source: handle,
97
+ summary: `Exact bounded child output for ${current.agent || "UES child"}`,
98
+ })
99
+ : null
100
+ const aborted = Number(result.exitCode) === 130 || String(result.stopReason || "").toLowerCase() === "aborted"
101
+ const status = aborted ? "aborted" : Number(result.exitCode || 0) === 0 ? "completed" : "failed"
102
+ const timestamp = now()
103
+ const artifact = {
104
+ ...current,
105
+ file: undefined,
106
+ status,
107
+ updatedAt: timestamp,
108
+ completedAt: timestamp,
109
+ result: {
110
+ exitCode: Number(result.exitCode ?? 1),
111
+ verdict: result.verdict || null,
112
+ stopReason: result.stopReason || null,
113
+ durationMs: Number(result.durationMs || 0),
114
+ toolCalls: Number(result.toolCalls || 0),
115
+ toolNames: Array.isArray(result.toolNames) ? result.toolNames.slice(0, 40) : [],
116
+ childRuntime: result.childRuntime || null,
117
+ workerReused: result.workerReused === true,
118
+ outputRef: outputEvidence?.ref || null,
119
+ outputPreview: output.replace(/\s+/g, " ").trim().slice(0, 1000),
120
+ },
121
+ resume: {
122
+ state: status,
123
+ taskRef: current.taskRef,
124
+ outputRef: outputEvidence?.ref || null,
125
+ instruction: status === "completed"
126
+ ? "Reuse the output evidence by reference; do not replay the child unless new evidence or a new task requires it."
127
+ : "Inspect task/output evidence and dispatch a fresh bounded child attempt from this handle's exact task context; do not trust partial narrative state.",
128
+ },
129
+ }
130
+ await atomicJson(artifactPath(root, handle), artifact)
131
+ return publicArtifact(root, artifact)
132
+ }
133
+
134
+ export async function failSubagentArtifact(root, handle, error) {
135
+ const current = await readSubagentArtifact(root, handle)
136
+ const timestamp = now()
137
+ const artifact = {
138
+ ...current,
139
+ file: undefined,
140
+ status: "failed",
141
+ updatedAt: timestamp,
142
+ completedAt: timestamp,
143
+ result: {
144
+ exitCode: 1,
145
+ verdict: "FAIL",
146
+ stopReason: "launch-error",
147
+ error: String(error instanceof Error ? error.message : error).slice(0, 2000),
148
+ outputRef: null,
149
+ },
150
+ resume: {
151
+ state: "failed",
152
+ taskRef: current.taskRef,
153
+ outputRef: null,
154
+ instruction: "The child did not complete. Re-dispatch from taskRef after diagnosing the launch/runtime failure.",
155
+ },
156
+ }
157
+ await atomicJson(artifactPath(root, handle), artifact)
158
+ return publicArtifact(root, artifact)
159
+ }
160
+
161
+ export async function listSubagentArtifacts(root, options = {}) {
162
+ const dir = artifactDir(root)
163
+ if (!existsSync(dir)) return { schemaVersion: 1, root: dir, count: 0, artifacts: [] }
164
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => [])
165
+ const rows = []
166
+ for (const entry of entries) {
167
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue
168
+ try {
169
+ const parsed = JSON.parse(await readFile(path.join(dir, entry.name), "utf8"))
170
+ if (!HANDLE_PATTERN.test(String(parsed.handle || ""))) continue
171
+ rows.push(publicArtifact(root, parsed))
172
+ } catch {}
173
+ }
174
+ rows.sort((a, b) => Date.parse(b.updatedAt || b.startedAt || "") - Date.parse(a.updatedAt || a.startedAt || "") || a.handle.localeCompare(b.handle))
175
+ const limit = Math.max(1, Math.min(100, Number(options.limit || 20)))
176
+ return {
177
+ schemaVersion: 1,
178
+ root: path.relative(path.resolve(root), dir).replaceAll("\\", "/"),
179
+ count: rows.length,
180
+ artifacts: rows.slice(0, limit),
181
+ }
182
+ }
@@ -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,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
+ }