opencode-agent-skill 15.0.0 → 15.1.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.
Files changed (43) hide show
  1. package/README.md +5 -5
  2. package/docs/V15-MANAGED-RUNTIME.md +18 -0
  3. package/global-config/agents/verifier.md +6 -2
  4. package/lib/affected-tests.mjs +58 -8
  5. package/lib/capability-fabric.mjs +107 -48
  6. package/lib/code-intelligence/index.mjs +4 -15
  7. package/lib/code-intelligence/lsp-pool.mjs +694 -0
  8. package/lib/code-intelligence/lsp-provider.mjs +481 -67
  9. package/lib/compaction-resume-guard.mjs +327 -0
  10. package/lib/completion-auditor.mjs +20 -0
  11. package/lib/context-engine-v11.mjs +20 -7
  12. package/lib/context-manifest.mjs +38 -21
  13. package/lib/decision-policy.mjs +40 -2
  14. package/lib/evidence-store.mjs +121 -31
  15. package/lib/executable-probe.mjs +69 -0
  16. package/lib/execution-contract.mjs +29 -16
  17. package/lib/hierarchical-context.mjs +31 -3
  18. package/lib/learning-engine.mjs +164 -65
  19. package/lib/memory-engine.mjs +348 -98
  20. package/lib/model-config.mjs +86 -7
  21. package/lib/performance-fabric.mjs +354 -107
  22. package/lib/permission-policy.mjs +232 -0
  23. package/lib/pi-rpc-pool.mjs +170 -5
  24. package/lib/provider-recovery.mjs +51 -0
  25. package/lib/repo-graph.mjs +38 -5
  26. package/lib/runtime-events.mjs +31 -10
  27. package/lib/semantic-index.mjs +67 -20
  28. package/lib/skill-compiler.mjs +84 -18
  29. package/lib/subagent-artifacts.mjs +182 -0
  30. package/lib/task-policy.mjs +80 -7
  31. package/lib/trajectory.mjs +25 -1
  32. package/lib/untrusted-output.mjs +106 -0
  33. package/lib/verification-broker.mjs +32 -27
  34. package/lib/workspace-fingerprint.mjs +43 -8
  35. package/lib/workspace-hygiene.mjs +481 -0
  36. package/package.json +8 -6
  37. package/pi/extensions/ues-child-runtime.ts +103 -5
  38. package/pi/extensions/ues.ts +984 -95
  39. package/scripts/benchmark-lsp-pool.mjs +128 -0
  40. package/scripts/check-release-consistency.mjs +16 -4
  41. package/scripts/check-source-integrity.mjs +60 -1
  42. package/scripts/run-test-suite.mjs +143 -0
  43. package/scripts/smoke-package-closure.mjs +4 -0
