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.
- package/README.md +3 -3
- package/docs/V15-MANAGED-RUNTIME.md +18 -0
- package/global-config/agents/verifier.md +6 -2
- package/lib/affected-tests.mjs +21 -4
- package/lib/capability-fabric.mjs +107 -48
- package/lib/code-intelligence/index.mjs +3 -15
- package/lib/code-intelligence/lsp-pool.mjs +694 -0
- package/lib/code-intelligence/lsp-provider.mjs +160 -20
- package/lib/compaction-resume-guard.mjs +76 -4
- package/lib/completion-auditor.mjs +20 -0
- package/lib/context-manifest.mjs +38 -21
- package/lib/decision-policy.mjs +40 -2
- package/lib/evidence-store.mjs +121 -31
- package/lib/executable-probe.mjs +69 -0
- package/lib/hierarchical-context.mjs +31 -3
- package/lib/learning-engine.mjs +164 -65
- package/lib/memory-engine.mjs +163 -86
- package/lib/model-config.mjs +86 -7
- package/lib/performance-fabric.mjs +92 -0
- package/lib/permission-policy.mjs +232 -0
- package/lib/pi-rpc-pool.mjs +170 -5
- package/lib/provider-recovery.mjs +51 -0
- package/lib/repo-graph.mjs +38 -5
- package/lib/runtime-events.mjs +31 -10
- package/lib/semantic-index.mjs +67 -20
- package/lib/skill-compiler.mjs +84 -18
- package/lib/task-policy.mjs +80 -7
- package/lib/trajectory.mjs +25 -1
- package/lib/untrusted-output.mjs +106 -0
- package/package.json +8 -6
- package/pi/extensions/ues-child-runtime.ts +78 -3
- package/pi/extensions/ues.ts +695 -81
- package/scripts/benchmark-lsp-pool.mjs +128 -0
- package/scripts/check-release-consistency.mjs +16 -4
- package/scripts/check-source-integrity.mjs +60 -1
- package/scripts/run-test-suite.mjs +143 -0
- package/scripts/smoke-package-closure.mjs +4 -0
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { spawn
|
|
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))
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
},
|
package/lib/context-manifest.mjs
CHANGED
|
@@ -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
|
-
:
|
|
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:
|
|
278
|
+
workspaceFingerprint: reusableWorkspaceFingerprint,
|
|
267
279
|
}).catch(() => null)
|
|
268
280
|
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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: {
|
package/lib/decision-policy.mjs
CHANGED
|
@@ -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,
|
|
16
|
-
|
|
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
|
|