opencode-agent-skill 15.1.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 (37) hide show
  1. package/README.md +3 -3
  2. package/docs/V15-MANAGED-RUNTIME.md +18 -0
  3. package/global-config/agents/verifier.md +6 -2
  4. package/lib/affected-tests.mjs +21 -4
  5. package/lib/capability-fabric.mjs +107 -48
  6. package/lib/code-intelligence/index.mjs +3 -15
  7. package/lib/code-intelligence/lsp-pool.mjs +694 -0
  8. package/lib/code-intelligence/lsp-provider.mjs +160 -20
  9. package/lib/compaction-resume-guard.mjs +76 -4
  10. package/lib/completion-auditor.mjs +20 -0
  11. package/lib/context-manifest.mjs +38 -21
  12. package/lib/decision-policy.mjs +40 -2
  13. package/lib/evidence-store.mjs +121 -31
  14. package/lib/executable-probe.mjs +69 -0
  15. package/lib/hierarchical-context.mjs +31 -3
  16. package/lib/learning-engine.mjs +164 -65
  17. package/lib/memory-engine.mjs +163 -86
  18. package/lib/model-config.mjs +86 -7
  19. package/lib/performance-fabric.mjs +92 -0
  20. package/lib/permission-policy.mjs +232 -0
  21. package/lib/pi-rpc-pool.mjs +170 -5
  22. package/lib/provider-recovery.mjs +51 -0
  23. package/lib/repo-graph.mjs +38 -5
  24. package/lib/runtime-events.mjs +31 -10
  25. package/lib/semantic-index.mjs +67 -20
  26. package/lib/skill-compiler.mjs +84 -18
  27. package/lib/task-policy.mjs +80 -7
  28. package/lib/trajectory.mjs +25 -1
  29. package/lib/untrusted-output.mjs +106 -0
  30. package/package.json +8 -6
  31. package/pi/extensions/ues-child-runtime.ts +78 -3
  32. package/pi/extensions/ues.ts +695 -81
  33. package/scripts/benchmark-lsp-pool.mjs +128 -0
  34. package/scripts/check-release-consistency.mjs +16 -4
  35. package/scripts/check-source-integrity.mjs +60 -1
  36. package/scripts/run-test-suite.mjs +143 -0
  37. package/scripts/smoke-package-closure.mjs +4 -0
@@ -1,6 +1,10 @@
1
- import { spawn, spawnSync } from "node:child_process"
1
+ import { spawn } from "node:child_process"
2
+ import { existsSync } from "node:fs"
2
3
  import { lstat, readFile, realpath } from "node:fs/promises"
3
4
  import path from "node:path"
5
+ import { commandExists } from "../executable-probe.mjs"
6
+ import { resolveWindowsCommand } from "../windows-shim.mjs"
7
+ import { lspPoolStatus as managedLspPoolStatus, shutdownLspPool as shutdownManagedLspPool, withManagedLspSession } from "./lsp-pool.mjs"
4
8
  import { fileURLToPath, pathToFileURL } from "node:url"
5
9
 
