opencode-agent-skill 13.0.0-beta.2 → 14.2.0-beta.1
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 +1556 -607
- package/bin/ocskill.mjs +172 -24
- package/docs/OPENCODE-COMPAT.md +34 -97
- package/docs/PI-COMPAT.md +188 -0
- package/docs/V14-CONTEXT-MEMORY-FABRIC.md +70 -0
- package/docs/V14.1-QUALITY-PERFORMANCE-FABRIC.md +114 -0
- package/docs/V14.2-TURBO-WEAK-MODEL-RUNTIME.md +448 -0
- package/evals/v14/tasks.json +46 -0
- package/global-config/agents/executor.md +7 -0
- package/global-config/agents/visual-verifier.md +22 -3
- package/global-config/plugins/ues-router/index.js +13 -8
- package/global-config/plugins/ues-router/policy-runtime.js +7 -0
- package/global-config/plugins/ues-router/router.js +12 -2
- package/global-config/skills/ecommerce-engineering/SKILL.md +1 -1
- package/global-config/skills/file-upload-engineering/SKILL.md +1 -1
- package/global-config/skills/git-safety/SKILL.md +1 -1
- package/global-config/skills/nestjs-engineering/SKILL.md +1 -1
- package/global-config/skills/performance-engineering/SKILL.md +1 -1
- package/global-config/skills/react-native-engineering/SKILL.md +1 -1
- package/global-config/skills/rest-api-design/SKILL.md +1 -1
- package/global-config/skills/ui-ux-engineering/SKILL.md +1 -1
- package/lib/adaptive-context-budget.mjs +97 -0
- package/lib/affected-tests.mjs +260 -0
- package/lib/benchmark-confidence.mjs +41 -2
- package/lib/browser-mcp-routing.mjs +166 -0
- package/lib/capability-fabric.mjs +336 -0
- package/lib/capability-registry.mjs +9 -0
- package/lib/context-engine-v11.mjs +65 -1
- package/lib/context-graph-rank.mjs +118 -0
- package/lib/context-manifest.mjs +97 -18
- package/lib/control-center.mjs +19 -1
- package/lib/dynamic-workflow.mjs +3 -1
- package/lib/evidence-store.mjs +82 -1
- package/lib/hierarchical-context.mjs +215 -0
- package/lib/memory-engine.mjs +465 -0
- package/lib/model-performance.mjs +33 -8
- package/lib/model-policy.mjs +3 -3
- package/lib/orchestrator-policy.mjs +5 -209
- package/lib/performance-fabric.mjs +229 -0
- package/lib/pi-rpc-pool.mjs +433 -0
- package/lib/process-hang-detector.mjs +83 -0
- package/lib/process-supervisor.mjs +193 -0
- package/lib/prompt-cache.mjs +2 -0
- package/lib/repo-graph.mjs +53 -2
- package/lib/runtime-config.mjs +31 -0
- package/lib/safety.mjs +132 -0
- package/lib/semantic-index.mjs +52 -3
- package/lib/skill-compiler.mjs +128 -0
- package/lib/skill-quality.mjs +48 -2
- package/lib/task-engine.mjs +66 -5
- package/lib/task-policy.mjs +235 -0
- package/lib/verification-broker.mjs +284 -0
- package/lib/verification-command.mjs +111 -0
- package/lib/windows-shim.mjs +35 -0
- package/lib/workspace-fingerprint.mjs +198 -0
- package/package.json +52 -42
- package/pi/extensions/ues-child-runtime.ts +238 -0
- package/pi/extensions/ues.ts +3200 -0
- package/pi/prompts/ues-audit.md +9 -0
- package/pi/prompts/ues-critique.md +9 -0
- package/pi/prompts/ues-debug.md +9 -0
- package/pi/prompts/ues-feature.md +9 -0
- package/pi/prompts/ues-fix.md +9 -0
- package/pi/prompts/ues-plan.md +9 -0
- package/pi/prompts/ues-research.md +9 -0
- package/pi/prompts/ues-resume.md +9 -0
- package/pi/prompts/ues-review.md +7 -0
- package/pi/prompts/ues-run.md +17 -0
- package/pi/prompts/ues-verify.md +9 -0
- package/scripts/check-release-consistency.mjs +119 -185
- package/scripts/check-runtime-exports.mjs +66 -0
- package/scripts/check-source-integrity.mjs +184 -0
- package/scripts/eval-pi.mjs +492 -0
- package/scripts/install.mjs +16 -0
- package/scripts/smoke-package-closure.mjs +110 -0
- package/scripts/smoke-packed-install.mjs +24 -11
- package/scripts/smoke-pi-extension.mjs +144 -0
- package/scripts/uninstall.mjs +44 -0
- package/CHANGELOG.md +0 -415
- package/docs/DETERMINISTIC-TOOLS.md +0 -105
- package/docs/ENGINEERING-DESIGN.md +0 -194
- package/docs/EVALS.md +0 -158
- package/docs/GITHUB-RULESET.md +0 -50
- package/docs/NPM-PUBLISH.md +0 -116
- package/docs/RESEARCH-SOURCES.md +0 -37
- package/docs/TRACE-SCHEMA.md +0 -122
- package/docs/V11-PERCEPTION-ADAPTIVE-EXECUTION.md +0 -75
- package/docs/V11-PERCEPTION-ADAPTIVE.md +0 -220
- package/docs/V12-WEAK-MODEL-INTELLIGENCE.md +0 -27
- package/docs/V13-PARALLEL-WEAK-MODEL-RUNTIME.md +0 -86
- package/docs/V7-INTELLIGENCE-RUNTIME.md +0 -166
- package/docs/V8-INTELLIGENCE-RELIABILITY.md +0 -206
- package/docs/V9-SPEED-INTELLIGENCE.md +0 -102
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
const SENSITIVE_DOMAIN = /(\bauth\b|authorization|authentication|security|permission|payment|schema|database|production|deploy|public api|secret|credential|phân quyền|bảo mật|thanh toán|cơ sở dữ liệu|triển khai|api công khai|bí mật|thông tin xác thực)/i
|
|
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
|
+
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
|
+
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 CONTRACT = /(public api|api contract|openapi|response schema|request schema|breaking api|hợp đồng api|api công khai)/i
|
|
6
|
+
const DATA = /(database|sql|migration|schema|transaction|index|cơ sở dữ liệu|dữ liệu|migrate)/i
|
|
7
|
+
|
|
8
|
+
function riskTextFor(value) {
|
|
9
|
+
return String(value || "")
|
|
10
|
+
.replace(/\b(?:do not|don't|without)\s+(?:edit|modify|change|write|delete|remove)[^.\n]*/gi, "")
|
|
11
|
+
.replace(/\b(?:no|read[- ]only)\s+(?:edits?|changes?|writes?)[^.\n]*/gi, "")
|
|
12
|
+
.replace(/không\s+(?:sửa|chỉnh sửa|thay đổi|ghi|xóa|xoá)[^.\n]*/gi, "")
|
|
13
|
+
.replace(/chỉ\s+đọc[^.\n]*/gi, "")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function signal(name, matched, weight) {
|
|
17
|
+
return matched ? { name, weight } : null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function profileFor(mode, risk) {
|
|
21
|
+
if (mode === "inline" && risk === "low") {
|
|
22
|
+
return {
|
|
23
|
+
name: "fast",
|
|
24
|
+
maxSkills: 2,
|
|
25
|
+
contextBudget: 8_000,
|
|
26
|
+
contextStrategy: "incremental-semantic",
|
|
27
|
+
skillLoading: "direct-only",
|
|
28
|
+
durableState: false,
|
|
29
|
+
worktree: "off",
|
|
30
|
+
critic: "off",
|
|
31
|
+
verification: "targeted",
|
|
32
|
+
fullCI: false,
|
|
33
|
+
containerVerification: "off",
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (mode === "long-horizon" || risk === "high") {
|
|
37
|
+
return {
|
|
38
|
+
name: "deep",
|
|
39
|
+
maxSkills: 5,
|
|
40
|
+
contextBudget: 48_000,
|
|
41
|
+
contextStrategy: "semantic+graph+git",
|
|
42
|
+
skillLoading: "orchestrated",
|
|
43
|
+
durableState: true,
|
|
44
|
+
worktree: "auto-writers",
|
|
45
|
+
critic: "required-for-high-risk-or-final",
|
|
46
|
+
verification: "targeted+integration",
|
|
47
|
+
fullCI: true,
|
|
48
|
+
containerVerification: risk === "high" ? "preferred-if-capable" : "optional",
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
name: "standard",
|
|
53
|
+
maxSkills: 4,
|
|
54
|
+
contextBudget: 20_000,
|
|
55
|
+
contextStrategy: "incremental-semantic+git",
|
|
56
|
+
skillLoading: "selective",
|
|
57
|
+
durableState: false,
|
|
58
|
+
worktree: "auto-on-conflict",
|
|
59
|
+
critic: "on-failure-or-elevated-risk",
|
|
60
|
+
verification: "targeted+affected",
|
|
61
|
+
fullCI: false,
|
|
62
|
+
containerVerification: "off",
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function boundedInt(value, fallback, min, max) {
|
|
67
|
+
const parsed = Number(value)
|
|
68
|
+
if (!Number.isFinite(parsed)) return fallback
|
|
69
|
+
return Math.min(max, Math.max(min, Math.round(parsed)))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function recoveryPolicyForAttempt(taskPolicy = {}, attempt = 1) {
|
|
73
|
+
const normalizedAttempt = boundedInt(attempt, 1, 1, 99)
|
|
74
|
+
const baseBudget = boundedInt(
|
|
75
|
+
taskPolicy.contextBudget ?? taskPolicy.profile?.contextBudget,
|
|
76
|
+
20_000,
|
|
77
|
+
4_000,
|
|
78
|
+
48_000,
|
|
79
|
+
)
|
|
80
|
+
const baseSkills = boundedInt(
|
|
81
|
+
taskPolicy.maxSkills ?? taskPolicy.profile?.maxSkills,
|
|
82
|
+
4,
|
|
83
|
+
1,
|
|
84
|
+
5,
|
|
85
|
+
)
|
|
86
|
+
const baseStrategy = taskPolicy.profile?.contextStrategy || "incremental-semantic+git"
|
|
87
|
+
|
|
88
|
+
if (normalizedAttempt <= 1) {
|
|
89
|
+
return {
|
|
90
|
+
schemaVersion: 1,
|
|
91
|
+
stage: "initial",
|
|
92
|
+
attempt: normalizedAttempt,
|
|
93
|
+
contextBudget: baseBudget,
|
|
94
|
+
maxSkills: baseSkills,
|
|
95
|
+
contextStrategy: baseStrategy,
|
|
96
|
+
requireDiagnosis: false,
|
|
97
|
+
requireCritic: false,
|
|
98
|
+
modelEscalation: false,
|
|
99
|
+
directives: [],
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (normalizedAttempt === 2) {
|
|
104
|
+
return {
|
|
105
|
+
schemaVersion: 1,
|
|
106
|
+
stage: "diagnose",
|
|
107
|
+
attempt: normalizedAttempt,
|
|
108
|
+
contextBudget: Math.min(48_000, Math.max(baseBudget, Math.round(baseBudget * 1.35))),
|
|
109
|
+
maxSkills: Math.min(5, baseSkills + 1),
|
|
110
|
+
contextStrategy:
|
|
111
|
+
taskPolicy.executionProfile === "fast"
|
|
112
|
+
? "incremental-semantic+git"
|
|
113
|
+
: baseStrategy,
|
|
114
|
+
requireDiagnosis: true,
|
|
115
|
+
requireCritic: false,
|
|
116
|
+
modelEscalation: true,
|
|
117
|
+
directives: [
|
|
118
|
+
"reproduce or capture the exact previous failure before editing",
|
|
119
|
+
"inspect the direct caller, nearest test and failure-adjacent evidence",
|
|
120
|
+
"do not stack another speculative patch on top of the failed attempt",
|
|
121
|
+
],
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
schemaVersion: 1,
|
|
127
|
+
stage: "deep-recovery",
|
|
128
|
+
attempt: normalizedAttempt,
|
|
129
|
+
contextBudget: Math.min(48_000, Math.max(20_000, Math.round(baseBudget * 1.75))),
|
|
130
|
+
maxSkills: Math.min(5, baseSkills + 2),
|
|
131
|
+
contextStrategy: "semantic+graph+git",
|
|
132
|
+
requireDiagnosis: true,
|
|
133
|
+
requireCritic: true,
|
|
134
|
+
modelEscalation: true,
|
|
135
|
+
directives: [
|
|
136
|
+
"re-investigate from fresh evidence and explicitly reject the failed hypothesis",
|
|
137
|
+
"expand to callers, dependencies, tests and boundary contracts before editing",
|
|
138
|
+
"challenge the architecture or coupling if repeated fixes expose a wider problem",
|
|
139
|
+
"run an independent critic or review pass before accepting the recovery",
|
|
140
|
+
],
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function shouldRunDedicatedDiagnosis(taskPolicy = {}, attempt = 1) {
|
|
145
|
+
const hasDebugSignal = Array.isArray(taskPolicy.signals) &&
|
|
146
|
+
taskPolicy.signals.some((item) => item?.name === "debugging")
|
|
147
|
+
if (!hasDebugSignal) return false
|
|
148
|
+
|
|
149
|
+
const normalizedAttempt = boundedInt(attempt, 1, 1, 99)
|
|
150
|
+
if (normalizedAttempt > 1) {
|
|
151
|
+
return recoveryPolicyForAttempt(taskPolicy, normalizedAttempt).requireDiagnosis === true
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return taskPolicy.executionProfile !== "fast" || taskPolicy.risk !== "low"
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function classifyEngineeringTask(text, facts = {}) {
|
|
158
|
+
const value = String(text || "")
|
|
159
|
+
const riskText = riskTextFor(value)
|
|
160
|
+
const sensitiveDomain = SENSITIVE_DOMAIN.test(value)
|
|
161
|
+
const sensitiveMutation = HIGH_RISK_MUTATION.test(riskText)
|
|
162
|
+
const declaredHighRisk =
|
|
163
|
+
["high", "critical"].includes(String(facts.risk || "").toLowerCase()) ||
|
|
164
|
+
/\b(?:risk|rủi ro)\s*[:=\/-]?\s*(?:high|critical|cao|nghiêm trọng)\b/i.test(value) ||
|
|
165
|
+
/\b(?:high|critical)[-\s]?(?:risk|rủi ro)\b/i.test(value)
|
|
166
|
+
const declaredLongHorizon =
|
|
167
|
+
facts.longHorizon === true ||
|
|
168
|
+
["long", "long-horizon", "deep"].includes(String(facts.mode || "").toLowerCase())
|
|
169
|
+
const compoundLongRisk =
|
|
170
|
+
/\blong\s*[/|,+]\s*(?:high|critical)[-\s]?risk\b/i.test(value) ||
|
|
171
|
+
/\b(?:high|critical)[-\s]?risk\s*[/|,+]\s*long\b/i.test(value)
|
|
172
|
+
const explicitLongHorizon = declaredLongHorizon || compoundLongRisk || LONG.test(value)
|
|
173
|
+
const signals = [
|
|
174
|
+
signal("long-request-text", value.length > 700, 1),
|
|
175
|
+
signal("medium-request-text", value.length > 250, 1),
|
|
176
|
+
signal("high-risk-domain", sensitiveDomain, 1),
|
|
177
|
+
signal("high-risk-operation", sensitiveMutation, 2),
|
|
178
|
+
signal("declared-high-risk", declaredHighRisk, 2),
|
|
179
|
+
signal("explicit-long-horizon", explicitLongHorizon, 2),
|
|
180
|
+
signal("debugging", DEBUG.test(value), 1),
|
|
181
|
+
signal("public-contract", CONTRACT.test(value) || facts.hasPublicContract, 2),
|
|
182
|
+
signal("data-migration", (DATA.test(riskText) && /migration|schema|migrate|di trú|chuyển đổi/i.test(riskText)) || facts.hasMigration, 2),
|
|
183
|
+
signal("many-changed-files", Number(facts.changedFiles || 0) > 5, 1),
|
|
184
|
+
signal("very-many-changed-files", Number(facts.changedFiles || 0) > 12, 1),
|
|
185
|
+
signal("large-repository", Number(facts.repoFiles || 0) > 1500, 1),
|
|
186
|
+
signal("monorepo", facts.monorepo === true, 1),
|
|
187
|
+
].filter(Boolean)
|
|
188
|
+
|
|
189
|
+
const score = signals.reduce((sum, item) => sum + item.weight, 0)
|
|
190
|
+
const highRisk = declaredHighRisk || sensitiveMutation || Boolean(facts.hasMigration) || Boolean(facts.hasPublicContract)
|
|
191
|
+
const risk = highRisk ? "high" : score >= 3 ? "medium" : "low"
|
|
192
|
+
const mode = explicitLongHorizon || score >= 4 ? "long-horizon" : score >= 2 ? "standard" : "inline"
|
|
193
|
+
const modelTier = risk === "high" || mode === "long-horizon" ? "heavy" : score >= 2 ? "standard" : "light"
|
|
194
|
+
const maxAttempts = risk === "high" ? 2 : 3
|
|
195
|
+
const profile = profileFor(mode, risk)
|
|
196
|
+
|
|
197
|
+
const domains = []
|
|
198
|
+
if (/(auth|permission|tenant|token|session|phân quyền|xác thực)/i.test(value)) domains.push("auth-security")
|
|
199
|
+
if (/(payment|checkout|refund|webhook|thanh toán|hoàn tiền)/i.test(value)) domains.push("payment")
|
|
200
|
+
if (DATA.test(value)) domains.push("database")
|
|
201
|
+
if (CONTRACT.test(value)) domains.push("api-contract")
|
|
202
|
+
if (/(react native|expo|android|ios|gradle|xcode)/i.test(value)) domains.push("react-native")
|
|
203
|
+
else if (/(next\.js|nextjs|app router|server component)/i.test(value)) domains.push("nextjs")
|
|
204
|
+
else if (/\breact\b|useeffect|usestate|component/i.test(value)) domains.push("react")
|
|
205
|
+
if (/(docker|kubernetes|terraform|github actions|ci\/cd|deploy|triển khai)/i.test(value)) domains.push("devops")
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
schemaVersion: 4,
|
|
209
|
+
score,
|
|
210
|
+
signals,
|
|
211
|
+
risk,
|
|
212
|
+
mode,
|
|
213
|
+
executionProfile: profile.name,
|
|
214
|
+
modelTier,
|
|
215
|
+
maxAttempts,
|
|
216
|
+
contextBudget: profile.contextBudget,
|
|
217
|
+
maxSkills: profile.maxSkills,
|
|
218
|
+
requirePlanCheck: profile.durableState || risk === "high",
|
|
219
|
+
requireIntegrationVerification: mode !== "inline" || risk === "high",
|
|
220
|
+
requireFreshEvidence: true,
|
|
221
|
+
profile,
|
|
222
|
+
domains: [...new Set(domains)],
|
|
223
|
+
recovery: {
|
|
224
|
+
escalateAfterFailure: true,
|
|
225
|
+
diagnosisBeforePatch: true,
|
|
226
|
+
deepRecoveryFromAttempt: 3,
|
|
227
|
+
},
|
|
228
|
+
antiHallucination: {
|
|
229
|
+
evidenceFirst: true,
|
|
230
|
+
noCompletionWithoutVerification: mode !== "inline" || risk === "high",
|
|
231
|
+
noUnsupportedSemanticClaims: true,
|
|
232
|
+
failClosedOnMissingCapability: risk === "high",
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto"
|
|
4
|
+
import { createVerificationReceipt, validateVerificationReceipt } from "./evidence-receipt.mjs"
|
|
5
|
+
import { getEvidence, putEvidence } from "./evidence-store.mjs"
|
|
6
|
+
import { runtimeWorkspaceFingerprint } from "./workspace-fingerprint.mjs"
|
|
7
|
+
|
|
8
|
+
const CACHE_VERSION = 1
|
|
9
|
+
const CACHE_FILE = "verification-broker-v1.json"
|
|
10
|
+
|
|
11
|
+
const CACHE_WRITE_TAILS = new Map()
|
|
12
|
+
const CACHE_LOCK_SUFFIX = ".lock"
|
|
13
|
+
const CACHE_LOCK_STALE_MS = 5_000
|
|
14
|
+
const CACHE_LOCK_WAIT_MS = 8_000
|
|
15
|
+
|
|
16
|
+
function sleep(ms) {
|
|
17
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function cacheLockPath(root) {
|
|
21
|
+
return cachePath(root) + CACHE_LOCK_SUFFIX
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function withCrossProcessCacheLock(root, fn) {
|
|
25
|
+
const lockDir = cacheLockPath(root)
|
|
26
|
+
await mkdir(path.dirname(lockDir), { recursive: true })
|
|
27
|
+
const deadline = Date.now() + CACHE_LOCK_WAIT_MS
|
|
28
|
+
let delayMs = 8
|
|
29
|
+
|
|
30
|
+
while (true) {
|
|
31
|
+
try {
|
|
32
|
+
await mkdir(lockDir)
|
|
33
|
+
break
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error?.code !== "EEXIST") throw error
|
|
36
|
+
|
|
37
|
+
const info = await stat(lockDir).catch(() => null)
|
|
38
|
+
if (info && Date.now() - info.mtimeMs > CACHE_LOCK_STALE_MS) {
|
|
39
|
+
const confirmed = await stat(lockDir).catch(() => null)
|
|
40
|
+
if (
|
|
41
|
+
confirmed &&
|
|
42
|
+
confirmed.ino === info.ino &&
|
|
43
|
+
confirmed.mtimeMs === info.mtimeMs
|
|
44
|
+
) {
|
|
45
|
+
await rm(lockDir, { recursive: true, force: true }).catch(() => {})
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (Date.now() >= deadline) {
|
|
50
|
+
throw new Error("Timed out waiting for verification broker cache lock")
|
|
51
|
+
}
|
|
52
|
+
await sleep(delayMs)
|
|
53
|
+
delayMs = Math.min(80, delayMs * 2)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
return await fn()
|
|
59
|
+
} finally {
|
|
60
|
+
await rm(lockDir, { recursive: true, force: true }).catch(() => {})
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function withCacheWriteLock(root, fn) {
|
|
65
|
+
const key = path.resolve(root)
|
|
66
|
+
const previous = CACHE_WRITE_TAILS.get(key) || Promise.resolve()
|
|
67
|
+
let release
|
|
68
|
+
const barrier = new Promise((resolve) => { release = resolve })
|
|
69
|
+
const tail = previous.catch(() => {}).then(() => barrier)
|
|
70
|
+
CACHE_WRITE_TAILS.set(key, tail)
|
|
71
|
+
|
|
72
|
+
await previous.catch(() => {})
|
|
73
|
+
try {
|
|
74
|
+
return await withCrossProcessCacheLock(root, fn)
|
|
75
|
+
} finally {
|
|
76
|
+
release()
|
|
77
|
+
if (CACHE_WRITE_TAILS.get(key) === tail) CACHE_WRITE_TAILS.delete(key)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function cachePath(root) {
|
|
82
|
+
return path.join(path.resolve(root), ".ues-cache", CACHE_FILE)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function keyFor(command, args = []) {
|
|
86
|
+
return createHash("sha256")
|
|
87
|
+
.update(JSON.stringify([String(command || ""), (args || []).map(String)]))
|
|
88
|
+
.digest("hex")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function receiptReusableAtFingerprint(receipt, fingerprint) {
|
|
92
|
+
if (!validateVerificationReceipt(receipt).valid) return false
|
|
93
|
+
if (receipt.exitCode !== 0 || receipt.passed !== true) return false
|
|
94
|
+
if (!Number.isFinite(Date.parse(receipt.startedAt || ""))) return false
|
|
95
|
+
if (!Number.isFinite(Date.parse(receipt.finishedAt || ""))) return false
|
|
96
|
+
return Boolean(
|
|
97
|
+
receipt.workspaceBefore &&
|
|
98
|
+
receipt.workspaceAfter &&
|
|
99
|
+
receipt.workspaceBefore === receipt.workspaceAfter &&
|
|
100
|
+
receipt.workspaceAfter === fingerprint
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function entryMatchesKey(key, entry) {
|
|
105
|
+
const receipt = entry?.receipt
|
|
106
|
+
if (!receipt || !String(receipt.command || "").trim()) return false
|
|
107
|
+
if (!Array.isArray(receipt.args)) return false
|
|
108
|
+
return keyFor(receipt.command, receipt.args) === key
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function finishedAtMs(entry) {
|
|
112
|
+
const value = Date.parse(entry?.finishedAt || entry?.receipt?.finishedAt || "")
|
|
113
|
+
return Number.isFinite(value) ? value : null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function freshEnough(entry, maxAgeMs, now = Date.now()) {
|
|
117
|
+
const finished = finishedAtMs(entry)
|
|
118
|
+
if (finished == null) return false
|
|
119
|
+
if (finished > now + 60_000) return false
|
|
120
|
+
if (!maxAgeMs) return true
|
|
121
|
+
return now - finished <= maxAgeMs
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function readCache(root) {
|
|
125
|
+
try {
|
|
126
|
+
const parsed = JSON.parse(await readFile(cachePath(root), "utf8"))
|
|
127
|
+
if (parsed?.schemaVersion !== CACHE_VERSION || typeof parsed.entries !== "object") return { schemaVersion: CACHE_VERSION, entries: {} }
|
|
128
|
+
return parsed
|
|
129
|
+
} catch {
|
|
130
|
+
return { schemaVersion: CACHE_VERSION, entries: {} }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function writeCache(root, value) {
|
|
135
|
+
const file = cachePath(root)
|
|
136
|
+
await mkdir(path.dirname(file), { recursive: true })
|
|
137
|
+
const temp = file + "." + process.pid + "." + Date.now() + "." + randomUUID() + ".tmp"
|
|
138
|
+
await writeFile(temp, JSON.stringify(value, null, 2) + "\n", "utf8")
|
|
139
|
+
try {
|
|
140
|
+
await rename(temp, file)
|
|
141
|
+
} catch (error) {
|
|
142
|
+
await rm(temp, { force: true }).catch(() => {})
|
|
143
|
+
throw error
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function findReusableVerification(root, command, args = [], options = {}) {
|
|
148
|
+
root = path.resolve(root)
|
|
149
|
+
const maxAgeMs = Math.max(0, Number(options.maxAgeMs ?? 20 * 60_000))
|
|
150
|
+
const cache = await readCache(root)
|
|
151
|
+
const key = keyFor(command, args)
|
|
152
|
+
const entry = cache.entries[key]
|
|
153
|
+
if (!entry || !entryMatchesKey(key, entry)) return null
|
|
154
|
+
if (!freshEnough(entry, maxAgeMs)) return null
|
|
155
|
+
|
|
156
|
+
const currentFingerprint = String(options.workspaceFingerprint || runtimeWorkspaceFingerprint(root))
|
|
157
|
+
if (!receiptReusableAtFingerprint(entry.receipt, currentFingerprint)) return null
|
|
158
|
+
|
|
159
|
+
const stdout = entry.stdoutRef
|
|
160
|
+
? await getEvidence(root, entry.stdoutRef, { maxBytes: options.maxBytes || 16_000 }).catch(() => null)
|
|
161
|
+
: null
|
|
162
|
+
const stderr = entry.stderrRef
|
|
163
|
+
? await getEvidence(root, entry.stderrRef, { maxBytes: options.maxBytes || 8_000 }).catch(() => null)
|
|
164
|
+
: null
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
schemaVersion: 1,
|
|
168
|
+
reused: true,
|
|
169
|
+
key,
|
|
170
|
+
receipt: entry.receipt,
|
|
171
|
+
stdoutRef: entry.stdoutRef || null,
|
|
172
|
+
stderrRef: entry.stderrRef || null,
|
|
173
|
+
stdout: stdout?.content || "",
|
|
174
|
+
stderr: stderr?.content || "",
|
|
175
|
+
ageMs: Math.max(0, Date.now() - finishedAtMs(entry)),
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function recordVerification(root, input = {}) {
|
|
180
|
+
root = path.resolve(root)
|
|
181
|
+
const stdout = String(input.stdout || "")
|
|
182
|
+
const stderr = String(input.stderr || "")
|
|
183
|
+
const stdoutEvidence = await putEvidence(root, stdout, {
|
|
184
|
+
kind: "verification-stdout",
|
|
185
|
+
source: input.command || "verification-broker",
|
|
186
|
+
summary: "Captured stdout preserved for reusable verification receipt",
|
|
187
|
+
})
|
|
188
|
+
const stderrEvidence = await putEvidence(root, stderr, {
|
|
189
|
+
kind: "verification-stderr",
|
|
190
|
+
source: input.command || "verification-broker",
|
|
191
|
+
summary: "Captured stderr preserved for reusable verification receipt",
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
const receipt = createVerificationReceipt({
|
|
195
|
+
task: input.task || null,
|
|
196
|
+
runId: input.runId || null,
|
|
197
|
+
command: input.command,
|
|
198
|
+
args: input.args || [],
|
|
199
|
+
cwd: root,
|
|
200
|
+
exitCode: input.exitCode,
|
|
201
|
+
startedAt: input.startedAt,
|
|
202
|
+
finishedAt: input.finishedAt,
|
|
203
|
+
durationMs: input.durationMs,
|
|
204
|
+
stdout,
|
|
205
|
+
stderr,
|
|
206
|
+
workspaceBefore: input.workspaceBefore || null,
|
|
207
|
+
workspaceAfter: input.workspaceAfter || runtimeWorkspaceFingerprint(root),
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
const key = keyFor(input.command, input.args || [])
|
|
211
|
+
await withCacheWriteLock(root, async () => {
|
|
212
|
+
const cache = await readCache(root)
|
|
213
|
+
cache.entries[key] = {
|
|
214
|
+
receipt,
|
|
215
|
+
stdoutRef: stdoutEvidence.ref,
|
|
216
|
+
stderrRef: stderrEvidence.ref,
|
|
217
|
+
finishedAt: receipt.finishedAt,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Keep cache small and deterministic.
|
|
221
|
+
const rows = Object.entries(cache.entries)
|
|
222
|
+
.sort((a, b) => (finishedAtMs(b[1]) || 0) - (finishedAtMs(a[1]) || 0))
|
|
223
|
+
.slice(0, 200)
|
|
224
|
+
cache.entries = Object.fromEntries(rows)
|
|
225
|
+
await writeCache(root, cache)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
schemaVersion: 1,
|
|
230
|
+
reused: false,
|
|
231
|
+
key,
|
|
232
|
+
receipt,
|
|
233
|
+
stdoutRef: stdoutEvidence.ref,
|
|
234
|
+
stderrRef: stderrEvidence.ref,
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function listReusableVerification(root, options = {}) {
|
|
239
|
+
root = path.resolve(root)
|
|
240
|
+
const currentFingerprint = String(options.workspaceFingerprint || runtimeWorkspaceFingerprint(root))
|
|
241
|
+
const maxAgeMs = Math.max(0, Number(options.maxAgeMs ?? 30 * 60_000))
|
|
242
|
+
const limit = Math.max(1, Math.min(50, Number(options.limit || 12)))
|
|
243
|
+
const now = Date.now()
|
|
244
|
+
const cache = await readCache(root)
|
|
245
|
+
const rows = []
|
|
246
|
+
|
|
247
|
+
for (const [key, entry] of Object.entries(cache.entries || {})) {
|
|
248
|
+
const receipt = entry?.receipt
|
|
249
|
+
if (!entryMatchesKey(key, entry)) continue
|
|
250
|
+
if (!receiptReusableAtFingerprint(receipt, currentFingerprint)) continue
|
|
251
|
+
const finished = finishedAtMs(entry)
|
|
252
|
+
if (!freshEnough(entry, maxAgeMs, now)) continue
|
|
253
|
+
rows.push({
|
|
254
|
+
key,
|
|
255
|
+
receipt,
|
|
256
|
+
stdoutRef: entry.stdoutRef || null,
|
|
257
|
+
stderrRef: entry.stderrRef || null,
|
|
258
|
+
finishedAt: entry.finishedAt || receipt.finishedAt || null,
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
rows.sort((a, b) => (Date.parse(b.finishedAt || "") || 0) - (Date.parse(a.finishedAt || "") || 0))
|
|
263
|
+
const selected = rows.slice(0, limit)
|
|
264
|
+
const output = []
|
|
265
|
+
for (const row of selected) {
|
|
266
|
+
const stdout = row.stdoutRef
|
|
267
|
+
? await getEvidence(root, row.stdoutRef, { maxBytes: options.previewBytes || 2400 }).catch(() => null)
|
|
268
|
+
: null
|
|
269
|
+
const stderr = row.stderrRef
|
|
270
|
+
? await getEvidence(root, row.stderrRef, { maxBytes: options.previewBytes || 1200 }).catch(() => null)
|
|
271
|
+
: null
|
|
272
|
+
output.push({
|
|
273
|
+
...row,
|
|
274
|
+
stdoutPreview: stdout?.content || "",
|
|
275
|
+
stderrPreview: stderr?.content || "",
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
schemaVersion: 1,
|
|
280
|
+
workspaceFingerprint: currentFingerprint,
|
|
281
|
+
count: output.length,
|
|
282
|
+
results: output,
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export const VERIFICATION_COMMAND_RE =
|
|
2
|
+
/(?:^|\s|&&|;|\|)(?:pnpm|npm|yarn|bun|npx|node|python|pytest|go|cargo|dotnet|mvn|gradle|\.\/gradlew|gradlew\.bat)[^\n]*(?:test|jest|vitest|pytest|typecheck|tsc|lint|eslint|ruff|mypy|check|build|compile)/i
|
|
3
|
+
|
|
4
|
+
export function looksLikeVerificationCommand(command = "") {
|
|
5
|
+
return VERIFICATION_COMMAND_RE.test(String(command || ""))
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function hasMaskedShellExitRisk(command = "") {
|
|
9
|
+
const value = String(command || "")
|
|
10
|
+
// Receipt reuse is an optimization, so ambiguity fails closed. Quoted shell
|
|
11
|
+
// metacharacters may cause a false negative here, which only means rerunning
|
|
12
|
+
// the check; it can never create a false PASS receipt.
|
|
13
|
+
if (value.includes("||") || value.includes(";") || value.includes("|") || /\r|\n/.test(value)) {
|
|
14
|
+
return true
|
|
15
|
+
}
|
|
16
|
+
// Reject a single/background '&' while allowing '&&'.
|
|
17
|
+
const withoutAndAnd = value.replaceAll("&&", "")
|
|
18
|
+
if (withoutAndAnd.includes("&")) return true
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function canRecordReusableVerification(command = "") {
|
|
23
|
+
const value = String(command || "").trim()
|
|
24
|
+
return Boolean(value) &&
|
|
25
|
+
looksLikeVerificationCommand(value) &&
|
|
26
|
+
!hasMaskedShellExitRisk(value)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function tokenizeSimpleShell(command) {
|
|
30
|
+
const value = String(command || "").trim()
|
|
31
|
+
if (!value || hasMaskedShellExitRisk(value)) return null
|
|
32
|
+
// Exact executable+args reuse is stricter than generic receipt eligibility:
|
|
33
|
+
// a compound chain has one aggregate exit status but no single argv identity.
|
|
34
|
+
if (/&&|\|\||;|\||\r|\n/.test(value)) return null
|
|
35
|
+
// Fail closed on expansion, redirection, globs and backslash-sensitive
|
|
36
|
+
// shell syntax. Receipt reuse is only an optimization, so false negatives
|
|
37
|
+
// here cost latency rather than correctness.
|
|
38
|
+
if (/[<>\\]/.test(value) || value.includes("\u0060") || /\$\(|\$\{|\$[A-Za-z_]|%[A-Za-z_][A-Za-z0-9_]*%|\*|\?|\[|\]/.test(value)) return null
|
|
39
|
+
|
|
40
|
+
const tokens = []
|
|
41
|
+
let current = ""
|
|
42
|
+
let quote = null
|
|
43
|
+
let escaped = false
|
|
44
|
+
|
|
45
|
+
const push = () => {
|
|
46
|
+
if (!current) return
|
|
47
|
+
tokens.push(current)
|
|
48
|
+
current = ""
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
52
|
+
const char = value[index]
|
|
53
|
+
if (escaped) {
|
|
54
|
+
current += char
|
|
55
|
+
escaped = false
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
if (char === "\\" && quote !== "'") {
|
|
59
|
+
escaped = true
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
if (quote) {
|
|
63
|
+
if (char === quote) quote = null
|
|
64
|
+
else current += char
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
if (char === "'" || char === '"') {
|
|
68
|
+
quote = char
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
if (/\s/.test(char)) {
|
|
72
|
+
push()
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
current += char
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (escaped || quote) return null
|
|
79
|
+
push()
|
|
80
|
+
return tokens.length ? tokens : null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function canonicalVerificationCommand(command = "") {
|
|
84
|
+
const value = String(command || "").trim()
|
|
85
|
+
if (!canRecordReusableVerification(value)) return null
|
|
86
|
+
|
|
87
|
+
const tokens = tokenizeSimpleShell(value)
|
|
88
|
+
if (!tokens?.length) return null
|
|
89
|
+
|
|
90
|
+
const firstRaw = String(tokens[0] || "")
|
|
91
|
+
const first = firstRaw.toLowerCase()
|
|
92
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(firstRaw) || firstRaw.startsWith("~")) {
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
if ((first === "cmd" || first === "cmd.exe") && /^\/c$/i.test(tokens[1] || "")) {
|
|
96
|
+
if (tokens.length < 3 || /\s/.test(tokens[2])) return null
|
|
97
|
+
return { command: tokens[2], args: tokens.slice(3), raw: value }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// PowerShell -Command is intentionally not canonicalized: quoting and command
|
|
101
|
+
// parsing semantics are too rich for a safe exact-reuse key.
|
|
102
|
+
if (first === "powershell" || first === "powershell.exe" || first === "pwsh" || first === "pwsh.exe") {
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
command: tokens[0],
|
|
108
|
+
args: tokens.slice(1),
|
|
109
|
+
raw: value,
|
|
110
|
+
}
|
|
111
|
+
}
|
package/lib/windows-shim.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from "node:fs"
|
|
2
2
|
import path from "node:path"
|
|
3
|
+
import os from "node:os"
|
|
3
4
|
import { spawnSync } from "node:child_process"
|
|
4
5
|
|
|
5
6
|
function within(base, candidate) {
|
|
@@ -219,6 +220,40 @@ export function resolveWindowsCommand(name) {
|
|
|
219
220
|
return resolveWindowsCommandCandidates(candidates)
|
|
220
221
|
}
|
|
221
222
|
|
|
223
|
+
export function resolveManagedPiCommand(homeDir = os.homedir()) {
|
|
224
|
+
const releasesDir = path.join(homeDir, ".pi", "agent", "install", "releases")
|
|
225
|
+
let releases = []
|
|
226
|
+
try {
|
|
227
|
+
releases = readdirSync(releasesDir, { withFileTypes: true })
|
|
228
|
+
.filter((entry) => entry.isDirectory())
|
|
229
|
+
.map((entry) => entry.name)
|
|
230
|
+
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: "base" }))
|
|
231
|
+
} catch {
|
|
232
|
+
return null
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
for (const release of releases) {
|
|
236
|
+
const packageDir = path.join(
|
|
237
|
+
releasesDir,
|
|
238
|
+
release,
|
|
239
|
+
"node_modules",
|
|
240
|
+
"@earendil-works",
|
|
241
|
+
"pi-coding-agent",
|
|
242
|
+
)
|
|
243
|
+
const target = packageBinTarget(packageDir, "pi")
|
|
244
|
+
if (!target) continue
|
|
245
|
+
const resolved = executionForTarget(target)
|
|
246
|
+
if (resolved) {
|
|
247
|
+
return {
|
|
248
|
+
...resolved,
|
|
249
|
+
source: target,
|
|
250
|
+
managedRelease: release,
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return null
|
|
255
|
+
}
|
|
256
|
+
|
|
222
257
|
function spawnWhere(name) {
|
|
223
258
|
// Lazy import avoidance is unnecessary here; child_process is builtin and this
|
|
224
259
|
// module is Node-only. Kept as one helper so tests can cover candidate selection
|