@@ -0,0 +1,327 @@
1
+ import { createHash } from "node:crypto"
2
+ import { existsSync } from "node:fs"
3
+ import { readdir, readFile } from "node:fs/promises"
4
+ import path from "node:path"
5
+ import { checkpointWork } from "./task-engine.mjs"
6
+
7
+ const WORK_DIR = ".ues-work"
8
+ const MAX_WORKSPACES = 3
9
+ const MAX_TASKS = 28
10
+ const MAX_RECEIPTS = 20
11
+
12
+ function digest(value) {
13
+ return createHash("sha256").update(String(value || "")).digest("hex").slice(0, 20)
14
+ }
15
+
16
+ async function readJson(file, fallback = null) {
17
+ try {
18
+ return JSON.parse(await readFile(file, "utf8"))
19
+ } catch (error) {
20
+ if (error?.code === "ENOENT") return fallback
21
+ return fallback
22
+ }
23
+ }
24
+
25
+ function inside(base, target) {
26
+ const root = path.resolve(base)
27
+ const resolved = path.resolve(target)
28
+ return resolved === root || resolved.startsWith(root + path.sep)
29
+ }
30
+
31
+ function normalizePhasePath(value) {
32
+ return String(value || "").replaceAll("\\", "/").replace(/^\.\/+/, "")
33
+ }
34
+
35
+ async function activeWorkspaces(root, max = MAX_WORKSPACES) {
36
+ const base = path.join(path.resolve(root), WORK_DIR)
37
+ if (!existsSync(base)) return []
38
+ const entries = await readdir(base, { withFileTypes: true }).catch(() => [])
39
+ const rows = []
40
+ for (const entry of entries) {
41
+ if (!entry.isDirectory()) continue
42
+ const dir = path.join(base, entry.name)
43
+ const state = await readJson(path.join(dir, "STATE.json"), null)
44
+ if (!state || String(state.status || "").toLowerCase() === "completed") continue
45
+ rows.push({
46
+ slug: entry.name,
47
+ dir,
48
+ state,
49
+ updatedAt: Date.parse(state.updatedAt || state.createdAt || "") || 0,
50
+ })
51
+ }
52
+ return rows
53
+ .sort((a, b) => b.updatedAt - a.updatedAt || a.slug.localeCompare(b.slug))
54
+ .slice(0, Math.max(1, Math.min(10, Number(max || MAX_WORKSPACES))))
55
+ }
56
+
57
+ function taskRows(plan, state) {
58
+ const tasks = Array.isArray(plan?.tasks) ? plan.tasks : []
59
+ const rows = tasks.map((task) => {
60
+ const runtime = state?.tasks?.[task.id] || {}
61
+ return {
62
+ id: task.id,
63
+ title: String(task.title || "").slice(0, 240),
64
+ status: runtime.status || "unknown",
65
+ attempts: Number(runtime.attempts || 0),
66
+ runId: runtime.runId || null,
67
+ dependsOn: Array.isArray(task.dependsOn) ? task.dependsOn.slice(0, 24) : [],
68
+ evidenceStrength: runtime.evidenceStrength || null,
69
+ report: runtime.report || null,
70
+ lastError: runtime.lastError ? String(runtime.lastError).slice(0, 800) : null,
71
+ }
72
+ })
73
+ rows.sort((a, b) => {
74
+ const rank = (row) => row.status === "running" ? 0 : row.status === "completed" ? 2 : 1
75
+ return rank(a) - rank(b) || String(a.id).localeCompare(String(b.id))
76
+ })
77
+ return rows.slice(0, MAX_TASKS)
78
+ }
79
+
80
+ function receiptRows(evidence) {
81
+ return (Array.isArray(evidence?.receipts) ? evidence.receipts : [])
82
+ .slice(-MAX_RECEIPTS)
83
+ .map((item) => ({
84
+ id: item.id || null,
85
+ kind: item.kind || "verification-receipt",
86
+ task: item.task || null,
87
+ runId: item.runId || null,
88
+ passed: item.passed === true,
89
+ verdict: item.verdict || (item.passed === true ? "PASS" : null),
90
+ command: item.command || null,
91
+ workspaceAfter: item.workspaceAfter || null,
92
+ recordedAt: item.recordedAt || item.finishedAt || null,
93
+ }))
94
+ }
95
+
96
+ function gateRows(evidence) {
97
+ return (Array.isArray(evidence?.gateReceipts) ? evidence.gateReceipts : [])
98
+ .slice(-12)
99
+ .map((item) => ({
100
+ id: item.id || null,
101
+ kind: item.kind || null,
102
+ verdict: item.verdict || null,
103
+ verifier: item.verifier || null,
104
+ planHash: item.planHash || null,
105
+ workspaceFingerprint: item.workspaceFingerprint || null,
106
+ recordedAt: item.recordedAt || null,
107
+ }))
108
+ }
109
+
110
+ async function phaseRows(dir, manifest) {
111
+ const artifacts = Array.isArray(manifest?.artifacts) ? manifest.artifacts : []
112
+ const rows = []
113
+ for (const relative of artifacts.slice(0, 32)) {
114
+ const normalized = normalizePhasePath(relative)
115
+ const target = path.resolve(dir, normalized)
116
+ if (!normalized || !inside(dir, target)) continue
117
+ const phase = await readJson(target, null)
118
+ if (!phase) continue
119
+ rows.push({
120
+ artifact: normalized,
121
+ phase: phase.phase ?? null,
122
+ title: String(phase.title || "").slice(0, 240),
123
+ status: phase.status || "UNKNOWN",
124
+ sourceBodyHash: phase.sourceBodyHash || null,
125
+ evidenceCount: Array.isArray(phase.evidence) ? phase.evidence.length : 0,
126
+ finalVerdicts: phase.finalVerdicts || null,
127
+ })
128
+ }
129
+ return rows
130
+ }
131
+
132
+ function stateSummary(state) {
133
+ return {
134
+ status: state?.status || null,
135
+ goal: state?.goal ? String(state.goal).slice(0, 800) : null,
136
+ updatedAt: state?.updatedAt || null,
137
+ planHash: state?.planHash || null,
138
+ planApproved: state?.planApproved === true,
139
+ nextAction: state?.nextAction || null,
140
+ checkpoint: state?.checkpoint ? {
141
+ id: state.checkpoint.id || null,
142
+ reason: state.checkpoint.reason || null,
143
+ createdAt: state.checkpoint.createdAt || null,
144
+ currentTaskId: state.checkpoint.currentTaskId || null,
145
+ runId: state.checkpoint.runId || null,
146
+ planHash: state.checkpoint.planHash || null,
147
+ workspaceFingerprint: state.checkpoint.workspaceFingerprint || null,
148
+ nextAction: state.checkpoint.nextAction || null,
149
+ evidencePointers: Array.isArray(state.checkpoint.evidencePointers)
150
+ ? state.checkpoint.evidencePointers.slice(-20)
151
+ : [],
152
+ } : null,
153
+ }
154
+ }
155
+
156
+ function buildInstructionEpoch(state, tasks, receipts, gateReceipts) {
157
+ const rows = Array.isArray(tasks) ? tasks : []
158
+ const passedTaskReceipts = new Set(
159
+ (Array.isArray(receipts) ? receipts : [])
160
+ .filter((item) => item?.passed === true && item?.task)
161
+ .map((item) => String(item.task)),
162
+ )
163
+ const completedTaskIds = rows.filter((row) => row.status === "completed").map((row) => row.id)
164
+ const activeTaskIds = rows.filter((row) => row.status === "running").map((row) => row.id)
165
+ const failedTaskIds = rows.filter((row) => row.status === "failed").map((row) => row.id)
166
+ const pendingTaskIds = rows
167
+ .filter((row) => !["completed", "failed"].includes(String(row.status || "").toLowerCase()))
168
+ .map((row) => row.id)
169
+ const tasksWithoutPassingReceipt = completedTaskIds.filter((id) => !passedTaskReceipts.has(String(id)))
170
+ const decisions = (Array.isArray(state?.decisions) ? state.decisions : [])
171
+ .slice(-8)
172
+ .map((item) => ({
173
+ at: item?.at || null,
174
+ text: item?.text ? String(item.text).slice(0, 600) : null,
175
+ source: item?.source || null,
176
+ risk: item?.policy?.risk || null,
177
+ autoResolvable: item?.policy?.autoResolvable === true,
178
+ confidence: Number.isFinite(Number(item?.policy?.decision?.confidence))
179
+ ? Number(item.policy.decision.confidence)
180
+ : null,
181
+ }))
182
+
183
+ const body = {
184
+ schemaVersion: 1,
185
+ source: "durable-state-fold",
186
+ objective: state?.goal ? String(state.goal).slice(0, 1200) : null,
187
+ status: state?.status || null,
188
+ planHash: state?.planHash || null,
189
+ planApproved: state?.planApproved === true,
190
+ activeTaskIds,
191
+ pendingTaskIds,
192
+ completedTaskIds,
193
+ failedTaskIds,
194
+ blockers: (Array.isArray(state?.blockers) ? state.blockers : []).slice(-12).map((item) => String(item).slice(0, 500)),
195
+ recentDecisions: decisions,
196
+ checkpoint: state?.checkpoint ? {
197
+ id: state.checkpoint.id || null,
198
+ currentTaskId: state.checkpoint.currentTaskId || null,
199
+ runId: state.checkpoint.runId || null,
200
+ workspaceFingerprint: state.checkpoint.workspaceFingerprint || null,
201
+ nextAction: state.checkpoint.nextAction || null,
202
+ } : null,
203
+ nextAction: state?.nextAction || null,
204
+ verification: {
205
+ tasksWithoutPassingReceipt,
206
+ passingReceiptIds: (Array.isArray(receipts) ? receipts : [])
207
+ .filter((item) => item?.passed === true && item?.id)
208
+ .slice(-20)
209
+ .map((item) => item.id),
210
+ passingGateReceiptIds: (Array.isArray(gateReceipts) ? gateReceipts : [])
211
+ .filter((item) => String(item?.verdict || "").toUpperCase() === "PASS" && item?.id)
212
+ .slice(-12)
213
+ .map((item) => item.id),
214
+ },
215
+ }
216
+ return {
217
+ ...body,
218
+ epochHash: digest(JSON.stringify(body)),
219
+ }
220
+ }
221
+
222
+ export async function checkpointDurableWorkBeforeCompaction(root, options = {}) {
223
+ const reason = String(options.reason || "unknown")
224
+ const rows = await activeWorkspaces(root, options.maxWorkspaces || MAX_WORKSPACES)
225
+ const results = []
226
+ for (const row of rows) {
227
+ try {
228
+ const checkpoint = await checkpointWork(root, row.slug, {
229
+ reason: `pi-compaction:${reason}`,
230
+ })
231
+ results.push({ slug: row.slug, checkpoint, ok: true })
232
+ } catch (error) {
233
+ results.push({
234
+ slug: row.slug,
235
+ checkpoint: null,
236
+ ok: false,
237
+ error: error instanceof Error ? error.message : String(error),
238
+ })
239
+ }
240
+ }
241
+ return results
242
+ }
243
+
244
+ export async function buildCompactionResumeGuard(root, options = {}) {
245
+ const rows = await activeWorkspaces(root, options.maxWorkspaces || MAX_WORKSPACES)
246
+ const workspaces = []
247
+ for (const row of rows) {
248
+ const [plan, evidence, contract, manifest] = await Promise.all([
249
+ readJson(path.join(row.dir, "PLAN.json"), null),
250
+ readJson(path.join(row.dir, "EVIDENCE.json"), {}),
251
+ readJson(path.join(row.dir, "EXECUTION_CONTRACT.json"), null),
252
+ readJson(path.join(row.dir, "phases", "MANIFEST.json"), null),
253
+ ])
254
+ const phases = await phaseRows(row.dir, manifest)
255
+ const sources = {
256
+ state: path.relative(root, path.join(row.dir, "STATE.json")).replaceAll("\\", "/"),
257
+ plan: path.relative(root, path.join(row.dir, "PLAN.json")).replaceAll("\\", "/"),
258
+ evidence: path.relative(root, path.join(row.dir, "EVIDENCE.json")).replaceAll("\\", "/"),
259
+ executionContract: contract
260
+ ? path.relative(root, path.join(row.dir, "EXECUTION_CONTRACT.json")).replaceAll("\\", "/")
261
+ : null,
262
+ phaseManifest: manifest
263
+ ? path.relative(root, path.join(row.dir, "phases", "MANIFEST.json")).replaceAll("\\", "/")
264
+ : null,
265
+ }
266
+ const tasks = taskRows(plan, row.state)
267
+ const receipts = receiptRows(evidence)
268
+ const gateReceipts = gateRows(evidence)
269
+ const instructionEpoch = buildInstructionEpoch(row.state, tasks, receipts, gateReceipts)
270
+ workspaces.push({
271
+ slug: row.slug,
272
+ sources,
273
+ sourceHashes: {
274
+ state: digest(JSON.stringify(row.state)),
275
+ plan: digest(JSON.stringify(plan || {})),
276
+ evidence: digest(JSON.stringify(evidence || {})),
277
+ executionContract: contract ? digest(JSON.stringify(contract)) : null,
278
+ phaseManifest: manifest ? digest(JSON.stringify(manifest)) : null,
279
+ },
280
+ state: stateSummary(row.state),
281
+ instructionEpoch,
282
+ tasks,
283
+ receipts,
284
+ gateReceipts,
285
+ executionContract: contract ? {
286
+ schemaVersion: contract.schemaVersion || null,
287
+ taskHash: contract.taskHash || null,
288
+ gates: contract.gates || null,
289
+ phaseCount: Array.isArray(contract.phases) ? contract.phases.length : 0,
290
+ approvedInheritedDirtyPaths: Array.isArray(contract.approvedInheritedDirtyPaths)
291
+ ? contract.approvedInheritedDirtyPaths.slice(0, 40)
292
+ : [],
293
+ } : null,
294
+ phases,
295
+ })
296
+ }
297
+
298
+ return {
299
+ schemaVersion: 1,
300
+ kind: "ues-durable-compaction-resume-guard",
301
+ generatedAt: new Date().toISOString(),
302
+ compactionReason: options.reason || null,
303
+ source: "deterministic-filesystem-artifacts",
304
+ modelSummaryTrustedForDurableState: false,
305
+ workspaceCount: workspaces.length,
306
+ workspaces,
307
+ }
308
+ }
309
+
310
+ export function renderCompactionResumeGuard(packet, options = {}) {
311
+ if (!packet?.workspaces?.length) return ""
312
+ const maxChars = Math.max(8000, Math.min(48000, Number(options.maxChars || 28000)))
313
+ const preface = [
314
+ "## UES Durable Compaction Resume Guard",
315
+ "Authoritative durable state was rebuilt from filesystem artifacts after Pi compaction.",
316
+ "The model-generated compaction summary is advisory only for durable execution state.",
317
+ "If the summary conflicts with STATE.json, PLAN.json, EXECUTION_CONTRACT.json, phase artifacts, instructionEpoch, or evidence receipts below, the deterministic artifacts win.",
318
+ "Treat instructionEpoch as the compact deterministic control-plane fold: objective, active work, blockers, decisions, receipt gaps and next action.",
319
+ "Do not mark a task/phase complete unless the rebuilt state and fresh receipts support it.",
320
+ "Follow checkpoint.nextAction/state.nextAction and re-read the referenced artifact when more detail is required.",
321
+ "",
322
+ ].join("\n")
323
+ const encoded = JSON.stringify(packet, null, 2)
324
+ if (preface.length + encoded.length <= maxChars) return preface + encoded
325
+ return preface + encoded.slice(0, Math.max(0, maxChars - preface.length - 120)) +
326
+ "\n...[resume packet bounded; re-read referenced .ues-work artifacts for exact detail]"
327
+ }
@@ -42,6 +42,17 @@ function receiptPassed(row = {}) {
42
42
  return Boolean(receipt?.passed === true && Number(receipt?.exitCode) === 0)
43
43
  }
