opencode-agent-skill 14.2.0-beta.1 → 14.4.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 +23 -8
- package/docs/PI-COMPAT.md +16 -1
- package/lib/capability-fabric.mjs +23 -0
- package/lib/code-intelligence/edit-anchor.mjs +102 -0
- package/lib/code-intelligence/index.mjs +95 -0
- package/lib/code-intelligence/lsp-provider.mjs +189 -0
- package/lib/completion-auditor.mjs +82 -0
- package/lib/document-ingestion.mjs +60 -0
- package/lib/fast-verification-gate.mjs +69 -0
- package/lib/mcp-health.mjs +144 -0
- package/lib/mcp-tool-policy.mjs +19 -0
- package/lib/memory-engine.mjs +28 -0
- package/lib/prompt-cache.mjs +26 -12
- package/lib/reversible-context.mjs +49 -0
- package/lib/task-engine.mjs +6 -10
- package/lib/task-policy.mjs +61 -5
- package/package.json +3 -3
- package/pi/extensions/ues-child-runtime.ts +155 -0
- package/pi/extensions/ues.ts +145 -18
- package/scripts/check-source-integrity.mjs +62 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const BEHAVIORAL_TEST_COMMAND = [
|
|
2
|
+
/(?:^|\s)(?:node|bun)\s+--test(?:\s|$)/i,
|
|
3
|
+
/(?:^|\s)(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?test(?:\s|$|:)/i,
|
|
4
|
+
/(?:^|\s)(?:npx\s+)?(?:jest|vitest|pytest)(?:\s|$)/i,
|
|
5
|
+
/(?:^|\s)go\s+test(?:\s|$)/i,
|
|
6
|
+
/(?:^|\s)cargo\s+test(?:\s|$)/i,
|
|
7
|
+
/(?:^|\s)dotnet\s+test(?:\s|$)/i,
|
|
8
|
+
/(?:^|\s)(?:mvn|mvnw|gradle|gradlew|\.\/gradlew|gradlew\.bat)\b[^\n]*\btest\b/i,
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
function commandText(row = {}) {
|
|
12
|
+
const receipt = row?.receipt || {}
|
|
13
|
+
return [receipt.command, ...(Array.isArray(receipt.args) ? receipt.args : [])].filter(Boolean).join(" ").trim()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function finishedAtMs(row = {}) {
|
|
17
|
+
const value = Date.parse(row?.finishedAt || row?.receipt?.finishedAt || "")
|
|
18
|
+
return Number.isFinite(value) ? value : null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isBehavioralVerificationReceipt(row = {}) {
|
|
22
|
+
const receipt = row?.receipt || {}
|
|
23
|
+
if (receipt.passed !== true || Number(receipt.exitCode) !== 0) return false
|
|
24
|
+
const command = commandText(row)
|
|
25
|
+
return Boolean(command) && BEHAVIORAL_TEST_COMMAND.some((pattern) => pattern.test(command))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function evaluateFastVerificationGate(input = {}) {
|
|
29
|
+
const policy = input.policy || {}
|
|
30
|
+
const implementation = input.implementation || {}
|
|
31
|
+
const receipts = Array.isArray(input.receipts) ? input.receipts : []
|
|
32
|
+
const attemptStartedAtMs = Number(input.attemptStartedAtMs || 0)
|
|
33
|
+
const eligible =
|
|
34
|
+
policy.executionProfile === "fast" &&
|
|
35
|
+
policy.singleFileBounded === true &&
|
|
36
|
+
policy.risk === "low" &&
|
|
37
|
+
input.visualRequired !== true
|
|
38
|
+
const implementationOk =
|
|
39
|
+
Number(implementation.exitCode) === 0 &&
|
|
40
|
+
String(implementation.stopReason || "").toLowerCase() !== "error"
|
|
41
|
+
const verificationText = String(implementation?.report?.sections?.verification || "").trim()
|
|
42
|
+
const implementationReportedVerification =
|
|
43
|
+
Boolean(verificationText) &&
|
|
44
|
+
!/(?:not\s+run|not\s+available|skipped|unverified|no\s+(?:tests?|checks?)\s+(?:run|available))/i.test(verificationText)
|
|
45
|
+
const behavioralReceipts = receipts
|
|
46
|
+
.filter((row) => {
|
|
47
|
+
const finished = finishedAtMs(row)
|
|
48
|
+
return isBehavioralVerificationReceipt(row) &&
|
|
49
|
+
(attemptStartedAtMs <= 0 || (finished != null && finished >= attemptStartedAtMs))
|
|
50
|
+
})
|
|
51
|
+
.map((row) => ({
|
|
52
|
+
command: commandText(row),
|
|
53
|
+
finishedAt: row?.finishedAt || row?.receipt?.finishedAt || null,
|
|
54
|
+
receiptId: row?.receipt?.id || null,
|
|
55
|
+
}))
|
|
56
|
+
let reason = "fresh-behavioral-evidence"
|
|
57
|
+
if (!eligible) reason = "not-fast-bounded"
|
|
58
|
+
else if (!implementationOk) reason = "implementation-not-clean"
|
|
59
|
+
else if (!implementationReportedVerification) reason = "implementation-did-not-report-verification"
|
|
60
|
+
else if (!behavioralReceipts.length) reason = "no-fresh-behavioral-receipt"
|
|
61
|
+
return {
|
|
62
|
+
schemaVersion: 1,
|
|
63
|
+
eligible,
|
|
64
|
+
passed: eligible && implementationOk && implementationReportedVerification && behavioralReceipts.length > 0,
|
|
65
|
+
reason,
|
|
66
|
+
behavioralReceiptCount: behavioralReceipts.length,
|
|
67
|
+
behavioralReceipts,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const TRANSIENT_ERROR =
|
|
2
|
+
/(timeout|timed out|econnreset|econnrefused|socket|connection (?:closed|reset|lost)|transport|network|temporar(?:y|ily)|service unavailable|bad gateway|gateway timeout|\b502\b|\b503\b|\b504\b|rate limit|\b429\b)/i
|
|
3
|
+
|
|
4
|
+
function nowMs(value) {
|
|
5
|
+
const number = Number(value)
|
|
6
|
+
return Number.isFinite(number) ? number : Date.now()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isTransientMcpFailure(value = "") {
|
|
10
|
+
return TRANSIENT_ERROR.test(String(value || ""))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function mcpReconnectAdvice(input = {}) {
|
|
14
|
+
const policy = input.policy || {}
|
|
15
|
+
const attempt = Math.max(1, Math.trunc(Number(input.attempt || 1)))
|
|
16
|
+
const transient = isTransientMcpFailure(input.error || input.message || "")
|
|
17
|
+
const destructive = policy.confirmationRequired === true || policy.annotations?.destructiveHint === true
|
|
18
|
+
const retryable = policy.retryAllowed === true && transient && !destructive && attempt < Math.max(1, Number(input.maxAttempts || 3))
|
|
19
|
+
return {
|
|
20
|
+
schemaVersion: 1,
|
|
21
|
+
retryable,
|
|
22
|
+
reconnectRecommended: retryable,
|
|
23
|
+
reason: destructive
|
|
24
|
+
? "destructive-tool-never-auto-reconnect"
|
|
25
|
+
: !transient
|
|
26
|
+
? "non-transient-failure"
|
|
27
|
+
: policy.retryAllowed !== true
|
|
28
|
+
? "tool-not-explicitly-idempotent"
|
|
29
|
+
: retryable
|
|
30
|
+
? "transient-idempotent-failure"
|
|
31
|
+
: "retry-budget-exhausted",
|
|
32
|
+
backoffMs: retryable ? Math.min(5_000, 250 * (2 ** Math.max(0, attempt - 1))) : 0,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class McpHealthTracker {
|
|
37
|
+
constructor(options = {}) {
|
|
38
|
+
this.failureThreshold = Math.max(1, Math.min(10, Number(options.failureThreshold || 2)))
|
|
39
|
+
this.cooldownMs = Math.max(1_000, Math.min(5 * 60_000, Number(options.cooldownMs || 15_000)))
|
|
40
|
+
this.maxEntries = Math.max(16, Math.min(2_000, Number(options.maxEntries || 256)))
|
|
41
|
+
this.state = new Map()
|
|
42
|
+
this.active = new Map()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
clear() {
|
|
46
|
+
this.state.clear()
|
|
47
|
+
this.active.clear()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
begin(callId, toolName, policy = {}, at = Date.now()) {
|
|
51
|
+
const id = String(callId || "")
|
|
52
|
+
const name = String(toolName || "")
|
|
53
|
+
if (id && name) this.active.set(id, { name, policy, startedAt: nowMs(at) })
|
|
54
|
+
this.#trim()
|
|
55
|
+
return this.status(name, at)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
finish(callId, result = {}, at = Date.now()) {
|
|
59
|
+
const id = String(callId || "")
|
|
60
|
+
const active = this.active.get(id)
|
|
61
|
+
if (!active) return null
|
|
62
|
+
this.active.delete(id)
|
|
63
|
+
const timestamp = nowMs(at)
|
|
64
|
+
const previous = this.state.get(active.name) || {
|
|
65
|
+
successes: 0,
|
|
66
|
+
failures: 0,
|
|
67
|
+
consecutiveFailures: 0,
|
|
68
|
+
cooldownUntil: 0,
|
|
69
|
+
lastError: null,
|
|
70
|
+
lastFinishedAt: 0,
|
|
71
|
+
}
|
|
72
|
+
const errorText = [
|
|
73
|
+
result.error,
|
|
74
|
+
result.message,
|
|
75
|
+
result.stderr,
|
|
76
|
+
result.text,
|
|
77
|
+
].filter(Boolean).join("\n").slice(0, 4000)
|
|
78
|
+
const failed = result.isError === true || Number(result.exitCode) > 0
|
|
79
|
+
const transient = failed && isTransientMcpFailure(errorText)
|
|
80
|
+
const consecutiveFailures = failed ? Number(previous.consecutiveFailures || 0) + 1 : 0
|
|
81
|
+
const cooldownUntil = transient && consecutiveFailures >= this.failureThreshold
|
|
82
|
+
? timestamp + this.cooldownMs
|
|
83
|
+
: failed
|
|
84
|
+
? Number(previous.cooldownUntil || 0)
|
|
85
|
+
: 0
|
|
86
|
+
const reconnectAdvice = failed
|
|
87
|
+
? mcpReconnectAdvice({ policy: active.policy, error: errorText, attempt: consecutiveFailures })
|
|
88
|
+
: null
|
|
89
|
+
const next = {
|
|
90
|
+
successes: Number(previous.successes || 0) + (failed ? 0 : 1),
|
|
91
|
+
failures: Number(previous.failures || 0) + (failed ? 1 : 0),
|
|
92
|
+
consecutiveFailures,
|
|
93
|
+
cooldownUntil,
|
|
94
|
+
lastError: failed ? errorText || "tool-result-error" : null,
|
|
95
|
+
lastTransientFailure: transient,
|
|
96
|
+
reconnectAdvice,
|
|
97
|
+
lastFinishedAt: timestamp,
|
|
98
|
+
policy: active.policy,
|
|
99
|
+
}
|
|
100
|
+
this.state.set(active.name, next)
|
|
101
|
+
this.#trim()
|
|
102
|
+
return this.status(active.name, timestamp)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
status(toolName, at = Date.now()) {
|
|
106
|
+
const name = String(toolName || "")
|
|
107
|
+
const row = this.state.get(name)
|
|
108
|
+
const timestamp = nowMs(at)
|
|
109
|
+
if (!row) {
|
|
110
|
+
return { schemaVersion: 1, tool: name, status: "unknown", available: true, consecutiveFailures: 0, cooldownUntil: null }
|
|
111
|
+
}
|
|
112
|
+
const coolingDown = Number(row.cooldownUntil || 0) > timestamp
|
|
113
|
+
return {
|
|
114
|
+
schemaVersion: 1,
|
|
115
|
+
tool: name,
|
|
116
|
+
status: coolingDown ? "degraded" : row.consecutiveFailures > 0 ? "recovering" : "healthy",
|
|
117
|
+
available: !coolingDown,
|
|
118
|
+
successes: row.successes,
|
|
119
|
+
failures: row.failures,
|
|
120
|
+
consecutiveFailures: row.consecutiveFailures,
|
|
121
|
+
cooldownUntil: coolingDown ? new Date(row.cooldownUntil).toISOString() : null,
|
|
122
|
+
lastError: row.lastError,
|
|
123
|
+
lastTransientFailure: row.lastTransientFailure === true,
|
|
124
|
+
reconnectAdvice: row.reconnectAdvice || null,
|
|
125
|
+
lastFinishedAt: row.lastFinishedAt ? new Date(row.lastFinishedAt).toISOString() : null,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
available(toolName, at = Date.now()) {
|
|
130
|
+
return this.status(toolName, at).available !== false
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
snapshot(at = Date.now()) {
|
|
134
|
+
return [...this.state.keys()]
|
|
135
|
+
.sort()
|
|
136
|
+
.map((name) => this.status(name, at))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#trim() {
|
|
140
|
+
if (this.state.size <= this.maxEntries) return
|
|
141
|
+
const ordered = [...this.state.entries()].sort((a, b) => Number(a[1]?.lastFinishedAt || 0) - Number(b[1]?.lastFinishedAt || 0))
|
|
142
|
+
for (const [name] of ordered.slice(0, Math.max(0, this.state.size - this.maxEntries))) this.state.delete(name)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function firstObject(...values) { return values.find((value) => value && typeof value === "object" && !Array.isArray(value)) || {} }
|
|
2
|
+
function explicitBoolean(value) { return typeof value === "boolean" ? value : null }
|
|
3
|
+
export function normalizeMcpAnnotations(tool = {}) {
|
|
4
|
+
const raw = firstObject(tool.annotations, tool.metadata?.annotations, tool._meta?.annotations, tool._meta?.mcp?.annotations)
|
|
5
|
+
const annotations = { readOnlyHint: explicitBoolean(raw.readOnlyHint), destructiveHint: explicitBoolean(raw.destructiveHint), idempotentHint: explicitBoolean(raw.idempotentHint), openWorldHint: explicitBoolean(raw.openWorldHint) }
|
|
6
|
+
return { schemaVersion: 1, explicit: Object.values(annotations).some((value) => value !== null), ...annotations }
|
|
7
|
+
}
|
|
8
|
+
export function mcpExecutionPolicy(tool = {}) {
|
|
9
|
+
const annotations = normalizeMcpAnnotations(tool)
|
|
10
|
+
const destructive = annotations.destructiveHint === true
|
|
11
|
+
const readOnly = annotations.readOnlyHint === true && !destructive
|
|
12
|
+
const idempotent = annotations.idempotentHint === true && !destructive
|
|
13
|
+
return { schemaVersion: 1, tool: String(tool?.name || ""), annotations, fastAllowed: readOnly, confirmationRequired: destructive, retryAllowed: idempotent, externalEvidenceBoundary: annotations.openWorldHint === true, trust: "hint-only" }
|
|
14
|
+
}
|
|
15
|
+
export function mcpPolicyMap(tools = []) {
|
|
16
|
+
const map = new Map()
|
|
17
|
+
for (const tool of Array.isArray(tools) ? tools : []) { const name = String(tool?.name || "").trim(); if (name) map.set(name, mcpExecutionPolicy(tool)) }
|
|
18
|
+
return map
|
|
19
|
+
}
|
package/lib/memory-engine.mjs
CHANGED
|
@@ -303,6 +303,33 @@ function rankMap(rows, key) {
|
|
|
303
303
|
)
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
export function buildMemorySnapshot(memories = []) {
|
|
307
|
+
const entries = (Array.isArray(memories) ? memories : [])
|
|
308
|
+
.map((item) => ({
|
|
309
|
+
id: item.id || null,
|
|
310
|
+
type: item.type || null,
|
|
311
|
+
scope: item.scope || null,
|
|
312
|
+
content: normalizedContent(item.content),
|
|
313
|
+
confidence: clamp(item.confidence),
|
|
314
|
+
files: uniq((item.files || []).map(normalizeFile)).sort(),
|
|
315
|
+
taskClass: item.taskClass || null,
|
|
316
|
+
verifiedAt: item.verifiedAt || null,
|
|
317
|
+
evidenceRefs: uniq(item.evidenceRefs || []).sort(),
|
|
318
|
+
}))
|
|
319
|
+
.filter((item) => item.id && item.content)
|
|
320
|
+
.sort((a, b) => String(a.id).localeCompare(String(b.id)))
|
|
321
|
+
const generation = createHash("sha256")
|
|
322
|
+
.update(JSON.stringify(entries))
|
|
323
|
+
.digest("hex")
|
|
324
|
+
.slice(0, 20)
|
|
325
|
+
return {
|
|
326
|
+
schemaVersion: 1,
|
|
327
|
+
generation,
|
|
328
|
+
entryCount: entries.length,
|
|
329
|
+
entries,
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
306
333
|
export async function retrieveMemories(root, query, options = {}) {
|
|
307
334
|
const state = await readMemoryState(root)
|
|
308
335
|
const memories = state.memories.filter((item) => item.status === "verified" && !item.supersededBy && !isExpired(item))
|
|
@@ -365,6 +392,7 @@ export async function retrieveMemories(root, query, options = {}) {
|
|
|
365
392
|
query: String(query || ""),
|
|
366
393
|
eligible: memories.length,
|
|
367
394
|
results,
|
|
395
|
+
snapshot: buildMemorySnapshot(results),
|
|
368
396
|
}
|
|
369
397
|
}
|
|
370
398
|
|
package/lib/prompt-cache.mjs
CHANGED
|
@@ -5,31 +5,35 @@ function canonical(value) {
|
|
|
5
5
|
if (!value || typeof value !== "object") return value
|
|
6
6
|
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]))
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
function stringify(value)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return createHash("sha256").update(stringify(value)).digest("hex")
|
|
8
|
+
function stringify(value) { return typeof value === "string" ? value : JSON.stringify(canonical(value)) }
|
|
9
|
+
function digest(value) { return createHash("sha256").update(stringify(value)).digest("hex") }
|
|
10
|
+
function chars(value) { return stringify(value ?? "").length }
|
|
11
|
+
function finiteTokenCount(value) {
|
|
12
|
+
const number = Number(value)
|
|
13
|
+
return Number.isFinite(number) && number >= 0 ? number : null
|
|
15
14
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
function changedStableKeys(previous, current) {
|
|
16
|
+
const left = previous?.stable || {}
|
|
17
|
+
const right = current?.stable || {}
|
|
18
|
+
return [...new Set([...Object.keys(left), ...Object.keys(right)])]
|
|
19
|
+
.filter((key) => digest(left[key]) !== digest(right[key]))
|
|
20
|
+
.sort()
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export function buildPromptEnvelope(input = {}) {
|
|
24
|
+
const memorySnapshot = input.memorySnapshot || null
|
|
22
25
|
const stable = {
|
|
23
26
|
invariants: input.invariants || "",
|
|
24
27
|
role: input.role || "",
|
|
25
28
|
skills: input.skills || [],
|
|
26
29
|
projectFacts: input.projectFacts || {},
|
|
27
30
|
toolPolicy: input.toolPolicy || {},
|
|
31
|
+
memorySnapshot,
|
|
28
32
|
}
|
|
29
33
|
const dynamic = {
|
|
30
34
|
task: input.task || {},
|
|
31
35
|
evidence: input.evidence || [],
|
|
32
|
-
memories: input.memories || [],
|
|
36
|
+
memories: memorySnapshot ? [] : (input.memories || []),
|
|
33
37
|
contextHints: input.contextHints || {},
|
|
34
38
|
recentFailure: input.recentFailure || null,
|
|
35
39
|
nextAction: input.nextAction || null,
|
|
@@ -45,18 +49,28 @@ export function buildPromptEnvelope(input = {}) {
|
|
|
45
49
|
dynamicHash: digest(dynamic),
|
|
46
50
|
stableChars,
|
|
47
51
|
dynamicChars,
|
|
52
|
+
cacheableChars: stableChars,
|
|
48
53
|
cacheableRatio: stableChars + dynamicChars ? stableChars / (stableChars + dynamicChars) : 0,
|
|
54
|
+
cacheReadTokens: finiteTokenCount(input.cacheReadTokens),
|
|
55
|
+
cacheWriteTokens: finiteTokenCount(input.cacheWriteTokens),
|
|
49
56
|
}
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
export function comparePromptEnvelopes(previous, current) {
|
|
59
|
+
export function comparePromptEnvelopes(previous, current, usage = {}) {
|
|
53
60
|
const stableReused = Boolean(previous?.stablePrefixHash && previous.stablePrefixHash === current?.stablePrefixHash)
|
|
54
61
|
const repeatedStableChars = stableReused ? Number(current?.stableChars || 0) : 0
|
|
62
|
+
const mutated = stableReused ? [] : changedStableKeys(previous, current)
|
|
55
63
|
return {
|
|
56
64
|
schemaVersion: 1,
|
|
57
65
|
stableReused,
|
|
58
66
|
repeatedStableChars,
|
|
59
67
|
avoidableRepeatedChars: stableReused ? 0 : Math.min(Number(previous?.stableChars || 0), Number(current?.stableChars || 0)),
|
|
60
68
|
currentCacheableRatio: Number(current?.cacheableRatio || 0),
|
|
69
|
+
cacheableChars: Number(current?.cacheableChars ?? current?.stableChars ?? 0),
|
|
70
|
+
dynamicChars: Number(current?.dynamicChars || 0),
|
|
71
|
+
cacheReadTokens: finiteTokenCount(usage.cacheReadTokens ?? current?.cacheReadTokens),
|
|
72
|
+
cacheWriteTokens: finiteTokenCount(usage.cacheWriteTokens ?? current?.cacheWriteTokens),
|
|
73
|
+
prefixMutationReason: stableReused ? null : (mutated.length ? mutated.join(",") : "stable-prefix-changed"),
|
|
74
|
+
mutatedStableKeys: mutated,
|
|
61
75
|
}
|
|
62
76
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { putEvidence, getEvidence, getEvidenceSelected } from "./evidence-store.mjs"
|
|
2
|
+
|
|
3
|
+
function normalize(value) { return String(value ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n") }
|
|
4
|
+
function significantLines(text) {
|
|
5
|
+
const lines = normalize(text).split("\n")
|
|
6
|
+
const scored = lines.map((line, index) => {
|
|
7
|
+
const trimmed = line.trim(); let score = 0
|
|
8
|
+
if (/^#{1,6}\s/.test(trimmed)) score += 8
|
|
9
|
+
if (/^(?:[-*+] |\d+[.)]\s)/.test(trimmed)) score += 5
|
|
10
|
+
if (/\b(?:error|fail|warning|todo|fixme|pass|test|result|summary|important|must|required)\b/i.test(trimmed)) score += 4
|
|
11
|
+
if (/^(?:export|import|class|function|def|interface|type|CREATE|ALTER)\b/i.test(trimmed)) score += 3
|
|
12
|
+
if (trimmed.length > 0 && trimmed.length <= 180) score += 1
|
|
13
|
+
return { index, line, score }
|
|
14
|
+
})
|
|
15
|
+
return { lines, scored }
|
|
16
|
+
}
|
|
17
|
+
function summary(text, maxChars) {
|
|
18
|
+
const { lines, scored } = significantLines(text)
|
|
19
|
+
if (normalize(text).length <= maxChars) return normalize(text)
|
|
20
|
+
const wanted = new Set(scored.filter((row) => row.score > 0).sort((a,b) => b.score - a.score || a.index - b.index).slice(0,80).map((row) => row.index))
|
|
21
|
+
for (let i = Math.max(0, lines.length - 8); i < lines.length; i += 1) wanted.add(i)
|
|
22
|
+
let out = ""
|
|
23
|
+
for (const index of [...wanted].sort((a,b) => a-b)) {
|
|
24
|
+
const candidate = (out ? out + "\n" : "") + lines[index]
|
|
25
|
+
if (candidate.length > maxChars) break
|
|
26
|
+
out = candidate
|
|
27
|
+
}
|
|
28
|
+
return out || normalize(text).slice(0, maxChars)
|
|
29
|
+
}
|
|
30
|
+
export async function compactContext(root, content, options = {}) {
|
|
31
|
+
const raw = normalize(content)
|
|
32
|
+
const stored = await putEvidence(root, raw, { kind: options.kind || "context-block", source: options.source || "ues-context", summary: options.summary || "Reversible raw context block" })
|
|
33
|
+
return { schemaVersion: 1, ref: stored.ref, originalChars: raw.length, levels: { T1: summary(raw, Math.max(512, Number(options.t1Chars || 1600))), T2: summary(raw, Math.max(1200, Number(options.t2Chars || 5000))), T3: summary(raw, Math.max(2400, Number(options.t3Chars || 12000))) } }
|
|
34
|
+
}
|
|
35
|
+
export async function expandContext(root, ref, options = {}) {
|
|
36
|
+
return getEvidenceSelected(root, ref, { start: Math.max(0, Number(options.start || 0)), maxBytes: Math.max(1, Math.min(128_000, Number(options.maxBytes || 16_000))) })
|
|
37
|
+
}
|
|
38
|
+
export async function searchContext(root, ref, query, options = {}) {
|
|
39
|
+
const maxBytes = Math.max(16_000, Math.min(4 * 1024 * 1024, Number(options.maxBytes || 512_000)))
|
|
40
|
+
const evidence = await getEvidence(root, ref, { maxBytes })
|
|
41
|
+
const source = normalize(evidence?.content ?? evidence ?? "")
|
|
42
|
+
const terms = [...new Set(String(query || "").toLowerCase().split(/[^\p{L}\p{N}_$.-]+/u).filter((item) => item.length >= 2))].slice(0, 16)
|
|
43
|
+
const lines = source.split("\n"); const radius = Math.max(0, Math.min(12, Number(options.radius || 2))); const maxMatches = Math.max(1, Math.min(50, Number(options.maxMatches || 10))); const hits = []
|
|
44
|
+
for (let index = 0; index < lines.length && hits.length < maxMatches; index += 1) {
|
|
45
|
+
const lower = lines[index].toLowerCase(); const matched = terms.filter((term) => lower.includes(term)); if (!matched.length) continue
|
|
46
|
+
hits.push({ line: index + 1, terms: matched, excerpt: lines.slice(Math.max(0,index-radius), Math.min(lines.length,index+radius+1)).join("\n") })
|
|
47
|
+
}
|
|
48
|
+
return { schemaVersion: 1, ref, query: String(query || ""), terms, hits, truncatedSource: source.length >= maxBytes }
|
|
49
|
+
}
|
package/lib/task-engine.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { putEvidence } from "./evidence-store.mjs"
|
|
|
15
15
|
import { inferTaskCapabilities } from "./capability-registry.mjs"
|
|
16
16
|
import { buildPromptEnvelope } from "./prompt-cache.mjs"
|
|
17
17
|
import { externalizeContextExcerpts } from "./context-engine-v11.mjs"
|
|
18
|
-
import { recordVerifiedTaskMemory, retrieveMemories } from "./memory-engine.mjs"
|
|
18
|
+
import { buildMemorySnapshot, recordVerifiedTaskMemory, retrieveMemories } from "./memory-engine.mjs"
|
|
19
19
|
import { capabilityFabricStatus } from "./capability-fabric.mjs"
|
|
20
20
|
import { assertActivePlanScope, persistPlanScope } from "./work-plan-scope.mjs"
|
|
21
21
|
import { classifyDecisionPolicy } from "./decision-policy.mjs"
|
|
@@ -1183,6 +1183,7 @@ async function buildContextPack(loaded, taskID) {
|
|
|
1183
1183
|
touch: true,
|
|
1184
1184
|
}).catch(() => ({ schemaVersion: 1, query: taskText, eligible: 0, results: [] }))
|
|
1185
1185
|
const memories = memoryRetrieval.results || []
|
|
1186
|
+
const memorySnapshot = memoryRetrieval.snapshot || buildMemorySnapshot(memories)
|
|
1186
1187
|
const fabric = await capabilityFabricStatus(loaded.paths.root).catch(() => null)
|
|
1187
1188
|
const providerHints = (fabric?.rows || [])
|
|
1188
1189
|
.filter((row) => row.selected)
|
|
@@ -1216,15 +1217,7 @@ async function buildContextPack(loaded, taskID) {
|
|
|
1216
1217
|
...(contextManifest?.evidencePointers || []).map((item) => item.ref),
|
|
1217
1218
|
...(contextManifest?.rankedReferences || []).slice(0, 12).map((item) => item.path),
|
|
1218
1219
|
],
|
|
1219
|
-
|
|
1220
|
-
id: item.id,
|
|
1221
|
-
type: item.type,
|
|
1222
|
-
scope: item.scope,
|
|
1223
|
-
content: item.content,
|
|
1224
|
-
confidence: item.confidence,
|
|
1225
|
-
files: item.files,
|
|
1226
|
-
retrieval: item.retrieval,
|
|
1227
|
-
})),
|
|
1220
|
+
memorySnapshot,
|
|
1228
1221
|
contextHints: {
|
|
1229
1222
|
hierarchy: (contextManifest?.hierarchy?.scopes || []).slice(0, 6).map((item) => ({
|
|
1230
1223
|
path: item.path,
|
|
@@ -1248,7 +1241,9 @@ async function buildContextPack(loaded, taskID) {
|
|
|
1248
1241
|
dynamicHash: promptEnvelope.dynamicHash,
|
|
1249
1242
|
stableChars: promptEnvelope.stableChars,
|
|
1250
1243
|
dynamicChars: promptEnvelope.dynamicChars,
|
|
1244
|
+
cacheableChars: promptEnvelope.cacheableChars,
|
|
1251
1245
|
cacheableRatio: promptEnvelope.cacheableRatio,
|
|
1246
|
+
memoryGeneration: memorySnapshot.generation,
|
|
1252
1247
|
},
|
|
1253
1248
|
attempt,
|
|
1254
1249
|
slug: loaded.state.slug,
|
|
@@ -1275,6 +1270,7 @@ async function buildContextPack(loaded, taskID) {
|
|
|
1275
1270
|
schemaVersion: memoryRetrieval.schemaVersion || 1,
|
|
1276
1271
|
eligible: memoryRetrieval.eligible || 0,
|
|
1277
1272
|
returned: memories.length,
|
|
1273
|
+
generation: memorySnapshot.generation,
|
|
1278
1274
|
},
|
|
1279
1275
|
capabilityFabric: {
|
|
1280
1276
|
healthyCapabilities: fabric?.healthyCapabilities || 0,
|
package/lib/task-policy.mjs
CHANGED
|
@@ -4,6 +4,40 @@ const LONG = /(whole repo|whole repository|whole project|entire repo|entire proj
|
|
|
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
5
|
const CONTRACT = /(public api|api contract|openapi|response schema|request schema|breaking api|hợp đồng api|api công khai)/i
|
|
6
6
|
const DATA = /(database|sql|migration|schema|transaction|index|cơ sở dữ liệu|dữ liệu|migrate)/i
|
|
7
|
+
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
|
+
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
|
|
9
|
+
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
|
+
|
|
11
|
+
function referencedSourceFiles(value) {
|
|
12
|
+
const files = []
|
|
13
|
+
const seen = new Set()
|
|
14
|
+
SOURCE_FILE_RE.lastIndex = 0
|
|
15
|
+
let match
|
|
16
|
+
while ((match = SOURCE_FILE_RE.exec(String(value || "")))) {
|
|
17
|
+
const normalized = String(match[1] || "").replaceAll("\\", "/").toLowerCase()
|
|
18
|
+
if (!normalized || seen.has(normalized)) continue
|
|
19
|
+
seen.add(normalized)
|
|
20
|
+
files.push(normalized)
|
|
21
|
+
}
|
|
22
|
+
return files
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function singleFileBoundedTask(value, facts = {}) {
|
|
26
|
+
const declaredFiles = Array.isArray(facts.declaredFiles) ? facts.declaredFiles.map(String).filter(Boolean) : []
|
|
27
|
+
const changedFileCount = Array.isArray(facts.changedFiles)
|
|
28
|
+
? facts.changedFiles.length
|
|
29
|
+
: Number.isFinite(Number(facts.changedFiles)) ? Number(facts.changedFiles) : 0
|
|
30
|
+
const referencedFiles = referencedSourceFiles(value)
|
|
31
|
+
const counts = [declaredFiles.length, changedFileCount, referencedFiles.length].filter((count) => count > 0)
|
|
32
|
+
if (!counts.length || counts.some((count) => count > 1)) return false
|
|
33
|
+
if (BROAD_FILE_SCOPE.test(String(value || "")) || LONG.test(String(value || ""))) return false
|
|
34
|
+
return counts.some((count) => count === 1) && (
|
|
35
|
+
declaredFiles.length === 1 ||
|
|
36
|
+
changedFileCount === 1 ||
|
|
37
|
+
/\b(?:fix|repair|change|update|modify|rename|implement|correct|sửa|chỉnh sửa|thay đổi|cập nhật)\b/i.test(String(value || ""))
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
7
41
|
|
|
8
42
|
function riskTextFor(value) {
|
|
9
43
|
return String(value || "")
|
|
@@ -158,7 +192,18 @@ export function classifyEngineeringTask(text, facts = {}) {
|
|
|
158
192
|
const value = String(text || "")
|
|
159
193
|
const riskText = riskTextFor(value)
|
|
160
194
|
const sensitiveDomain = SENSITIVE_DOMAIN.test(value)
|
|
161
|
-
const
|
|
195
|
+
const singleFileBounded = singleFileBoundedTask(value, facts)
|
|
196
|
+
const rawSensitiveMutation = HIGH_RISK_MUTATION.test(riskText)
|
|
197
|
+
const sensitiveMutation =
|
|
198
|
+
rawSensitiveMutation &&
|
|
199
|
+
(!singleFileBounded || CRITICAL_LOCAL_MUTATION.test(riskText))
|
|
200
|
+
const dataMigration =
|
|
201
|
+
Boolean(facts.hasMigration) ||
|
|
202
|
+
(
|
|
203
|
+
!singleFileBounded &&
|
|
204
|
+
DATA.test(riskText) &&
|
|
205
|
+
/migration|schema|migrate|di trú|chuyển đổi/i.test(riskText)
|
|
206
|
+
)
|
|
162
207
|
const declaredHighRisk =
|
|
163
208
|
["high", "critical"].includes(String(facts.risk || "").toLowerCase()) ||
|
|
164
209
|
/\b(?:risk|rủi ro)\s*[:=\/-]?\s*(?:high|critical|cao|nghiêm trọng)\b/i.test(value) ||
|
|
@@ -178,18 +223,28 @@ export function classifyEngineeringTask(text, facts = {}) {
|
|
|
178
223
|
signal("declared-high-risk", declaredHighRisk, 2),
|
|
179
224
|
signal("explicit-long-horizon", explicitLongHorizon, 2),
|
|
180
225
|
signal("debugging", DEBUG.test(value), 1),
|
|
226
|
+
signal("single-file-bounded", singleFileBounded, -2),
|
|
181
227
|
signal("public-contract", CONTRACT.test(value) || facts.hasPublicContract, 2),
|
|
182
|
-
signal("data-migration",
|
|
228
|
+
signal("data-migration", dataMigration, 2),
|
|
183
229
|
signal("many-changed-files", Number(facts.changedFiles || 0) > 5, 1),
|
|
184
230
|
signal("very-many-changed-files", Number(facts.changedFiles || 0) > 12, 1),
|
|
185
231
|
signal("large-repository", Number(facts.repoFiles || 0) > 1500, 1),
|
|
186
232
|
signal("monorepo", facts.monorepo === true, 1),
|
|
187
233
|
].filter(Boolean)
|
|
188
234
|
|
|
189
|
-
const score = signals.reduce((sum, item) => sum + item.weight, 0)
|
|
235
|
+
const score = Math.max(0, signals.reduce((sum, item) => sum + item.weight, 0))
|
|
190
236
|
const highRisk = declaredHighRisk || sensitiveMutation || Boolean(facts.hasMigration) || Boolean(facts.hasPublicContract)
|
|
191
|
-
const risk = highRisk ? "high" : score >= 3 ? "medium" : "low"
|
|
192
|
-
const mode =
|
|
237
|
+
const risk = highRisk ? "high" : singleFileBounded ? "low" : score >= 3 ? "medium" : "low"
|
|
238
|
+
const mode =
|
|
239
|
+
explicitLongHorizon
|
|
240
|
+
? "long-horizon"
|
|
241
|
+
: singleFileBounded && !highRisk
|
|
242
|
+
? "inline"
|
|
243
|
+
: score >= 4
|
|
244
|
+
? "long-horizon"
|
|
245
|
+
: score >= 2
|
|
246
|
+
? "standard"
|
|
247
|
+
: "inline"
|
|
193
248
|
const modelTier = risk === "high" || mode === "long-horizon" ? "heavy" : score >= 2 ? "standard" : "light"
|
|
194
249
|
const maxAttempts = risk === "high" ? 2 : 3
|
|
195
250
|
const profile = profileFor(mode, risk)
|
|
@@ -218,6 +273,7 @@ export function classifyEngineeringTask(text, facts = {}) {
|
|
|
218
273
|
requirePlanCheck: profile.durableState || risk === "high",
|
|
219
274
|
requireIntegrationVerification: mode !== "inline" || risk === "high",
|
|
220
275
|
requireFreshEvidence: true,
|
|
276
|
+
singleFileBounded,
|
|
221
277
|
profile,
|
|
222
278
|
domains: [...new Set(domains)],
|
|
223
279
|
recovery: {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-agent-skill",
|
|
3
|
-
"version": "14.
|
|
4
|
-
"description": "Pi Agent engineering runtime with
|
|
3
|
+
"version": "14.4.0",
|
|
4
|
+
"description": "Pi Agent engineering runtime with code intelligence, hash-anchored editing, verified persistent memory, health-aware capability routing, reversible context, and evidence-gated completion",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"ues": "bin/ocskill.mjs",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"ci": "npm run integrity && npm run runtime:exports && npm run syntax && npm test && npm run docs:check && npm pack --dry-run && npm run smoke:pi && npm run smoke:package && npm run smoke:packed",
|
|
39
39
|
"prepublishOnly": "npm run ci",
|
|
40
40
|
"smoke:packed": "node scripts/smoke-packed-install.mjs",
|
|
41
|
-
"eval:v14": "node --test test/capability-fabric-v14.test.mjs test/hierarchical-context-v14.test.mjs test/memory-engine-v14.test.mjs test/context-engine-v14.test.mjs test/task-engine-v14-context.test.mjs test/cli-v14.test.mjs test/v14-contract.test.mjs test/performance-fabric-v14.test.mjs test/browser-mcp-routing-v14.test.mjs test/process-hang-detector-v14.test.mjs test/v14.2-runtime.test.mjs test/benchmark-confidence.test.mjs",
|
|
41
|
+
"eval:v14": "node --test test/capability-fabric-v14.test.mjs test/hierarchical-context-v14.test.mjs test/memory-engine-v14.test.mjs test/context-engine-v14.test.mjs test/task-engine-v14-context.test.mjs test/cli-v14.test.mjs test/v14-contract.test.mjs test/performance-fabric-v14.test.mjs test/browser-mcp-routing-v14.test.mjs test/process-hang-detector-v14.test.mjs test/v14.2-runtime.test.mjs test/benchmark-confidence.test.mjs test/v14.3-intelligence.test.mjs",
|
|
42
42
|
"integrity": "node scripts/check-source-integrity.mjs",
|
|
43
43
|
"runtime:exports": "node scripts/check-runtime-exports.mjs"
|
|
44
44
|
},
|