6
10
  const PROVIDERS = [
@@ -22,19 +26,6 @@ export const LSP_OPERATIONS = Object.freeze([
22
26
  "outgoing-calls",
23
27
  ])
24
28
 
25
- const commandCache = new Map()
26
- const COMMAND_TTL_MS = 60_000
27
-
28
- function commandExists(command) {
29
- const now = Date.now()
30
- const cached = commandCache.get(command)
31
- if (cached && now - cached.at < COMMAND_TTL_MS) return cached.available
32
- const finder = process.platform === "win32" ? "where" : "which"
33
- const available = spawnSync(finder, [command], { stdio: "ignore", windowsHide: true }).status === 0
34
- commandCache.set(command, { available, at: now })
35
- return available
36
- }
37
-
38
29
  function inside(root, target) {
39
30
  return target === root || target.startsWith(root + path.sep)
40
31
  }
@@ -50,18 +41,73 @@ async function safeFile(root, relative) {
50
41
  return { base, file: actual, info }
51
42
  }
52
43
 
44
+ export function resolveTypeScriptTsserverFallback(options = {}) {
45
+ const explicit = String(options.explicitPath ?? process.env.UES_TYPESCRIPT_TSSERVER_PATH ?? "").trim()
46
+ if (explicit && existsSync(path.resolve(explicit))) return path.resolve(explicit)
47
+
48
+ const platform = String(options.platform || process.platform)
49
+ if (platform !== "win32") return null
50
+
51
+ const execution = options.execution || resolveWindowsCommand("tsserver")
52
+ const candidates = [
53
+ execution?.entry,
54
+ ...(Array.isArray(execution?.argsPrefix) ? execution.argsPrefix : []),
55
+ ].filter(Boolean)
56
+
57
+ for (const value of candidates) {
58
+ const entry = path.resolve(String(value))
59
+ const binDir = path.dirname(entry)
60
+ const packageRoot = path.basename(binDir).toLowerCase() === "bin"
61
+ ? path.dirname(binDir)
62
+ : null
63
+ const preferred = packageRoot ? path.join(packageRoot, "lib", "tsserver.js") : null
64
+ if (preferred && existsSync(preferred)) return preferred
65
+ if (existsSync(entry)) return entry
66
+ }
67
+ return null
68
+ }
69
+
70
+ function providerRuntimeOptions(provider) {
71
+ if (provider?.id !== "typescript-language-server") return {}
72
+ const fallbackPath = resolveTypeScriptTsserverFallback()
73
+ return fallbackPath
74
+ ? { initializationOptions: { tsserver: { fallbackPath } } }
75
+ : {}
76
+ }
77
+
53
78
  function providerFor(file) {
54
79
  const ext = path.extname(file).toLowerCase()
55
80
  for (const provider of PROVIDERS) {
56
81
  if (!provider.extensions.has(ext)) continue
57
82
  for (const [command, args] of provider.commands) {
58
- if (commandExists(command)) return { ...provider, command, args, languageId: provider.languageId(ext) }
83
+ if (commandExists(command)) {
84
+ return {
85
+ ...provider,
86
+ ...providerRuntimeOptions(provider),
87
+ command,
88
+ args,
89
+ languageId: provider.languageId(ext),
90
+ }
91
+ }
59
92
  }
60
93
  return { ...provider, command: null, args: [], languageId: provider.languageId(ext) }
61
94
  }
62
95
  return null
63
96
  }
64
97
 
98
+ export function lspPersistencePolicy(options = {}) {
99
+ if (typeof options.persistent === "boolean") {
100
+ return { enabled: options.persistent, source: "explicit-option" }
101
+ }
102
+ const env = String(process.env.UES_LSP_PERSISTENT || "").trim()
103
+ if (env === "0") return { enabled: false, source: "env-disabled" }
104
+ if (env === "1") return { enabled: true, source: "env-enabled" }
105
+ if (process.env.UES_CHILD_PROCESS === "1") {
106
+ return { enabled: true, source: "pi-child-runtime" }
107
+ }
108
+ return { enabled: false, source: "short-lived-default" }
109
+ }
110
+
65
111
  export function lspProviderStatus(file = "") {
66
112
  const ext = path.extname(String(file || "")).toLowerCase()
67
113
  const providers = PROVIDERS
@@ -75,6 +121,13 @@ export function lspProviderStatus(file = "") {
75
121
  extension: ext,
76
122
  available: providers.some((row) => row.available),
77
123
  operations: [...LSP_OPERATIONS],
124
+ persistentPool: {
125
+ ...managedLspPoolStatus({
126
+ includeSessions: false,
127
+ persistent: lspPersistencePolicy().enabled,
128
+ }),
129
+ policy: lspPersistencePolicy(),
130
+ },
78
131
  providers,
79
132
  }
80
133
  }
@@ -199,7 +252,7 @@ function sanitizeHierarchyCalls(base, values, direction, limit = 120) {
199
252
  })
200
253
  }
201
254
 