44
44
 
45
+ function claimEvidenceStatus(value) {
46
+ const lines = text(value).split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
47
+ const counts = { verified: 0, inferred: 0, unknown: 0 }
48
+ for (const line of lines) {
49
+ if (/^(?:[-*]\s*)?VERIFIED\s*:/i.test(line)) counts.verified += 1
50
+ else if (/^(?:[-*]\s*)?INFERRED\s*:/i.test(line)) counts.inferred += 1
51
+ else if (/^(?:[-*]\s*)?UNKNOWN\s*:/i.test(line)) counts.unknown += 1
52
+ }
53
+ return counts
54
+ }
55
+
45
56
  export function auditCompletion(input = {}) {
46
57
  const failures = []
47
58
  const warnings = []
@@ -67,6 +78,14 @@ export function auditCompletion(input = {}) {
67
78
  }
68
79
  if (Object.hasOwn(sections, "checks-not-run") && !benignEmptySection(sections["checks-not-run"])) warnings.push("verification-reports-checks-not-run")
69
80
 
81
+ const claimStatus = claimEvidenceStatus(sections["acceptance-criteria-proven"])
82
+ const claimStatusCount = claimStatus.verified + claimStatus.inferred + claimStatus.unknown
83
+ if (input.requireClaimEvidenceStatus === true && claimStatusCount === 0) {
84
+ failures.push("acceptance-criteria-evidence-status-missing")
85
+ }
86
+ if (claimStatus.inferred > 0) failures.push("acceptance-criteria-inferred-not-proven")
87
+ if (claimStatus.unknown > 0) failures.push("acceptance-criteria-unknown-not-proven")
88
+
70
89
  const snapshot = input.workspaceSnapshot || null
71
90
  if (snapshot) {
72
91
  if (snapshot.cacheable === true && !text(snapshot.fingerprint)) failures.push("workspace-fingerprint-missing")
@@ -95,6 +114,7 @@ export function auditCompletion(input = {}) {
95
114
  visual: requireVisual ? passRun(visual) : null,
96
115
  reportSections: Object.keys(sections).sort(),
97
116
  passingReceiptCount: passingReceipts.length,
117
+ claimEvidenceStatus: claimStatus,
98
118
  workspaceFingerprint: snapshot?.fingerprint || null,
99
119
  changedFiles: Array.isArray(snapshot?.changedFiles) ? snapshot.changedFiles.slice(0, 200) : [],
100
120
  },
@@ -101,6 +101,16 @@ export async function buildAdaptiveTaskContext(root, task, options = {}) {
101
101
  const policy = options.policy || {}
102
102
  const capabilities = options.capabilities || inferTaskCapabilities(taskText(task), options.facts || {})
103
103
  const evidenceBudget = options.evidenceBudget || planEvidenceBudget(policy, task, capabilities.required)
104
+ const contextBuildStartedAt = Date.now()
105
+ const memoryPromise = retrieveMemories(root, taskText(task), {
106
+ files: taskFilesForMemory(task),
107
+ limit: options.memoryLimit ?? 6,
108
+ taskClass: options.taskClass || task.type || policy.executionProfile || policy.profile?.name || null,
109
+ touch: options.touchMemories !== false,
110
+ }).catch(() => ({ schemaVersion: 1, query: taskText(task), eligible: 0, results: [] }))
111
+ const fabricPromise = capabilityFabricStatus(root).catch(() => null)
112
+
113
+ const manifestStartedAt = Date.now()
104
114
  const manifest = await buildContextManifest(root, task, {
105
115
  budget: evidenceBudget.total,
106
116
  evidenceBudget,
@@ -110,21 +120,18 @@ export async function buildAdaptiveTaskContext(root, task, options = {}) {
110
120
  changedFiles: options.changedFiles,
111
121
  workspaceFingerprint: options.workspaceFingerprint,
112
122
  })
123
+ const manifestMs = Date.now() - manifestStartedAt
113
124
 
125
+ const externalizeStartedAt = Date.now()
114
126
  const externalized = await externalizeContextExcerpts(root, manifest, {
115
127
  task,
116
128
  threshold: options.externalizeThreshold,
117
129
  inlineChars: options.inlineChars,
118
130
  })
131
+ const externalizeMs = Date.now() - externalizeStartedAt
119
132
 
120
- const memoryRetrieval = await retrieveMemories(root, taskText(task), {
121
- files: taskFilesForMemory(task),
122
- limit: options.memoryLimit ?? 6,
123
- taskClass: options.taskClass || task.type || policy.executionProfile || policy.profile?.name || null,
124
- touch: options.touchMemories !== false,
125
- }).catch(() => ({ schemaVersion: 1, query: taskText(task), eligible: 0, results: [] }))
133
+ const [memoryRetrieval, fabric] = await Promise.all([memoryPromise, fabricPromise])
126
134
  const memories = memoryRetrieval.results || []
127
- const fabric = await capabilityFabricStatus(root).catch(() => null)
128
135
  const providerHints = (fabric?.rows || [])
129
136
  .filter((row) => row.selected)
130
137
  .slice(0, 12)
@@ -202,6 +209,12 @@ export async function buildAdaptiveTaskContext(root, task, options = {}) {
202
209
  entries: externalized.externalized,
203
210
  },
204
211
  promptEnvelope,
212
+ performance: {
213
+ parallelContextPreparation: true,
214
+ manifestMs,
215
+ externalizeMs,
216
+ totalMs: Date.now() - contextBuildStartedAt,
217
+ },
205
218
  promptCache: {
206
219
  stablePrefixHash: promptEnvelope.stablePrefixHash,
207
220
  dynamicHash: promptEnvelope.dynamicHash,
@@ -7,6 +7,7 @@ import { taskFiles } from "./task-graph.mjs"
7
7
  import { buildSemanticIndexCached, querySemanticIndex } from "./semantic-index.mjs"
8
8
  import { queryContextHierarchy } from "./hierarchical-context.mjs"
9
9
  import { rankContextGraph } from "./context-graph-rank.mjs"
10
+ import { runtimeWorkspaceSnapshot } from "./workspace-fingerprint.mjs"
10
11
 
11
12
  const ROOT_INSTRUCTIONS = [
12
13
  "AGENTS.md",
@@ -249,9 +250,20 @@ export async function buildContextManifest(root, task, options = {}) {
249
250
  const evidenceBudget = options.evidenceBudget || null
250
251
  const declared = taskFiles(task)
251
252
  const terms = taskTerms(task)
253
+ let workspaceSnapshot = null
254
+ const suppliedFingerprint = String(options.workspaceFingerprint || "").trim()
255
+ if (!Array.isArray(options.changedFiles) || !suppliedFingerprint) {
256
+ try {
257
+ workspaceSnapshot = runtimeWorkspaceSnapshot(root, { workspaceState: options.workspaceState })
258
+ } catch {}
259
+ }
260
+ const reusableWorkspaceFingerprint = suppliedFingerprint ||
261
+ (workspaceSnapshot?.cacheable ? String(workspaceSnapshot.fingerprint || "") : "")
252
262
  const changed = Array.isArray(options.changedFiles)
253
263
  ? options.changedFiles.map((file) => String(file || "").replaceAll("\\", "/")).filter(Boolean)
254
- : gitChangedFiles(root)
264
+ : Array.isArray(workspaceSnapshot?.changedFiles)
265
+ ? workspaceSnapshot.changedFiles
266
+ : gitChangedFiles(root)
255
267
  const strategy = String(options.strategy || "semantic+graph+git")
256
268
  const queryText = [
257
269
  task?.title,
@@ -263,22 +275,32 @@ export async function buildContextManifest(root, task, options = {}) {
263
275
  const semanticSnapshot = await buildSemanticIndexCached(root, {
264
276
  maxFiles: options.semanticMaxFiles ?? 6000,
265
277
  maxDepth: options.semanticMaxDepth ?? 14,
266
- workspaceFingerprint: options.workspaceFingerprint,
278
+ workspaceFingerprint: reusableWorkspaceFingerprint,
267
279
  }).catch(() => null)
268
280
 
269
- const hierarchy = await queryContextHierarchy(root, queryText, {
270
- declaredFiles: declared,
271
- maxScopes: options.hierarchyMaxScopes ?? 6,
272
- maxFiles: options.semanticMaxFiles ?? 6000,
273
- builtIndex: semanticSnapshot || undefined,
274
- }).catch(() => null)
281
+ const useGraph = strategy.includes("graph")
282
+ const [hierarchy, semantic, graph, instructions] = await Promise.all([
283
+ queryContextHierarchy(root, queryText, {
284
+ declaredFiles: declared,
285
+ maxScopes: options.hierarchyMaxScopes ?? 6,
286
+ maxFiles: options.semanticMaxFiles ?? 6000,
287
+ builtIndex: semanticSnapshot || undefined,
288
+ }).catch(() => null),
289
+ querySemanticIndex(root, queryText, {
290
+ limit: 32,
291
+ maxFiles: options.semanticMaxFiles ?? 6000,
292
+ builtIndex: semanticSnapshot || undefined,
293
+ }).catch(() => null),
294
+ useGraph
295
+ ? buildRepoGraphCached(root, {
296
+ maxFiles: options.maxFiles ?? 2500,
297
+ workspaceFingerprint: reusableWorkspaceFingerprint,
298
+ }).catch(() => ({ nodes: [], edges: [], hotspots: [], scannedFiles: 0, truncated: false }))
299
+ : Promise.resolve({ nodes: [], edges: [], hotspots: [], scannedFiles: 0, truncated: false }),
300
+ nearbyInstructions(root, declared).catch(() => []),
301
+ ])
275
302
  const hierarchyScopes = hierarchy?.scopes?.map((item) => item.path) || []
276
303
 
277
- const semantic = await querySemanticIndex(root, queryText, {
278
- limit: 32,
279
- maxFiles: options.semanticMaxFiles ?? 6000,
280
- builtIndex: semanticSnapshot || undefined,
281
- }).catch(() => null)
282
304
  const scopedSemanticResults = (semantic?.results || []).map((item) => {
283
305
  const scopeRank = hierarchyScopes.findIndex((scope) =>
284
306
  scope === "." || item.path === scope || item.path.startsWith(scope + "/"),
@@ -293,13 +315,6 @@ export async function buildContextManifest(root, task, options = {}) {
293
315
  }).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
294
316
  const semanticPaths = scopedSemanticResults.map((item) => item.path)
295
317
 
296
- const useGraph = strategy.includes("graph")
297
- const graph = useGraph
298
- ? await buildRepoGraphCached(root, {
299
- maxFiles: options.maxFiles ?? 2500,
300
- workspaceFingerprint: options.workspaceFingerprint,
301
- })
302
- : { nodes: [], edges: [], hotspots: [], scannedFiles: 0, truncated: false }
303
318
  const nodeMap = new Map(graph.nodes.map((node) => [node.path, node]))
304
319
  const incoming = new Map()
305
320
 
@@ -330,7 +345,6 @@ export async function buildContextManifest(root, task, options = {}) {
330
345
  .slice(0, 24)
331
346
  .map((item) => item.file)
332
347
 
333
- const instructions = await nearbyInstructions(root, declared)
334
348
  const graphRanked = useGraph
335
349
  ? rankContextGraph(graph, {
336
350
  semanticResults: scopedSemanticResults,
@@ -437,6 +451,7 @@ export async function buildContextManifest(root, task, options = {}) {
437
451
  excerpts,
438
452
  hierarchy: hierarchy ? {
439
453
  schemaVersion: hierarchy.schemaVersion,
454
+ runtimeCacheHit: hierarchy.runtimeCacheHit === true,
440
455
  levels: hierarchy.levels,
441
456
  scopes: hierarchy.scopes.slice(0, 6).map((item) => ({
442
457
  path: item.path,
@@ -450,6 +465,8 @@ export async function buildContextManifest(root, task, options = {}) {
450
465
  semantic: semantic ? {
451
466
  evidenceLevel: semantic.evidenceLevel,
452
467
  resultCount: semantic.results.length,
468
+ runtimeCacheHit: semanticSnapshot?.runtimeCacheHit === true,
469
+ runtimeCacheCoalesced: semanticSnapshot?.runtimeCacheCoalesced === true,
453
470
  stats: semantic.stats,
454
471
  } : null,
455
472
  graph: {
@@ -2,6 +2,21 @@ const HIGH_RISK = /(npm publish|publish package|deploy|production|git push|force
2
2
  const MEDIUM_RISK = /(migration|schema|dependency upgrade|lockfile|generated code|shared config|auth|permission|security)/i
3
3
  const REVERSIBLE = /(local|test|rename|format|refactor|temporary|fixture|internal|reversible)/i
4
4
 
5
+ function confidenceForDecision(input = {}) {
6
+ if (input.requiresUser === true && (input.destructive === true || input.externalSideEffect === true || input.explicitlyIrreversible === true)) return 0.99
7
+ if (input.reversible === true && input.risk === "low") return 0.96
8
+ if (input.risk === "medium") return 0.82
9
+ if (input.requiresUser === true) return 0.90
10
+ return 0.74
11
+ }
12
+
13
+ function confidenceBand(value) {
14
+ const score = Number(value || 0)
15
+ if (score >= 0.90) return "high"
16
+ if (score >= 0.72) return "medium"
17
+ return "low"
18
+ }
19
+
5
20
  export function classifyDecisionPolicy(text = "", facts = {}) {
6
21
  const value = String(text || "").trim()
7
22
  const explicitlyIrreversible = facts.irreversible === true
@@ -11,10 +26,33 @@ export function classifyDecisionPolicy(text = "", facts = {}) {
11
26
  const reversible = facts.reversible === true || (!destructive && REVERSIBLE.test(value))
12
27
  const risk = destructive || explicitlyIrreversible || externalSideEffect ? "high" : medium ? "medium" : "low"
13
28
  const requiresUser = risk === "high" || facts.productDecision === true
29
+ const autoResolvable = !requiresUser && (reversible || risk === "low")
30
+ const confidence = confidenceForDecision({
31
+ requiresUser,
32
+ destructive,
33
+ externalSideEffect,
34
+ explicitlyIrreversible,
35
+ reversible,
36
+ risk,
37
+ })
14
38
  return {
15
- schemaVersion: 1, risk, reversible, destructive, externalSideEffect, requiresUser,
16
- autoResolvable: !requiresUser && (reversible || risk === "low"),
39
+ schemaVersion: 1,
40
+ risk,
41
+ reversible,
42
+ destructive,
43
+ externalSideEffect,
44
+ requiresUser,
45
+ autoResolvable,
17
46
  reason: requiresUser ? "human-approval-required" : reversible ? "reversible-local-decision" : "bounded-engineering-decision",
47
+ decision: {
48
+ schemaVersion: 1,
49
+ kind: String(facts.decisionKind || "engineering-choice"),
50
+ source: "deterministic",
51
+ value: requiresUser ? "escalate-to-user" : autoResolvable ? "auto-resolve" : "bounded-review",
52
+ confidence,
53
+ confidenceBand: confidenceBand(confidence),
54
+ crossCheckRecommended: confidence < 0.72 || risk === "medium",
55
+ },
18
56
  }
19
57
  }
20
58