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.
- package/README.md +34 -16
- package/docs/PI-COMPAT.md +151 -2
- package/docs/V15-MANAGED-RUNTIME.md +128 -0
- package/global-config/agents/architect.md +63 -1
- package/global-config/agents/executor.md +7 -0
- package/global-config/agents/plan-checker.md +8 -1
- package/global-config/agents/verifier.md +3 -3
- package/global-config/skills/database-engineering/references/workflow.md +15 -0
- package/global-config/skills/engineering-orchestrator/references/long-horizon.md +21 -11
- package/lib/activity-deadline.mjs +70 -0
- package/lib/adaptive-context-budget.mjs +25 -5
- package/lib/affected-tests.mjs +38 -5
- package/lib/capability-fabric.mjs +17 -0
- package/lib/code-intelligence/index.mjs +3 -2
- package/lib/code-intelligence/lsp-provider.mjs +326 -52
- package/lib/compaction-resume-guard.mjs +255 -0
- package/lib/completion-auditor.mjs +25 -5
- package/lib/context-engine-v11.mjs +20 -7
- package/lib/evidence-store.mjs +92 -8
- package/lib/execution-contract.mjs +592 -0
- package/lib/leaf-runtime-optimizer.mjs +76 -0
- package/lib/memory-engine.mjs +189 -16
- package/lib/performance-fabric.mjs +262 -107
- package/lib/pi-child-invocation.mjs +65 -0
- package/lib/pi-rpc-pool.mjs +61 -9
- package/lib/plan-salvage.mjs +124 -0
- package/lib/planning-speed-policy.mjs +131 -0
- package/lib/repo-graph.mjs +1 -1
- package/lib/repo-inspect.mjs +1 -0
- package/lib/runtime-artifacts.mjs +42 -0
- package/lib/runtime-events.mjs +33 -1
- package/lib/safety.mjs +1 -0
- package/lib/semantic-index.mjs +1 -1
- package/lib/service-manager.mjs +526 -0
- package/lib/session-display.mjs +49 -0
- package/lib/subagent-artifacts.mjs +182 -0
- package/lib/task-graph.mjs +87 -0
- package/lib/task-policy.mjs +150 -4
- package/lib/trajectory.mjs +111 -1
- package/lib/turbo-fast-path.mjs +44 -0
- package/lib/verification-broker.mjs +32 -27
- package/lib/workspace-fingerprint.mjs +45 -19
- package/lib/workspace-hygiene.mjs +481 -0
- package/lib/workspace-root.mjs +49 -0
- package/lib/worktree-sandbox.mjs +234 -11
- package/package.json +8 -5
- package/pi/extensions/ues-child-runtime.ts +170 -4
- package/pi/extensions/ues.ts +2063 -188
- package/scripts/check-release-consistency.mjs +25 -2
- package/scripts/check-source-integrity.mjs +471 -4
- package/scripts/eval-pi.mjs +76 -36
- package/scripts/smoke-pi-extension.mjs +1 -1
- 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
|
+
}
|
package/lib/task-graph.mjs
CHANGED
|
@@ -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
|
package/lib/task-policy.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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,
|
package/lib/trajectory.mjs
CHANGED
|
@@ -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
|
+
}
|