202
- async function withLspSession(root, relative, options, operation) {
255
+ async function withEphemeralLspSession(root, relative, options, operation) {
203
256
  const target = await safeFile(root, relative)
204
257
  const provider = providerFor(target.file)
205
258
  const relativeFile = path.relative(target.base, target.file).replaceAll("\\", "/")
@@ -222,8 +275,26 @@ async function withLspSession(root, relative, options, operation) {
222
275
  const rootUri = pathToFileURL(target.base + path.sep).href
223
276
  const timeoutMs = Math.max(500, Math.min(15_000, Number(options.timeoutMs || 5000)))
224
277
 
278
+ const execution = process.platform === "win32"
279
+ ? resolveWindowsCommand(provider.command)
280
+ : { executable: provider.command, argsPrefix: [] }
281
+ if (!execution?.executable) {
282
+ return {
283
+ schemaVersion: 2,
284
+ file: relativeFile,
285
+ available: true,
286
+ provider: provider.id,
287
+ command: provider.command,
288
+ operation: options.operation || null,
289
+ reason: "spawn-error",
290
+ result: null,
291
+ stderr: null,
292
+ error: "unable to resolve LSP executable: " + provider.command,
293
+ }
294
+ }
295
+
225
296
  return await new Promise((resolve) => {
226
- const child = spawn(provider.command, provider.args, {
297
+ const child = spawn(execution.executable, [...(execution.argsPrefix || []), ...provider.args], {
227
298
  cwd: target.base,
228
299
  stdio: ["pipe", "pipe", "pipe"],
229
300
  windowsHide: true,
@@ -364,6 +435,7 @@ async function withLspSession(root, relative, options, operation) {
364
435
  workspace: { workspaceFolders: true, applyEdit: false },
365
436
  },
366
437
  workspaceFolders: [{ uri: rootUri, name: path.basename(target.base) || "workspace" }],
438
+ initializationOptions: provider.initializationOptions || undefined,
367
439
  })
368
440
  notify("initialized", {})
369
441
  notify("textDocument/didOpen", {
@@ -388,13 +460,35 @@ async function withLspSession(root, relative, options, operation) {
388
460
  export async function lspOperation(root, relative, operation, options = {}) {
389
461
  const op = String(operation || "")
390
462
  if (!LSP_OPERATIONS.includes(op)) throw new Error("unsupported LSP operation: " + op)
463
+
464
+ const target = await safeFile(root, relative)
465
+ const provider = providerFor(target.file)
466
+ const relativeFile = path.relative(target.base, target.file).replaceAll("\\", "/")
467
+ if (!provider?.command) {
468
+ return {
469
+ schemaVersion: 2,
470
+ file: relativeFile,
471
+ available: false,
472
+ provider: provider?.id || null,
473
+ operation: op,
474
+ reason: provider ? "lsp-command-unavailable" : "unsupported-extension",
475
+ result: null,
476
+ persistent: false,
477
+ }
478
+ }
479
+
391
480
  const maxResults = Math.max(1, Math.min(500, Number(options.maxResults || 120)))
392
- return withLspSession(root, relative, { ...options, operation: op }, async (session) => {
481
+ const execute = async (session) => {
393
482
  const pos = position(options)
394
483
  const textDocument = { uri: session.uri }
395
484
 
396
485
  if (op === "diagnostics") {
397
- const params = await session.waitForNotification("textDocument/publishDiagnostics")
486
+ const params = await session.waitForNotification("textDocument/publishDiagnostics", {
487
+ predicate: (value) =>
488
+ (!value?.uri || value.uri === session.uri) &&
489
+ (value?.version == null || Number(value.version) >= Number(session.documentSync?.version || 0)),
490
+ afterAt: session.documentSync?.changed ? session.documentSync.syncedAt : 0,
491
+ })
398
492
  const values = Array.isArray(params?.diagnostics) ? params.diagnostics : []
399
493
  return { diagnostics: values.slice(0, maxResults).map(sanitizeDiagnostic) }
400
494
  }
@@ -450,7 +544,53 @@ export async function lspOperation(root, relative, operation, options = {}) {
450
544
  }
451
545
  }
452
546
  return null
453
- })
547
+ }
548
+
549
+ const persistence = lspPersistencePolicy(options)
550
+ const managed = await withManagedLspSession(
551
+ target,
552
+ provider,
553
+ { ...options, persistent: persistence.enabled, operation: op },
554
+ execute,
555
+ ).catch((error) => ({
556
+ ok: false,
557
+ reason: "managed-lsp-exception",
558
+ error: String(error instanceof Error ? error.message : error).slice(0, 1200),
559
+ }))
560
+
561
+ if (managed?.ok) {
562
+ return {
563
+ schemaVersion: 2,
564
+ file: relativeFile,
565
+ available: true,
566
+ provider: provider.id,
567
+ command: provider.command,
568
+ operation: op,
569
+ reason: "ok",
570
+ result: managed.result,
571
+ stderr: managed.stderr || null,
572
+ error: null,
573
+ persistent: true,
574
+ pool: { ...managed.meta, policy: persistence },
575
+ }
576
+ }
577
+
578
+ const fallback = await withEphemeralLspSession(root, relative, { ...options, operation: op }, execute)
579
+ return {
580
+ ...fallback,
581
+ persistent: false,
582
+ fallbackFrom: managed?.reason || "managed-lsp-unavailable",
583
+ managedError: managed?.error || null,
584
+ persistencePolicy: persistence,
585
+ }
586
+ }
587
+
588
+ export function lspPoolStatus(options = {}) {
589
+ return managedLspPoolStatus(options)
590
+ }
591
+
592
+ export async function shutdownLspPool(root = null) {
593
+ return shutdownManagedLspPool(root)
454
594
  }
455
595
 
456
596
  export async function diagnoseCode(root, relative, options = {}) {
@@ -153,6 +153,72 @@ function stateSummary(state) {
153
153
  }
154
154
  }
155
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
+
156
222
  export async function checkpointDurableWorkBeforeCompaction(root, options = {}) {
157
223
  const reason = String(options.reason || "unknown")
158
224
  const rows = await activeWorkspaces(root, options.maxWorkspaces || MAX_WORKSPACES)
@@ -197,6 +263,10 @@ export async function buildCompactionResumeGuard(root, options = {}) {
197
263
  ? path.relative(root, path.join(row.dir, "phases", "MANIFEST.json")).replaceAll("\\", "/")
198
264
  : null,
199
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)
200
270
  workspaces.push({
201
271
  slug: row.slug,
202
272
  sources,
@@ -208,9 +278,10 @@ export async function buildCompactionResumeGuard(root, options = {}) {
208
278
  phaseManifest: manifest ? digest(JSON.stringify(manifest)) : null,
209
279
  },
210
280
  state: stateSummary(row.state),
211
- tasks: taskRows(plan, row.state),
212
- receipts: receiptRows(evidence),
213
- gateReceipts: gateRows(evidence),
281
+ instructionEpoch,
282
+ tasks,
283
+ receipts,
284
+ gateReceipts,
214
285
  executionContract: contract ? {
215
286
  schemaVersion: contract.schemaVersion || null,
216
287
  taskHash: contract.taskHash || null,
@@ -243,7 +314,8 @@ export function renderCompactionResumeGuard(packet, options = {}) {
243
314
  "## UES Durable Compaction Resume Guard",
244
315
  "Authoritative durable state was rebuilt from filesystem artifacts after Pi compaction.",
245
316
  "The model-generated compaction summary is advisory only for durable execution state.",
246
- "If the summary conflicts with STATE.json, PLAN.json, EXECUTION_CONTRACT.json, phase artifacts, or evidence receipts below, the deterministic artifacts win.",
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.",
247
319
  "Do not mark a task/phase complete unless the rebuilt state and fresh receipts support it.",
248
320
  "Follow checkpoint.nextAction/state.nextAction and re-read the referenced artifact when more detail is required.",
249
321
  "",
@@ -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
  },
@@ -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