opencode-agent-skill 15.1.0 → 15.1.2
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 +5 -5
- package/docs/V15-MANAGED-RUNTIME.md +18 -0
- package/global-config/agents/verifier.md +6 -2
- package/lib/affected-tests.mjs +28 -7
- package/lib/capability-fabric.mjs +107 -48
- package/lib/code-intelligence/index.mjs +50 -24
- package/lib/code-intelligence/lsp-pool.mjs +831 -0
- package/lib/code-intelligence/lsp-provider.mjs +197 -20
- package/lib/compaction-resume-guard.mjs +76 -4
- package/lib/completion-auditor.mjs +20 -0
- package/lib/context-manifest.mjs +88 -36
- package/lib/decision-policy.mjs +40 -2
- package/lib/evidence-store.mjs +121 -31
- package/lib/executable-probe.mjs +69 -0
- package/lib/execution-contract.mjs +2 -2
- package/lib/hierarchical-context.mjs +31 -3
- package/lib/learning-engine.mjs +164 -65
- package/lib/memory-engine.mjs +177 -95
- 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 +220 -24
- package/lib/provider-recovery.mjs +51 -0
- package/lib/repo-graph.mjs +40 -7
- package/lib/repo-inspect.mjs +3 -2
- package/lib/runtime-events.mjs +31 -10
- package/lib/semantic-index.mjs +69 -21
- package/lib/skill-compiler.mjs +84 -18
- package/lib/task-engine.mjs +28 -9
- package/lib/task-policy.mjs +80 -7
- package/lib/trajectory.mjs +25 -1
- package/lib/untrusted-output.mjs +106 -0
- package/lib/workspace-fingerprint.mjs +17 -9
- package/lib/workspace-hygiene.mjs +2 -2
- package/lib/worktree-sandbox.mjs +14 -8
- package/package.json +8 -6
- package/pi/extensions/ues-child-runtime.ts +116 -5
- package/pi/extensions/ues.ts +780 -81
- package/scripts/benchmark-lsp-pool.mjs +128 -0
- package/scripts/check-release-consistency.mjs +16 -4
- package/scripts/check-source-integrity.mjs +118 -6
- package/scripts/run-test-suite.mjs +143 -0
- package/scripts/smoke-package-closure.mjs +4 -0
|
@@ -1,8 +1,15 @@
|
|
|
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
|
|
|
10
|
+
const TYPESCRIPT_FALLBACK_CACHE_TTL_MS = 60_000
|
|
11
|
+
let typescriptFallbackCache = null
|
|
12
|
+
|
|
6
13
|
const PROVIDERS = [
|
|
7
14
|
{ id: "typescript-language-server", extensions: new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]), commands: [["typescript-language-server", ["--stdio"]]], languageId: (ext) => [".ts", ".tsx"].includes(ext) ? "typescript" : "javascript" },
|
|
8
15
|
{ id: "pyright-langserver", extensions: new Set([".py"]), commands: [["pyright-langserver", ["--stdio"]], ["pylsp", []]], languageId: () => "python" },
|
|
@@ -22,19 +29,6 @@ export const LSP_OPERATIONS = Object.freeze([
|
|
|
22
29
|
"outgoing-calls",
|
|
23
30
|
])
|
|
24
31
|
|
|
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
32
|
function inside(root, target) {
|
|
39
33
|
return target === root || target.startsWith(root + path.sep)
|
|
40
34
|
}
|
|
@@ -50,18 +44,107 @@ async function safeFile(root, relative) {
|
|
|
50
44
|
return { base, file: actual, info }
|
|
51
45
|
}
|
|
52
46
|
|
|
47
|
+
export function clearTypeScriptTsserverFallbackCache() {
|
|
48
|
+
typescriptFallbackCache = null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function deriveTypeScriptTsserverFallback(execution) {
|
|
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
|
+
export function resolveTypeScriptTsserverFallback(options = {}) {
|
|
71
|
+
const explicit = String(options.explicitPath ?? process.env.UES_TYPESCRIPT_TSSERVER_PATH ?? "").trim()
|
|
72
|
+
if (explicit && existsSync(path.resolve(explicit))) return path.resolve(explicit)
|
|
73
|
+
|
|
74
|
+
const platform = String(options.platform || process.platform)
|
|
75
|
+
if (platform !== "win32") return null
|
|
76
|
+
|
|
77
|
+
// Explicit injected executions are deterministic probes used by tests/callers;
|
|
78
|
+
// do not mix them into the host PATH cache.
|
|
79
|
+
if (options.execution) return deriveTypeScriptTsserverFallback(options.execution)
|
|
80
|
+
|
|
81
|
+
const cacheKey = [
|
|
82
|
+
explicit,
|
|
83
|
+
String(process.env.PATH || ""),
|
|
84
|
+
String(process.env.PATHEXT || ""),
|
|
85
|
+
].join("\u0000")
|
|
86
|
+
const ttlMs = Math.max(0, Math.min(
|
|
87
|
+
5 * 60_000,
|
|
88
|
+
Number(options.cacheTtlMs ?? process.env.UES_LSP_PROVIDER_PROBE_TTL_MS ?? TYPESCRIPT_FALLBACK_CACHE_TTL_MS),
|
|
89
|
+
))
|
|
90
|
+
const now = Date.now()
|
|
91
|
+
if (
|
|
92
|
+
typescriptFallbackCache &&
|
|
93
|
+
typescriptFallbackCache.key === cacheKey &&
|
|
94
|
+
now - typescriptFallbackCache.checkedAt < ttlMs
|
|
95
|
+
) {
|
|
96
|
+
return typescriptFallbackCache.value
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const resolver = typeof options.resolveCommand === "function"
|
|
100
|
+
? options.resolveCommand
|
|
101
|
+
: resolveWindowsCommand
|
|
102
|
+
const value = deriveTypeScriptTsserverFallback(resolver("tsserver"))
|
|
103
|
+
typescriptFallbackCache = { key: cacheKey, checkedAt: now, value }
|
|
104
|
+
return value
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function providerRuntimeOptions(provider) {
|
|
108
|
+
if (provider?.id !== "typescript-language-server") return {}
|
|
109
|
+
const fallbackPath = resolveTypeScriptTsserverFallback()
|
|
110
|
+
return fallbackPath
|
|
111
|
+
? { initializationOptions: { tsserver: { fallbackPath } } }
|
|
112
|
+
: {}
|
|
113
|
+
}
|
|
114
|
+
|
|
53
115
|
function providerFor(file) {
|
|
54
116
|
const ext = path.extname(file).toLowerCase()
|
|
55
117
|
for (const provider of PROVIDERS) {
|
|
56
118
|
if (!provider.extensions.has(ext)) continue
|
|
57
119
|
for (const [command, args] of provider.commands) {
|
|
58
|
-
if (commandExists(command))
|
|
120
|
+
if (commandExists(command)) {
|
|
121
|
+
return {
|
|
122
|
+
...provider,
|
|
123
|
+
...providerRuntimeOptions(provider),
|
|
124
|
+
command,
|
|
125
|
+
args,
|
|
126
|
+
languageId: provider.languageId(ext),
|
|
127
|
+
}
|
|
128
|
+
}
|
|
59
129
|
}
|
|
60
130
|
return { ...provider, command: null, args: [], languageId: provider.languageId(ext) }
|
|
61
131
|
}
|
|
62
132
|
return null
|
|
63
133
|
}
|
|
64
134
|
|
|
135
|
+
export function lspPersistencePolicy(options = {}) {
|
|
136
|
+
if (typeof options.persistent === "boolean") {
|
|
137
|
+
return { enabled: options.persistent, source: "explicit-option" }
|
|
138
|
+
}
|
|
139
|
+
const env = String(process.env.UES_LSP_PERSISTENT || "").trim()
|
|
140
|
+
if (env === "0") return { enabled: false, source: "env-disabled" }
|
|
141
|
+
if (env === "1") return { enabled: true, source: "env-enabled" }
|
|
142
|
+
if (process.env.UES_CHILD_PROCESS === "1") {
|
|
143
|
+
return { enabled: true, source: "pi-child-runtime" }
|
|
144
|
+
}
|
|
145
|
+
return { enabled: false, source: "short-lived-default" }
|
|
146
|
+
}
|
|
147
|
+
|
|
65
148
|
export function lspProviderStatus(file = "") {
|
|
66
149
|
const ext = path.extname(String(file || "")).toLowerCase()
|
|
67
150
|
const providers = PROVIDERS
|
|
@@ -75,6 +158,13 @@ export function lspProviderStatus(file = "") {
|
|
|
75
158
|
extension: ext,
|
|
76
159
|
available: providers.some((row) => row.available),
|
|
77
160
|
operations: [...LSP_OPERATIONS],
|
|
161
|
+
persistentPool: {
|
|
162
|
+
...managedLspPoolStatus({
|
|
163
|
+
includeSessions: false,
|
|
164
|
+
persistent: lspPersistencePolicy().enabled,
|
|
165
|
+
}),
|
|
166
|
+
policy: lspPersistencePolicy(),
|
|
167
|
+
},
|
|
78
168
|
providers,
|
|
79
169
|
}
|
|
80
170
|
}
|
|
@@ -199,7 +289,7 @@ function sanitizeHierarchyCalls(base, values, direction, limit = 120) {
|
|
|
199
289
|
})
|
|
200
290
|
}
|
|
201
291
|
|
|
202
|
-
async function
|
|
292
|
+
async function withEphemeralLspSession(root, relative, options, operation) {
|
|
203
293
|
const target = await safeFile(root, relative)
|
|
204
294
|
const provider = providerFor(target.file)
|
|
205
295
|
const relativeFile = path.relative(target.base, target.file).replaceAll("\\", "/")
|
|
@@ -222,8 +312,26 @@ async function withLspSession(root, relative, options, operation) {
|
|
|
222
312
|
const rootUri = pathToFileURL(target.base + path.sep).href
|
|
223
313
|
const timeoutMs = Math.max(500, Math.min(15_000, Number(options.timeoutMs || 5000)))
|
|
224
314
|
|
|
315
|
+
const execution = process.platform === "win32"
|
|
316
|
+
? resolveWindowsCommand(provider.command)
|
|
317
|
+
: { executable: provider.command, argsPrefix: [] }
|
|
318
|
+
if (!execution?.executable) {
|
|
319
|
+
return {
|
|
320
|
+
schemaVersion: 2,
|
|
321
|
+
file: relativeFile,
|
|
322
|
+
available: true,
|
|
323
|
+
provider: provider.id,
|
|
324
|
+
command: provider.command,
|
|
325
|
+
operation: options.operation || null,
|
|
326
|
+
reason: "spawn-error",
|
|
327
|
+
result: null,
|
|
328
|
+
stderr: null,
|
|
329
|
+
error: "unable to resolve LSP executable: " + provider.command,
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
225
333
|
return await new Promise((resolve) => {
|
|
226
|
-
const child = spawn(
|
|
334
|
+
const child = spawn(execution.executable, [...(execution.argsPrefix || []), ...provider.args], {
|
|
227
335
|
cwd: target.base,
|
|
228
336
|
stdio: ["pipe", "pipe", "pipe"],
|
|
229
337
|
windowsHide: true,
|
|
@@ -364,6 +472,7 @@ async function withLspSession(root, relative, options, operation) {
|
|
|
364
472
|
workspace: { workspaceFolders: true, applyEdit: false },
|
|
365
473
|
},
|
|
366
474
|
workspaceFolders: [{ uri: rootUri, name: path.basename(target.base) || "workspace" }],
|
|
475
|
+
initializationOptions: provider.initializationOptions || undefined,
|
|
367
476
|
})
|
|
368
477
|
notify("initialized", {})
|
|
369
478
|
notify("textDocument/didOpen", {
|
|
@@ -388,13 +497,35 @@ async function withLspSession(root, relative, options, operation) {
|
|
|
388
497
|
export async function lspOperation(root, relative, operation, options = {}) {
|
|
389
498
|
const op = String(operation || "")
|
|
390
499
|
if (!LSP_OPERATIONS.includes(op)) throw new Error("unsupported LSP operation: " + op)
|
|
500
|
+
|
|
501
|
+
const target = await safeFile(root, relative)
|
|
502
|
+
const provider = providerFor(target.file)
|
|
503
|
+
const relativeFile = path.relative(target.base, target.file).replaceAll("\\", "/")
|
|
504
|
+
if (!provider?.command) {
|
|
505
|
+
return {
|
|
506
|
+
schemaVersion: 2,
|
|
507
|
+
file: relativeFile,
|
|
508
|
+
available: false,
|
|
509
|
+
provider: provider?.id || null,
|
|
510
|
+
operation: op,
|
|
511
|
+
reason: provider ? "lsp-command-unavailable" : "unsupported-extension",
|
|
512
|
+
result: null,
|
|
513
|
+
persistent: false,
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
391
517
|
const maxResults = Math.max(1, Math.min(500, Number(options.maxResults || 120)))
|
|
392
|
-
|
|
518
|
+
const execute = async (session) => {
|
|
393
519
|
const pos = position(options)
|
|
394
520
|
const textDocument = { uri: session.uri }
|
|
395
521
|
|
|
396
522
|
if (op === "diagnostics") {
|
|
397
|
-
const params = await session.waitForNotification("textDocument/publishDiagnostics"
|
|
523
|
+
const params = await session.waitForNotification("textDocument/publishDiagnostics", {
|
|
524
|
+
predicate: (value) =>
|
|
525
|
+
(!value?.uri || value.uri === session.uri) &&
|
|
526
|
+
(value?.version == null || Number(value.version) >= Number(session.documentSync?.version || 0)),
|
|
527
|
+
afterAt: session.documentSync?.changed ? session.documentSync.syncedAt : 0,
|
|
528
|
+
})
|
|
398
529
|
const values = Array.isArray(params?.diagnostics) ? params.diagnostics : []
|
|
399
530
|
return { diagnostics: values.slice(0, maxResults).map(sanitizeDiagnostic) }
|
|
400
531
|
}
|
|
@@ -450,7 +581,53 @@ export async function lspOperation(root, relative, operation, options = {}) {
|
|
|
450
581
|
}
|
|
451
582
|
}
|
|
452
583
|
return null
|
|
453
|
-
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const persistence = lspPersistencePolicy(options)
|
|
587
|
+
const managed = await withManagedLspSession(
|
|
588
|
+
target,
|
|
589
|
+
provider,
|
|
590
|
+
{ ...options, persistent: persistence.enabled, operation: op },
|
|
591
|
+
execute,
|
|
592
|
+
).catch((error) => ({
|
|
593
|
+
ok: false,
|
|
594
|
+
reason: "managed-lsp-exception",
|
|
595
|
+
error: String(error instanceof Error ? error.message : error).slice(0, 1200),
|
|
596
|
+
}))
|
|
597
|
+
|
|
598
|
+
if (managed?.ok) {
|
|
599
|
+
return {
|
|
600
|
+
schemaVersion: 2,
|
|
601
|
+
file: relativeFile,
|
|
602
|
+
available: true,
|
|
603
|
+
provider: provider.id,
|
|
604
|
+
command: provider.command,
|
|
605
|
+
operation: op,
|
|
606
|
+
reason: "ok",
|
|
607
|
+
result: managed.result,
|
|
608
|
+
stderr: managed.stderr || null,
|
|
609
|
+
error: null,
|
|
610
|
+
persistent: true,
|
|
611
|
+
pool: { ...managed.meta, policy: persistence },
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const fallback = await withEphemeralLspSession(root, relative, { ...options, operation: op }, execute)
|
|
616
|
+
return {
|
|
617
|
+
...fallback,
|
|
618
|
+
persistent: false,
|
|
619
|
+
fallbackFrom: managed?.reason || "managed-lsp-unavailable",
|
|
620
|
+
managedError: managed?.error || null,
|
|
621
|
+
persistencePolicy: persistence,
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export function lspPoolStatus(options = {}) {
|
|
626
|
+
return managedLspPoolStatus(options)
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export async function shutdownLspPool(root = null) {
|
|
630
|
+
return shutdownManagedLspPool(root)
|
|
454
631
|
}
|
|
455
632
|
|
|
456
633
|
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,8 @@ 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, workspaceStatusEntries } from "./workspace-fingerprint.mjs"
|
|
11
|
+
import { sourceFacingPaths, sourceGitPathspecs } from "./runtime-artifacts.mjs"
|
|
10
12
|
|
|
11
13
|
const ROOT_INSTRUCTIONS = [
|
|
12
14
|
"AGENTS.md",
|
|
@@ -28,6 +30,14 @@ function unique(values) {
|
|
|
28
30
|
return [...new Set(values.filter(Boolean))]
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
function boundedFailure(component, error, impact = "evidence") {
|
|
34
|
+
return {
|
|
35
|
+
component,
|
|
36
|
+
impact,
|
|
37
|
+
message: String(error instanceof Error ? error.message : error || "unknown failure").slice(0, 500),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
function safeFull(root, relative) {
|
|
32
42
|
const base = path.resolve(root)
|
|
33
43
|
const full = path.resolve(base, relative)
|
|
@@ -148,18 +158,22 @@ function symbolHits(source, terms, limit = 12) {
|
|
|
148
158
|
function gitChangedFiles(root) {
|
|
149
159
|
const result = spawnSync(
|
|
150
160
|
"git",
|
|
151
|
-
[
|
|
161
|
+
[
|
|
162
|
+
"status",
|
|
163
|
+
"--porcelain=v1",
|
|
164
|
+
"-z",
|
|
165
|
+
"--untracked-files=all",
|
|
166
|
+
"--",
|
|
167
|
+
...sourceGitPathspecs(),
|
|
168
|
+
],
|
|
152
169
|
{ cwd: root, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 },
|
|
153
170
|
)
|
|
154
171
|
if (result.status !== 0) return []
|
|
155
|
-
return unique(
|
|
156
|
-
result.stdout
|
|
157
|
-
.
|
|
158
|
-
.
|
|
159
|
-
|
|
160
|
-
.map((value) => value.includes(" -> ") ? value.split(" -> ").at(-1) : value)
|
|
161
|
-
.map((value) => value.replaceAll("\\", "/")),
|
|
162
|
-
)
|
|
172
|
+
return unique(sourceFacingPaths(
|
|
173
|
+
workspaceStatusEntries(result.stdout)
|
|
174
|
+
.filter((entry) => entry.renameSource !== true)
|
|
175
|
+
.map((entry) => entry.file),
|
|
176
|
+
))
|
|
163
177
|
}
|
|
164
178
|
|
|
165
179
|
async function nearbyInstructions(root, declared) {
|
|
@@ -249,9 +263,20 @@ export async function buildContextManifest(root, task, options = {}) {
|
|
|
249
263
|
const evidenceBudget = options.evidenceBudget || null
|
|
250
264
|
const declared = taskFiles(task)
|
|
251
265
|
const terms = taskTerms(task)
|
|
266
|
+
let workspaceSnapshot = null
|
|
267
|
+
const suppliedFingerprint = String(options.workspaceFingerprint || "").trim()
|
|
268
|
+
if (!Array.isArray(options.changedFiles) || !suppliedFingerprint) {
|
|
269
|
+
try {
|
|
270
|
+
workspaceSnapshot = runtimeWorkspaceSnapshot(root, { workspaceState: options.workspaceState })
|
|
271
|
+
} catch {}
|
|
272
|
+
}
|
|
273
|
+
const reusableWorkspaceFingerprint = suppliedFingerprint ||
|
|
274
|
+
(workspaceSnapshot?.cacheable ? String(workspaceSnapshot.fingerprint || "") : "")
|
|
252
275
|
const changed = Array.isArray(options.changedFiles)
|
|
253
276
|
? options.changedFiles.map((file) => String(file || "").replaceAll("\\", "/")).filter(Boolean)
|
|
254
|
-
:
|
|
277
|
+
: Array.isArray(workspaceSnapshot?.changedFiles)
|
|
278
|
+
? workspaceSnapshot.changedFiles
|
|
279
|
+
: gitChangedFiles(root)
|
|
255
280
|
const strategy = String(options.strategy || "semantic+graph+git")
|
|
256
281
|
const queryText = [
|
|
257
282
|
task?.title,
|
|
@@ -260,25 +285,51 @@ export async function buildContextManifest(root, task, options = {}) {
|
|
|
260
285
|
...(task?.verification || []),
|
|
261
286
|
].filter(Boolean).join(" ")
|
|
262
287
|
|
|
288
|
+
const contextDiagnostics = []
|
|
263
289
|
const semanticSnapshot = await buildSemanticIndexCached(root, {
|
|
264
290
|
maxFiles: options.semanticMaxFiles ?? 6000,
|
|
265
291
|
maxDepth: options.semanticMaxDepth ?? 14,
|
|
266
|
-
workspaceFingerprint:
|
|
267
|
-
}).catch(() =>
|
|
292
|
+
workspaceFingerprint: reusableWorkspaceFingerprint,
|
|
293
|
+
}).catch((error) => {
|
|
294
|
+
contextDiagnostics.push(boundedFailure("semantic-snapshot", error, "performance"))
|
|
295
|
+
return null
|
|
296
|
+
})
|
|
268
297
|
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
298
|
+
const useGraph = strategy.includes("graph")
|
|
299
|
+
const [hierarchy, semantic, graph, instructions] = await Promise.all([
|
|
300
|
+
queryContextHierarchy(root, queryText, {
|
|
301
|
+
declaredFiles: declared,
|
|
302
|
+
maxScopes: options.hierarchyMaxScopes ?? 6,
|
|
303
|
+
maxFiles: options.semanticMaxFiles ?? 6000,
|
|
304
|
+
builtIndex: semanticSnapshot || undefined,
|
|
305
|
+
}).catch((error) => {
|
|
306
|
+
contextDiagnostics.push(boundedFailure("hierarchy", error))
|
|
307
|
+
return null
|
|
308
|
+
}),
|
|
309
|
+
querySemanticIndex(root, queryText, {
|
|
310
|
+
limit: 32,
|
|
311
|
+
maxFiles: options.semanticMaxFiles ?? 6000,
|
|
312
|
+
builtIndex: semanticSnapshot || undefined,
|
|
313
|
+
}).catch((error) => {
|
|
314
|
+
contextDiagnostics.push(boundedFailure("semantic-query", error))
|
|
315
|
+
return null
|
|
316
|
+
}),
|
|
317
|
+
useGraph
|
|
318
|
+
? buildRepoGraphCached(root, {
|
|
319
|
+
maxFiles: options.maxFiles ?? 2500,
|
|
320
|
+
workspaceFingerprint: reusableWorkspaceFingerprint,
|
|
321
|
+
}).catch((error) => {
|
|
322
|
+
contextDiagnostics.push(boundedFailure("repo-graph", error))
|
|
323
|
+
return { nodes: [], edges: [], hotspots: [], scannedFiles: 0, truncated: false }
|
|
324
|
+
})
|
|
325
|
+
: Promise.resolve({ nodes: [], edges: [], hotspots: [], scannedFiles: 0, truncated: false }),
|
|
326
|
+
nearbyInstructions(root, declared).catch((error) => {
|
|
327
|
+
contextDiagnostics.push(boundedFailure("instructions", error))
|
|
328
|
+
return []
|
|
329
|
+
}),
|
|
330
|
+
])
|
|
275
331
|
const hierarchyScopes = hierarchy?.scopes?.map((item) => item.path) || []
|
|
276
332
|
|
|
277
|
-
const semantic = await querySemanticIndex(root, queryText, {
|
|
278
|
-
limit: 32,
|
|
279
|
-
maxFiles: options.semanticMaxFiles ?? 6000,
|
|
280
|
-
builtIndex: semanticSnapshot || undefined,
|
|
281
|
-
}).catch(() => null)
|
|
282
333
|
const scopedSemanticResults = (semantic?.results || []).map((item) => {
|
|
283
334
|
const scopeRank = hierarchyScopes.findIndex((scope) =>
|
|
284
335
|
scope === "." || item.path === scope || item.path.startsWith(scope + "/"),
|
|
@@ -293,13 +344,6 @@ export async function buildContextManifest(root, task, options = {}) {
|
|
|
293
344
|
}).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
|
|
294
345
|
const semanticPaths = scopedSemanticResults.map((item) => item.path)
|
|
295
346
|
|
|
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
347
|
const nodeMap = new Map(graph.nodes.map((node) => [node.path, node]))
|
|
304
348
|
const incoming = new Map()
|
|
305
349
|
|
|
@@ -330,7 +374,6 @@ export async function buildContextManifest(root, task, options = {}) {
|
|
|
330
374
|
.slice(0, 24)
|
|
331
375
|
.map((item) => item.file)
|
|
332
376
|
|
|
333
|
-
const instructions = await nearbyInstructions(root, declared)
|
|
334
377
|
const graphRanked = useGraph
|
|
335
378
|
? rankContextGraph(graph, {
|
|
336
379
|
semanticResults: scopedSemanticResults,
|
|
@@ -388,11 +431,8 @@ export async function buildContextManifest(root, task, options = {}) {
|
|
|
388
431
|
...instructions,
|
|
389
432
|
])
|
|
390
433
|
|
|
391
|
-
const files =
|
|
392
|
-
|
|
393
|
-
const info = await fileInfo(root, file)
|
|
394
|
-
if (info) files.push(info)
|
|
395
|
-
}
|
|
434
|
+
const files = (await Promise.all(priority.map((file) => fileInfo(root, file))))
|
|
435
|
+
.filter(Boolean)
|
|
396
436
|
|
|
397
437
|
let remaining = budget
|
|
398
438
|
const categoryRemaining = {
|
|
@@ -422,9 +462,18 @@ export async function buildContextManifest(root, task, options = {}) {
|
|
|
422
462
|
})
|
|
423
463
|
}
|
|
424
464
|
|
|
465
|
+
contextDiagnostics.sort((a, b) =>
|
|
466
|
+
a.component.localeCompare(b.component) ||
|
|
467
|
+
a.impact.localeCompare(b.impact) ||
|
|
468
|
+
a.message.localeCompare(b.message)
|
|
469
|
+
)
|
|
470
|
+
const evidenceDegraded = contextDiagnostics.some((item) => item.impact === "evidence")
|
|
471
|
+
|
|
425
472
|
return {
|
|
426
473
|
schemaVersion: 4,
|
|
427
474
|
task: task?.id || null,
|
|
475
|
+
contextQuality: evidenceDegraded ? "degraded" : "full",
|
|
476
|
+
degradedComponents: contextDiagnostics,
|
|
428
477
|
strategy,
|
|
429
478
|
queryTerms: terms,
|
|
430
479
|
declared,
|
|
@@ -437,6 +486,7 @@ export async function buildContextManifest(root, task, options = {}) {
|
|
|
437
486
|
excerpts,
|
|
438
487
|
hierarchy: hierarchy ? {
|
|
439
488
|
schemaVersion: hierarchy.schemaVersion,
|
|
489
|
+
runtimeCacheHit: hierarchy.runtimeCacheHit === true,
|
|
440
490
|
levels: hierarchy.levels,
|
|
441
491
|
scopes: hierarchy.scopes.slice(0, 6).map((item) => ({
|
|
442
492
|
path: item.path,
|
|
@@ -450,6 +500,8 @@ export async function buildContextManifest(root, task, options = {}) {
|
|
|
450
500
|
semantic: semantic ? {
|
|
451
501
|
evidenceLevel: semantic.evidenceLevel,
|
|
452
502
|
resultCount: semantic.results.length,
|
|
503
|
+
runtimeCacheHit: semanticSnapshot?.runtimeCacheHit === true,
|
|
504
|
+
runtimeCacheCoalesced: semanticSnapshot?.runtimeCacheCoalesced === true,
|
|
453
505
|
stats: semantic.stats,
|
|
454
506
|
} : null,
|
|
455
507
|
graph: {
|