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.
@@ -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
+ }
@@ -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
 
@@ -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
- return typeof value === "string" ? value : JSON.stringify(canonical(value))
11
- }
12
-
13
- function digest(value) {
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
- function chars(value) {
18
- return stringify(value ?? "").length
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
+ }
@@ -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
- memories: memories.map((item) => ({
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,
@@ -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 sensitiveMutation = HIGH_RISK_MUTATION.test(riskText)
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", (DATA.test(riskText) && /migration|schema|migrate|di trú|chuyển đổi/i.test(riskText)) || facts.hasMigration, 2),
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 = explicitLongHorizon || score >= 4 ? "long-horizon" : score >= 2 ? "standard" : "inline"
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.2.0-beta.1",
4
- "description": "Pi Agent engineering runtime with hierarchical context, verified persistent memory, learned capability failover, durable evidence, and evidence-gated verification",
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
  },