opencode-agent-skill 7.7.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -3
- package/README.md +675 -581
- package/bin/ocskill.mjs +358 -156
- package/docs/DETERMINISTIC-TOOLS.md +25 -8
- package/docs/ENGINEERING-DESIGN.md +31 -13
- package/docs/EVALS.md +34 -12
- package/docs/NPM-PUBLISH.md +6 -6
- package/docs/OPENCODE-COMPAT.md +11 -8
- package/docs/TRACE-SCHEMA.md +15 -2
- package/docs/V8-INTELLIGENCE-RELIABILITY.md +206 -0
- package/docs/V9-SPEED-INTELLIGENCE.md +102 -0
- package/evals/live/tasks.json +6 -6
- package/evals/polyglot/fixtures/polyglot-bench/api/generated/client.ts +2 -0
- package/evals/polyglot/fixtures/polyglot-bench/api/openapi.json +25 -0
- package/evals/polyglot/fixtures/polyglot-bench/db/migrations/20260920_add_order_key.sql +1 -0
- package/evals/polyglot/fixtures/polyglot-bench/dotnet/OrderService.cs +8 -0
- package/evals/polyglot/fixtures/polyglot-bench/java/PriceService.java +5 -0
- package/evals/polyglot/fixtures/polyglot-bench/monorepo/package.json +6 -0
- package/evals/polyglot/fixtures/polyglot-bench/monorepo/packages/api/package.json +4 -0
- package/evals/polyglot/fixtures/polyglot-bench/monorepo/packages/web/package.json +7 -0
- package/evals/polyglot/fixtures/polyglot-bench/monorepo/pnpm-lock.yaml +5 -0
- package/evals/polyglot/fixtures/polyglot-bench/next/app/api/products/route.ts +7 -0
- package/evals/polyglot/fixtures/polyglot-bench/python/tenant_auth.py +4 -0
- package/evals/polyglot/fixtures/polyglot-bench/react-native/keyboard.ts +3 -0
- package/evals/polyglot/graders/polyglot-bench.mjs +101 -0
- package/evals/polyglot/tasks.json +54 -0
- package/global-config/AGENTS.md +10 -7
- package/global-config/agents/integration-verifier.md +1 -1
- package/global-config/agents/plan-checker.md +1 -1
- package/global-config/commands/run.md +9 -5
- package/global-config/plugins/ues-router/capabilities.js +4 -0
- package/global-config/plugins/ues-router/index.js +414 -21
- package/global-config/plugins/ues-router/router.js +140 -23
- package/global-config/skills/engineering-orchestrator/references/long-horizon.md +6 -4
- package/lib/aci.mjs +128 -0
- package/lib/benchmark-confidence.mjs +135 -0
- package/lib/cli-utils.mjs +41 -0
- package/lib/container-sandbox.mjs +102 -0
- package/lib/context-manifest.mjs +300 -22
- package/lib/control-center.mjs +36 -3
- package/lib/eval-order.mjs +9 -0
- package/lib/eval-telemetry.mjs +5 -2
- package/lib/gate-receipt.mjs +52 -0
- package/lib/installer.mjs +39 -25
- package/lib/learning-engine.mjs +236 -38
- package/lib/opencode-compat.mjs +25 -10
- package/lib/orchestrator-policy.mjs +101 -20
- package/lib/process-runner.mjs +30 -9
- package/lib/runtime-events.mjs +31 -0
- package/lib/semantic-index.mjs +318 -0
- package/lib/task-engine.mjs +416 -24
- package/lib/trajectory.mjs +89 -0
- package/lib/windows-shim.mjs +227 -0
- package/lib/worktree-sandbox.mjs +85 -3
- package/package.json +10 -4
- package/scripts/check-release-tag.mjs +22 -0
- package/scripts/control-center.mjs +25 -0
- package/scripts/eval-live.mjs +39 -58
- package/scripts/eval-matrix.mjs +155 -0
- package/scripts/smoke-packed-install.mjs +138 -4
- package/scripts/smoke-plain-install.mjs +91 -0
- package/scripts/validate-live-suite.mjs +3 -3
- package/scripts/validate.mjs +27 -5
package/lib/task-engine.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync } from "node:fs"
|
|
1
|
+
import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync } from "node:fs"
|
|
2
2
|
import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises"
|
|
3
3
|
import { spawnSync } from "node:child_process"
|
|
4
4
|
import { createHash, randomUUID } from "node:crypto"
|
|
@@ -7,11 +7,15 @@ import { analyzePlan, taskFiles, validatePlan } from "./task-graph.mjs"
|
|
|
7
7
|
import { buildContextManifest } from "./context-manifest.mjs"
|
|
8
8
|
import { relevantAcceptedLearnings } from "./learning-engine.mjs"
|
|
9
9
|
import { validateVerificationReceipt } from "./evidence-receipt.mjs"
|
|
10
|
+
import { appendRuntimeEvent, readRuntimeEvents } from "./runtime-events.mjs"
|
|
11
|
+
import { createGateReceipt, validateGateReceipt } from "./gate-receipt.mjs"
|
|
12
|
+
import { classifyEngineeringTask } from "./orchestrator-policy.mjs"
|
|
10
13
|
|
|
11
14
|
const WORK_DIR = ".ues-work"
|
|
12
15
|
const STATE_SCHEMA = 3
|
|
13
16
|
const LOCK_TIMEOUT_MS = 30_000
|
|
14
17
|
const LOCK_STALE_MS = 120_000
|
|
18
|
+
const LOCK_HEARTBEAT_MS = 15_000
|
|
15
19
|
const DEFAULT_TASK_LEASE_MS = 10 * 60_000
|
|
16
20
|
|
|
17
21
|
function now() {
|
|
@@ -37,6 +41,7 @@ export function workPaths(root, slug) {
|
|
|
37
41
|
plan: path.join(dir, "PLAN.json"),
|
|
38
42
|
state: path.join(dir, "STATE.json"),
|
|
39
43
|
evidence: path.join(dir, "EVIDENCE.json"),
|
|
44
|
+
events: path.join(dir, "EVENTS.jsonl"),
|
|
40
45
|
tasks: path.join(dir, "tasks"),
|
|
41
46
|
reports: path.join(dir, "reports"),
|
|
42
47
|
lock: path.join(dir, ".state-lock"),
|
|
@@ -67,23 +72,43 @@ async function writeJson(file, value) {
|
|
|
67
72
|
await atomicWrite(file, JSON.stringify(value, null, 2) + "\n")
|
|
68
73
|
}
|
|
69
74
|
|
|
75
|
+
async function journal(paths, type, data = {}) {
|
|
76
|
+
return appendRuntimeEvent(paths.events, type, {
|
|
77
|
+
slug: path.basename(paths.dir),
|
|
78
|
+
...data,
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function lockOwner(paths) {
|
|
83
|
+
return readJson(path.join(paths.lock, "owner.json"), null).catch(() => null)
|
|
84
|
+
}
|
|
85
|
+
|
|
70
86
|
async function acquireWorkLock(paths) {
|
|
71
87
|
const started = Date.now()
|
|
72
88
|
while (true) {
|
|
89
|
+
const token = randomUUID()
|
|
73
90
|
try {
|
|
74
91
|
await mkdir(paths.lock)
|
|
92
|
+
const owner = { pid: process.pid, token, at: now(), heartbeatAt: now() }
|
|
75
93
|
await writeFile(
|
|
76
94
|
path.join(paths.lock, "owner.json"),
|
|
77
|
-
JSON.stringify(
|
|
95
|
+
JSON.stringify(owner) + "\n",
|
|
78
96
|
"utf8",
|
|
79
|
-
)
|
|
80
|
-
return
|
|
97
|
+
)
|
|
98
|
+
return owner
|
|
81
99
|
} catch (error) {
|
|
82
100
|
if (error?.code !== "EEXIST") throw error
|
|
83
|
-
const
|
|
101
|
+
const ownerFile = path.join(paths.lock, "owner.json")
|
|
102
|
+
const info = await stat(ownerFile).catch(() => stat(paths.lock).catch(() => null))
|
|
84
103
|
if (info && Date.now() - info.mtimeMs > LOCK_STALE_MS) {
|
|
85
|
-
|
|
86
|
-
|
|
104
|
+
const stale = paths.lock + ".stale-" + randomUUID()
|
|
105
|
+
try {
|
|
106
|
+
await rename(paths.lock, stale)
|
|
107
|
+
await rm(stale, { recursive: true, force: true }).catch(() => {})
|
|
108
|
+
continue
|
|
109
|
+
} catch {
|
|
110
|
+
// Another contender refreshed or took over the lock first.
|
|
111
|
+
}
|
|
87
112
|
}
|
|
88
113
|
if (Date.now() - started >= LOCK_TIMEOUT_MS) {
|
|
89
114
|
throw new Error(`timed out waiting for UES state lock: ${paths.lock}`)
|
|
@@ -93,13 +118,36 @@ async function acquireWorkLock(paths) {
|
|
|
93
118
|
}
|
|
94
119
|
}
|
|
95
120
|
|
|
121
|
+
async function heartbeatWorkLock(paths, token) {
|
|
122
|
+
const owner = await lockOwner(paths)
|
|
123
|
+
if (!owner || owner.token !== token) return false
|
|
124
|
+
await writeFile(
|
|
125
|
+
path.join(paths.lock, "owner.json"),
|
|
126
|
+
JSON.stringify({ ...owner, heartbeatAt: now() }) + "\n",
|
|
127
|
+
"utf8",
|
|
128
|
+
)
|
|
129
|
+
return true
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function releaseWorkLock(paths, token) {
|
|
133
|
+
const owner = await lockOwner(paths)
|
|
134
|
+
if (!owner || owner.token !== token) return false
|
|
135
|
+
await rm(paths.lock, { recursive: true, force: true })
|
|
136
|
+
return true
|
|
137
|
+
}
|
|
138
|
+
|
|
96
139
|
async function withWorkLock(root, slug, fn) {
|
|
97
140
|
const paths = workPaths(root, slug)
|
|
98
|
-
await acquireWorkLock(paths)
|
|
141
|
+
const owner = await acquireWorkLock(paths)
|
|
142
|
+
const heartbeat = setInterval(() => {
|
|
143
|
+
heartbeatWorkLock(paths, owner.token).catch(() => {})
|
|
144
|
+
}, LOCK_HEARTBEAT_MS)
|
|
145
|
+
heartbeat.unref?.()
|
|
99
146
|
try {
|
|
100
147
|
return await fn(paths)
|
|
101
148
|
} finally {
|
|
102
|
-
|
|
149
|
+
clearInterval(heartbeat)
|
|
150
|
+
await releaseWorkLock(paths, owner.token).catch(() => {})
|
|
103
151
|
}
|
|
104
152
|
}
|
|
105
153
|
|
|
@@ -115,19 +163,92 @@ function gitCapture(root, args) {
|
|
|
115
163
|
})
|
|
116
164
|
}
|
|
117
165
|
|
|
166
|
+
const FINGERPRINT_SKIP_DIRS = new Set([
|
|
167
|
+
".git", ".ues-work", ".ues-learning", ".ues-dashboard", ".ues-sandboxes", ".ues-cache", ".ues-traces",
|
|
168
|
+
"node_modules", ".next", "dist", "build", "coverage", ".venv", "venv",
|
|
169
|
+
"Pods", "DerivedData", ".gradle", ".cache", ".turbo", "target", "bin", "obj",
|
|
170
|
+
])
|
|
171
|
+
|
|
172
|
+
function nonGitWorkspaceFingerprint(root, options = {}) {
|
|
173
|
+
const maxFiles = Math.max(100, Number(options.maxFiles || 5000))
|
|
174
|
+
const maxFileBytes = Math.max(64 * 1024, Number(options.maxFileBytes || 2 * 1024 * 1024))
|
|
175
|
+
const files = []
|
|
176
|
+
|
|
177
|
+
function visit(dir) {
|
|
178
|
+
if (files.length >= maxFiles) return
|
|
179
|
+
let entries = []
|
|
180
|
+
try {
|
|
181
|
+
entries = readdirSync(dir, { withFileTypes: true })
|
|
182
|
+
} catch {
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
entries.sort((a, b) => a.name.localeCompare(b.name))
|
|
186
|
+
for (const entry of entries) {
|
|
187
|
+
if (files.length >= maxFiles) break
|
|
188
|
+
if (entry.isDirectory() && FINGERPRINT_SKIP_DIRS.has(entry.name)) continue
|
|
189
|
+
const full = path.join(dir, entry.name)
|
|
190
|
+
if (entry.isDirectory()) {
|
|
191
|
+
visit(full)
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
if (!entry.isFile()) continue
|
|
195
|
+
const relative = path.relative(root, full).replaceAll("\\", "/")
|
|
196
|
+
let info
|
|
197
|
+
try { info = statSync(full) } catch { continue }
|
|
198
|
+
files.push({ full, relative, size: info.size, mtimeMs: info.mtimeMs })
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
visit(root)
|
|
203
|
+
const hash = createHash("sha256")
|
|
204
|
+
hash.update("non-git-workspace-v2\n")
|
|
205
|
+
hash.update("truncated=" + String(files.length >= maxFiles) + "\n")
|
|
206
|
+
for (const file of files) {
|
|
207
|
+
hash.update(file.relative + "\0" + file.size + "\0")
|
|
208
|
+
if (file.size <= maxFileBytes) {
|
|
209
|
+
try {
|
|
210
|
+
hash.update(readFileSync(file.full))
|
|
211
|
+
} catch {
|
|
212
|
+
hash.update("READ_ERROR:" + file.mtimeMs)
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
let fd = null
|
|
216
|
+
try {
|
|
217
|
+
fd = openSync(file.full, "r")
|
|
218
|
+
const sampleBytes = Math.min(64 * 1024, file.size)
|
|
219
|
+
const first = Buffer.allocUnsafe(sampleBytes)
|
|
220
|
+
const last = Buffer.allocUnsafe(sampleBytes)
|
|
221
|
+
readSync(fd, first, 0, sampleBytes, 0)
|
|
222
|
+
readSync(fd, last, 0, sampleBytes, Math.max(0, file.size - sampleBytes))
|
|
223
|
+
hash.update("LARGE_SAMPLE:")
|
|
224
|
+
hash.update(first)
|
|
225
|
+
hash.update(last)
|
|
226
|
+
} catch {
|
|
227
|
+
hash.update("READ_ERROR:" + file.mtimeMs)
|
|
228
|
+
} finally {
|
|
229
|
+
if (fd !== null) {
|
|
230
|
+
try { closeSync(fd) } catch {}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
hash.update("\n")
|
|
235
|
+
}
|
|
236
|
+
return hash.digest("hex")
|
|
237
|
+
}
|
|
238
|
+
|
|
118
239
|
export function workspaceFingerprint(root) {
|
|
119
240
|
root = path.resolve(root)
|
|
120
241
|
const inside = gitCapture(root, ["rev-parse", "--is-inside-work-tree"])
|
|
121
242
|
if (inside.status !== 0) {
|
|
122
|
-
return
|
|
243
|
+
return nonGitWorkspaceFingerprint(root)
|
|
123
244
|
}
|
|
124
245
|
|
|
125
246
|
const parts = []
|
|
126
247
|
const commands = [
|
|
127
248
|
["rev-parse", "HEAD"],
|
|
128
|
-
["status", "--porcelain=v1", "--untracked-files=all", "--", ".", ":(exclude).ues-work", ":(exclude).ues-learning", ":(exclude).ues-dashboard", ":(exclude).ues-sandboxes"],
|
|
129
|
-
["diff", "--binary", "--no-ext-diff", "--", ".", ":(exclude).ues-work", ":(exclude).ues-learning", ":(exclude).ues-dashboard", ":(exclude).ues-sandboxes"],
|
|
130
|
-
["diff", "--cached", "--binary", "--no-ext-diff", "--", ".", ":(exclude).ues-work", ":(exclude).ues-learning", ":(exclude).ues-dashboard", ":(exclude).ues-sandboxes"],
|
|
249
|
+
["status", "--porcelain=v1", "--untracked-files=all", "--", ".", ":(exclude).ues-work", ":(exclude).ues-learning", ":(exclude).ues-dashboard", ":(exclude).ues-sandboxes", ":(exclude).ues-cache", ":(exclude).ues-traces"],
|
|
250
|
+
["diff", "--binary", "--no-ext-diff", "--", ".", ":(exclude).ues-work", ":(exclude).ues-learning", ":(exclude).ues-dashboard", ":(exclude).ues-sandboxes", ":(exclude).ues-cache", ":(exclude).ues-traces"],
|
|
251
|
+
["diff", "--cached", "--binary", "--no-ext-diff", "--", ".", ":(exclude).ues-work", ":(exclude).ues-learning", ":(exclude).ues-dashboard", ":(exclude).ues-sandboxes", ":(exclude).ues-cache", ":(exclude).ues-traces"],
|
|
131
252
|
]
|
|
132
253
|
for (const args of commands) {
|
|
133
254
|
const result = gitCapture(root, args)
|
|
@@ -171,7 +292,9 @@ export async function initWork(root, slug, goal) {
|
|
|
171
292
|
updatedAt: createdAt,
|
|
172
293
|
entries: [],
|
|
173
294
|
receipts: [],
|
|
295
|
+
gateReceipts: [],
|
|
174
296
|
})
|
|
297
|
+
await journal(paths, "work.init", { goal: cleanGoal })
|
|
175
298
|
|
|
176
299
|
return { ...paths, slug, goal: cleanGoal }
|
|
177
300
|
}
|
|
@@ -185,6 +308,24 @@ export async function loadWork(root, slug) {
|
|
|
185
308
|
return { paths, state, plan, evidence }
|
|
186
309
|
}
|
|
187
310
|
|
|
311
|
+
function evidencePolicyForPlan(plan) {
|
|
312
|
+
const tasks = Array.isArray(plan?.tasks) ? plan.tasks : []
|
|
313
|
+
const highRisk = tasks.some((task) => ["high", "critical"].includes(task?.risk))
|
|
314
|
+
const longHorizon = tasks.length > 1 || plan?.mode === "long-horizon" || plan?.longHorizon === true
|
|
315
|
+
return {
|
|
316
|
+
schemaVersion: 1,
|
|
317
|
+
strict: highRisk || longHorizon,
|
|
318
|
+
receiptRequiredForTasks: highRisk || longHorizon,
|
|
319
|
+
structuredPlanApproval: highRisk || longHorizon,
|
|
320
|
+
structuredIntegrationApproval: highRisk || longHorizon,
|
|
321
|
+
reason: highRisk ? "high-risk-plan" : longHorizon ? "long-horizon-plan" : "standard-plan",
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function currentEvidencePolicy(state, plan) {
|
|
326
|
+
return state?.evidencePolicy || evidencePolicyForPlan(plan)
|
|
327
|
+
}
|
|
328
|
+
|
|
188
329
|
function taskBrief(task) {
|
|
189
330
|
const files = taskFiles(task)
|
|
190
331
|
return `# ${task.id}: ${task.title}\n\n## Summary\n\n${task.summary || "No summary provided."}\n\n## Dependencies\n\n${(task.dependsOn || []).map((id) => `- ${id}`).join("\n") || "- None"}\n\n## Files\n\n${files.map((file) => `- ${file}`).join("\n") || "- Scope must be established before editing."}\n\n## Acceptance criteria\n\n${(task.acceptance || []).map((item) => `- ${item}`).join("\n")}\n\n## Verification\n\n${(task.verification || []).map((item) => `- ${item}`).join("\n")}\n\n## Risk\n\n${task.risk || "medium"}\n`
|
|
@@ -246,17 +387,21 @@ export async function importPlan(root, slug, planInput) {
|
|
|
246
387
|
planHash: hash,
|
|
247
388
|
planApproval: { status: "pending", planHash: hash, at: updatedAt, evidence: null },
|
|
248
389
|
integrationVerification: null,
|
|
390
|
+
evidencePolicy: evidencePolicyForPlan(plan),
|
|
249
391
|
tasks: stateTasks,
|
|
250
392
|
nextAction: "Run ues-plan-checker, then record PASS with 'ocskill work approve-plan'.",
|
|
251
393
|
}
|
|
252
394
|
await writeJson(loaded.paths.state, state)
|
|
395
|
+
await journal(loaded.paths, "plan.imported", {
|
|
396
|
+
planHash: hash,
|
|
397
|
+
taskCount: plan.tasks.length,
|
|
398
|
+
})
|
|
253
399
|
return { plan, analysis, state, paths: loaded.paths }
|
|
254
400
|
})
|
|
255
401
|
}
|
|
256
402
|
|
|
257
|
-
export async function approvePlan(root, slug, evidence) {
|
|
258
|
-
const evidenceText = String(evidence || "").trim()
|
|
259
|
-
if (!evidenceText) throw new Error("plan approval requires non-empty plan-checker evidence")
|
|
403
|
+
export async function approvePlan(root, slug, evidence, options = {}) {
|
|
404
|
+
const evidenceText = String(evidence || options.receipt?.evidence || "").trim()
|
|
260
405
|
|
|
261
406
|
return withWorkLock(root, slug, async () => {
|
|
262
407
|
const loaded = await loadWork(root, slug)
|
|
@@ -266,14 +411,43 @@ export async function approvePlan(root, slug, evidence) {
|
|
|
266
411
|
throw new Error("PLAN.json changed after import; re-import it before approval")
|
|
267
412
|
}
|
|
268
413
|
|
|
414
|
+
const policy = currentEvidencePolicy(loaded.state, loaded.plan)
|
|
415
|
+
let receipt = options.receipt || null
|
|
416
|
+
if (receipt) {
|
|
417
|
+
const validation = validateGateReceipt(receipt)
|
|
418
|
+
if (!validation.valid) {
|
|
419
|
+
const error = new Error("invalid plan verification receipt")
|
|
420
|
+
error.validation = validation
|
|
421
|
+
throw error
|
|
422
|
+
}
|
|
423
|
+
if (receipt.kind !== "plan-verification") throw new Error("plan approval requires a plan-verification receipt")
|
|
424
|
+
if (receipt.slug !== slug) throw new Error("plan verification receipt slug mismatch")
|
|
425
|
+
if (receipt.verdict !== "PASS") throw new Error("plan verification receipt must record PASS")
|
|
426
|
+
if (receipt.planHash !== hash) throw new Error("plan verification receipt planHash mismatch")
|
|
427
|
+
} else if (policy.structuredPlanApproval) {
|
|
428
|
+
throw new Error("strict plan approval requires a structured plan-verification receipt")
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (!evidenceText) throw new Error("plan approval requires non-empty plan-checker evidence")
|
|
432
|
+
|
|
269
433
|
const timestamp = now()
|
|
270
434
|
loaded.state.schemaVersion = STATE_SCHEMA
|
|
271
435
|
loaded.state.planHash = hash
|
|
436
|
+
loaded.state.evidencePolicy = policy
|
|
272
437
|
loaded.state.planApproval = {
|
|
273
438
|
status: "passed",
|
|
274
439
|
planHash: hash,
|
|
275
440
|
at: timestamp,
|
|
276
441
|
evidence: evidenceText,
|
|
442
|
+
receipt: receipt || null,
|
|
443
|
+
}
|
|
444
|
+
if (receipt) {
|
|
445
|
+
loaded.evidence.gateReceipts ??= []
|
|
446
|
+
if (!loaded.evidence.gateReceipts.some((item) => item.id === receipt.id)) {
|
|
447
|
+
loaded.evidence.gateReceipts.push({ ...receipt, recordedAt: timestamp })
|
|
448
|
+
}
|
|
449
|
+
loaded.evidence.updatedAt = timestamp
|
|
450
|
+
await writeJson(loaded.paths.evidence, loaded.evidence)
|
|
277
451
|
}
|
|
278
452
|
loaded.state.status = "ready"
|
|
279
453
|
loaded.state.updatedAt = timestamp
|
|
@@ -282,6 +456,12 @@ export async function approvePlan(root, slug, evidence) {
|
|
|
282
456
|
? `Execute ready task(s): ${next.join(", ")}`
|
|
283
457
|
: "No executable task; inspect dependencies and blockers."
|
|
284
458
|
await writeJson(loaded.paths.state, loaded.state)
|
|
459
|
+
await journal(loaded.paths, "plan.approved", {
|
|
460
|
+
planHash: hash,
|
|
461
|
+
ready: next,
|
|
462
|
+
structured: Boolean(receipt),
|
|
463
|
+
receiptId: receipt?.id || null,
|
|
464
|
+
})
|
|
285
465
|
return { planHash: hash, approval: loaded.state.planApproval, ready: next }
|
|
286
466
|
})
|
|
287
467
|
}
|
|
@@ -331,6 +511,9 @@ export async function workStatus(root, slug) {
|
|
|
331
511
|
taskID,
|
|
332
512
|
runId: value.runId || null,
|
|
333
513
|
attempts: value.attempts || 0,
|
|
514
|
+
sessionID: value.owner?.sessionID || null,
|
|
515
|
+
executionDir: value.owner?.executionDir || null,
|
|
516
|
+
sandboxDir: value.owner?.sandboxDir || null,
|
|
334
517
|
heartbeatAt: value.heartbeatAt || null,
|
|
335
518
|
leaseExpiresAt: value.leaseExpiresAt || null,
|
|
336
519
|
}))
|
|
@@ -355,6 +538,7 @@ export async function workStatus(root, slug) {
|
|
|
355
538
|
completed,
|
|
356
539
|
coverage: completed ? receiptBacked / completed : 0,
|
|
357
540
|
receipts: (loaded.evidence.receipts || []).length,
|
|
541
|
+
gateReceipts: (loaded.evidence.gateReceipts || []).length,
|
|
358
542
|
},
|
|
359
543
|
nextAction: loaded.state.nextAction,
|
|
360
544
|
updatedAt: loaded.state.updatedAt,
|
|
@@ -396,6 +580,13 @@ export async function startTask(root, slug, taskID, options = {}) {
|
|
|
396
580
|
loaded.state.updatedAt = timestamp
|
|
397
581
|
loaded.state.nextAction = `Complete ${taskID}, write its report, then record fresh verification evidence.`
|
|
398
582
|
await writeJson(loaded.paths.state, loaded.state)
|
|
583
|
+
await journal(loaded.paths, "task.started", {
|
|
584
|
+
task: taskID,
|
|
585
|
+
runId: record.runId,
|
|
586
|
+
attempt: record.attempts,
|
|
587
|
+
owner: record.owner,
|
|
588
|
+
leaseExpiresAt: record.leaseExpiresAt,
|
|
589
|
+
})
|
|
399
590
|
return { task, record: { ...record } }
|
|
400
591
|
})
|
|
401
592
|
|
|
@@ -405,11 +596,46 @@ export async function startTask(root, slug, taskID, options = {}) {
|
|
|
405
596
|
|
|
406
597
|
function assertRunFence(record, options = {}) {
|
|
407
598
|
const runId = options.runId || null
|
|
408
|
-
if (
|
|
599
|
+
if (record.runId && !runId) {
|
|
600
|
+
throw new Error("task run fence requires the active runId")
|
|
601
|
+
}
|
|
602
|
+
if (runId && record.runId !== runId) {
|
|
409
603
|
throw new Error("task run fence mismatch; the executor lease is stale")
|
|
410
604
|
}
|
|
411
605
|
}
|
|
412
606
|
|
|
607
|
+
export async function attachTaskSession(root, slug, taskID, runId, sessionID, options = {}) {
|
|
608
|
+
const value = String(sessionID || "").trim()
|
|
609
|
+
if (!value) throw new Error("sessionID is required")
|
|
610
|
+
return withWorkLock(root, slug, async () => {
|
|
611
|
+
const loaded = await loadWork(root, slug)
|
|
612
|
+
const record = loaded.state.tasks?.[taskID]
|
|
613
|
+
if (!record) throw new Error(`task '${taskID}' is not tracked`)
|
|
614
|
+
if (record.status !== "running") throw new Error(`task '${taskID}' is not running`)
|
|
615
|
+
assertRunFence(record, { runId })
|
|
616
|
+
record.owner ??= { pid: process.pid, sessionID: null }
|
|
617
|
+
record.owner.sessionID = value
|
|
618
|
+
record.owner.executionDir = options.executionDir ? path.resolve(options.executionDir) : null
|
|
619
|
+
record.owner.sandboxDir = options.sandboxDir ? path.resolve(options.sandboxDir) : null
|
|
620
|
+
loaded.state.updatedAt = now()
|
|
621
|
+
await writeJson(loaded.paths.state, loaded.state)
|
|
622
|
+
await journal(loaded.paths, "task.session-attached", {
|
|
623
|
+
task: taskID,
|
|
624
|
+
runId: record.runId,
|
|
625
|
+
sessionID: value,
|
|
626
|
+
executionDir: record.owner.executionDir,
|
|
627
|
+
sandboxDir: record.owner.sandboxDir,
|
|
628
|
+
})
|
|
629
|
+
return {
|
|
630
|
+
taskID,
|
|
631
|
+
runId: record.runId,
|
|
632
|
+
sessionID: value,
|
|
633
|
+
executionDir: record.owner.executionDir,
|
|
634
|
+
sandboxDir: record.owner.sandboxDir,
|
|
635
|
+
}
|
|
636
|
+
})
|
|
637
|
+
}
|
|
638
|
+
|
|
413
639
|
export async function heartbeatTask(root, slug, taskID, runId, options = {}) {
|
|
414
640
|
return withWorkLock(root, slug, async () => {
|
|
415
641
|
const loaded = await loadWork(root, slug)
|
|
@@ -423,10 +649,54 @@ export async function heartbeatTask(root, slug, taskID, runId, options = {}) {
|
|
|
423
649
|
record.leaseExpiresAt = new Date(Date.now() + leaseMs).toISOString()
|
|
424
650
|
loaded.state.updatedAt = timestamp
|
|
425
651
|
await writeJson(loaded.paths.state, loaded.state)
|
|
652
|
+
await journal(loaded.paths, "task.heartbeat", {
|
|
653
|
+
task: taskID,
|
|
654
|
+
runId: record.runId,
|
|
655
|
+
heartbeatAt: record.heartbeatAt,
|
|
656
|
+
leaseExpiresAt: record.leaseExpiresAt,
|
|
657
|
+
})
|
|
426
658
|
return { taskID, runId: record.runId, heartbeatAt: record.heartbeatAt, leaseExpiresAt: record.leaseExpiresAt }
|
|
427
659
|
})
|
|
428
660
|
}
|
|
429
661
|
|
|
662
|
+
export async function recoverTask(root, slug, taskID, options = {}) {
|
|
663
|
+
return withWorkLock(root, slug, async () => {
|
|
664
|
+
const loaded = await loadWork(root, slug)
|
|
665
|
+
const record = loaded.state.tasks?.[taskID]
|
|
666
|
+
if (!record) throw new Error(`task '${taskID}' is not tracked`)
|
|
667
|
+
if (record.status !== "running") throw new Error(`task '${taskID}' is not running`)
|
|
668
|
+
|
|
669
|
+
const expires = Date.parse(record.leaseExpiresAt || "")
|
|
670
|
+
const stale = options.force || !Number.isFinite(expires) || expires <= Date.now()
|
|
671
|
+
if (!stale) throw new Error(`task '${taskID}' lease is still active`)
|
|
672
|
+
|
|
673
|
+
const timestamp = now()
|
|
674
|
+
const previousOwner = record.owner ? { ...record.owner } : null
|
|
675
|
+
const previousRunId = record.runId || null
|
|
676
|
+
record.status = "failed"
|
|
677
|
+
record.lastFailure = String(options.reason || "stale executor lease recovered after interruption")
|
|
678
|
+
record.lastOwner = previousOwner
|
|
679
|
+
record.lastRunId = previousRunId
|
|
680
|
+
record.runId = null
|
|
681
|
+
record.owner = null
|
|
682
|
+
record.heartbeatAt = null
|
|
683
|
+
record.leaseExpiresAt = null
|
|
684
|
+
|
|
685
|
+
loaded.state.integrationVerification = null
|
|
686
|
+
loaded.state.status = "executing"
|
|
687
|
+
loaded.state.updatedAt = timestamp
|
|
688
|
+
loaded.state.nextAction = `Recovered stale task ${taskID}. Re-diagnose and retry from fresh evidence.`
|
|
689
|
+
await writeJson(loaded.paths.state, loaded.state)
|
|
690
|
+
await journal(loaded.paths, "task.recovered", {
|
|
691
|
+
tasks: [taskID],
|
|
692
|
+
previousRunId,
|
|
693
|
+
previousOwner,
|
|
694
|
+
forced: Boolean(options.force),
|
|
695
|
+
})
|
|
696
|
+
return { recovered: [taskID], previousRunId, previousOwner, state: loaded.state }
|
|
697
|
+
})
|
|
698
|
+
}
|
|
699
|
+
|
|
430
700
|
export async function recoverStaleTasks(root, slug, options = {}) {
|
|
431
701
|
return withWorkLock(root, slug, async () => {
|
|
432
702
|
const loaded = await loadWork(root, slug)
|
|
@@ -437,8 +707,12 @@ export async function recoverStaleTasks(root, slug, options = {}) {
|
|
|
437
707
|
const expires = Date.parse(record.leaseExpiresAt || "")
|
|
438
708
|
const stale = options.force || !Number.isFinite(expires) || expires <= Date.now()
|
|
439
709
|
if (!stale) continue
|
|
710
|
+
const previousOwner = record.owner ? { ...record.owner } : null
|
|
711
|
+
const previousRunId = record.runId || null
|
|
440
712
|
record.status = "failed"
|
|
441
713
|
record.lastFailure = "stale executor lease recovered after interruption"
|
|
714
|
+
record.lastOwner = previousOwner
|
|
715
|
+
record.lastRunId = previousRunId
|
|
442
716
|
record.runId = null
|
|
443
717
|
record.owner = null
|
|
444
718
|
record.heartbeatAt = null
|
|
@@ -451,6 +725,7 @@ export async function recoverStaleTasks(root, slug, options = {}) {
|
|
|
451
725
|
loaded.state.updatedAt = timestamp
|
|
452
726
|
loaded.state.nextAction = `Recovered stale task(s): ${recovered.join(", ")}. Re-diagnose and retry from fresh evidence.`
|
|
453
727
|
await writeJson(loaded.paths.state, loaded.state)
|
|
728
|
+
await journal(loaded.paths, "task.recovered", { tasks: recovered })
|
|
454
729
|
}
|
|
455
730
|
return { recovered, state: loaded.state }
|
|
456
731
|
})
|
|
@@ -481,6 +756,12 @@ export async function recordVerificationReceipt(root, slug, taskID, receipt) {
|
|
|
481
756
|
loaded.evidence.schemaVersion = STATE_SCHEMA
|
|
482
757
|
loaded.evidence.updatedAt = normalized.recordedAt
|
|
483
758
|
await writeJson(loaded.paths.evidence, loaded.evidence)
|
|
759
|
+
await journal(loaded.paths, "verification.receipt", {
|
|
760
|
+
task: taskID,
|
|
761
|
+
runId: record.runId,
|
|
762
|
+
receiptId: normalized.id,
|
|
763
|
+
passed: normalized.passed,
|
|
764
|
+
})
|
|
484
765
|
return normalized
|
|
485
766
|
})
|
|
486
767
|
}
|
|
@@ -499,18 +780,31 @@ export async function completeTask(root, slug, taskID, options = {}) {
|
|
|
499
780
|
assertRunFence(record, options)
|
|
500
781
|
|
|
501
782
|
const timestamp = now()
|
|
783
|
+
const receipts = (loaded.evidence.receipts || []).filter((item) => item.task === taskID && (!record.runId || item.runId === record.runId))
|
|
784
|
+
const successfulReceipts = receipts.filter((item) => item.passed)
|
|
785
|
+
const evidencePolicy = currentEvidencePolicy(loaded.state, loaded.plan)
|
|
786
|
+
const currentFingerprint = workspaceFingerprint(loaded.paths.root)
|
|
787
|
+
const freshSuccessfulReceipts = successfulReceipts.filter(
|
|
788
|
+
(item) => item.workspaceAfter && item.workspaceAfter === currentFingerprint,
|
|
789
|
+
)
|
|
790
|
+
if (evidencePolicy.receiptRequiredForTasks && freshSuccessfulReceipts.length === 0) {
|
|
791
|
+
throw new Error("strict task completion requires a successful verification receipt for the active run and current workspace fingerprint")
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const acceptedReceipts = evidencePolicy.receiptRequiredForTasks
|
|
795
|
+
? freshSuccessfulReceipts
|
|
796
|
+
: successfulReceipts
|
|
797
|
+
|
|
502
798
|
const reportPath = path.join(loaded.paths.reports, `${taskID}.md`)
|
|
503
799
|
if (options.report) await atomicWrite(reportPath, String(options.report).trimEnd() + "\n")
|
|
504
800
|
else if (!existsSync(reportPath)) {
|
|
505
801
|
await atomicWrite(reportPath, `# ${taskID} report\n\nCompleted with evidence recorded in EVIDENCE.json.\n`)
|
|
506
802
|
}
|
|
507
803
|
|
|
508
|
-
const receipts = (loaded.evidence.receipts || []).filter((item) => item.task === taskID && (!record.runId || item.runId === record.runId))
|
|
509
|
-
const successfulReceipts = receipts.filter((item) => item.passed)
|
|
510
804
|
record.status = "completed"
|
|
511
805
|
record.completedAt = timestamp
|
|
512
806
|
record.report = path.relative(loaded.paths.root, reportPath).replaceAll("\\", "/")
|
|
513
|
-
record.evidenceStrength =
|
|
807
|
+
record.evidenceStrength = acceptedReceipts.length ? "receipt-backed" : "narrative"
|
|
514
808
|
record.runId = null
|
|
515
809
|
record.owner = null
|
|
516
810
|
record.heartbeatAt = null
|
|
@@ -522,7 +816,7 @@ export async function completeTask(root, slug, taskID, options = {}) {
|
|
|
522
816
|
evidence: evidenceText,
|
|
523
817
|
verification: task.verification,
|
|
524
818
|
evidenceStrength: record.evidenceStrength,
|
|
525
|
-
receiptIDs:
|
|
819
|
+
receiptIDs: acceptedReceipts.map((item) => item.id),
|
|
526
820
|
})
|
|
527
821
|
loaded.evidence.schemaVersion = STATE_SCHEMA
|
|
528
822
|
loaded.evidence.updatedAt = timestamp
|
|
@@ -540,6 +834,11 @@ export async function completeTask(root, slug, taskID, options = {}) {
|
|
|
540
834
|
|
|
541
835
|
await writeJson(loaded.paths.evidence, loaded.evidence)
|
|
542
836
|
await writeJson(loaded.paths.state, loaded.state)
|
|
837
|
+
await journal(loaded.paths, "task.completed", {
|
|
838
|
+
task: taskID,
|
|
839
|
+
evidenceStrength: record.evidenceStrength,
|
|
840
|
+
receiptIDs: acceptedReceipts.map((item) => item.id),
|
|
841
|
+
})
|
|
543
842
|
return { task, state: loaded.state, ready }
|
|
544
843
|
})
|
|
545
844
|
}
|
|
@@ -554,6 +853,7 @@ export async function failTask(root, slug, taskID, reason, options = {}) {
|
|
|
554
853
|
taskByID(loaded.plan, taskID)
|
|
555
854
|
const record = loaded.state.tasks?.[taskID]
|
|
556
855
|
if (!record) throw new Error(`task '${taskID}' is not tracked`)
|
|
856
|
+
if (record.status !== "running") throw new Error(`task '${taskID}' must be running before it can fail`)
|
|
557
857
|
assertRunFence(record, options)
|
|
558
858
|
|
|
559
859
|
const timestamp = now()
|
|
@@ -570,6 +870,11 @@ export async function failTask(root, slug, taskID, reason, options = {}) {
|
|
|
570
870
|
? `Re-plan or escalate ${taskID}; it has failed ${record.attempts} attempt(s).`
|
|
571
871
|
: `Re-diagnose ${taskID} from fresh evidence before retrying.`
|
|
572
872
|
await writeJson(loaded.paths.state, loaded.state)
|
|
873
|
+
await journal(loaded.paths, "task.failed", {
|
|
874
|
+
task: taskID,
|
|
875
|
+
attempts: record.attempts,
|
|
876
|
+
reason: cleanReason,
|
|
877
|
+
})
|
|
573
878
|
return { taskID, attempts: record.attempts, state: loaded.state }
|
|
574
879
|
})
|
|
575
880
|
}
|
|
@@ -627,12 +932,12 @@ export async function resolveBlocker(root, slug, blocker) {
|
|
|
627
932
|
})
|
|
628
933
|
}
|
|
629
934
|
|
|
630
|
-
export async function recordIntegrationVerification(root, slug, verdict, evidence, report = null) {
|
|
935
|
+
export async function recordIntegrationVerification(root, slug, verdict, evidence, report = null, options = {}) {
|
|
631
936
|
const status = String(verdict || "").trim().toUpperCase()
|
|
632
937
|
if (!["PASS", "FAIL", "PARTIAL"].includes(status)) {
|
|
633
938
|
throw new Error("integration verdict must be PASS, FAIL, or PARTIAL")
|
|
634
939
|
}
|
|
635
|
-
const evidenceText = String(evidence || "").trim()
|
|
940
|
+
const evidenceText = String(evidence || options.receipt?.evidence || "").trim()
|
|
636
941
|
if (!evidenceText) throw new Error("integration verification requires non-empty evidence")
|
|
637
942
|
|
|
638
943
|
return withWorkLock(root, slug, async () => {
|
|
@@ -650,12 +955,37 @@ export async function recordIntegrationVerification(root, slug, verdict, evidenc
|
|
|
650
955
|
|
|
651
956
|
const timestamp = now()
|
|
652
957
|
const fingerprint = workspaceFingerprint(loaded.paths.root)
|
|
958
|
+
const policy = currentEvidencePolicy(loaded.state, loaded.plan)
|
|
959
|
+
const receipt = options.receipt || null
|
|
960
|
+
if (receipt) {
|
|
961
|
+
const validation = validateGateReceipt(receipt)
|
|
962
|
+
if (!validation.valid) {
|
|
963
|
+
const error = new Error("invalid integration verification receipt")
|
|
964
|
+
error.validation = validation
|
|
965
|
+
throw error
|
|
966
|
+
}
|
|
967
|
+
if (receipt.kind !== "integration-verification") throw new Error("integration verification requires an integration-verification receipt")
|
|
968
|
+
if (receipt.slug !== slug) throw new Error("integration verification receipt slug mismatch")
|
|
969
|
+
if (receipt.verdict !== status) throw new Error("integration verification receipt verdict mismatch")
|
|
970
|
+
if (receipt.workspaceFingerprint !== fingerprint) {
|
|
971
|
+
throw new Error("integration verification receipt workspace fingerprint mismatch")
|
|
972
|
+
}
|
|
973
|
+
} else if (status === "PASS" && policy.structuredIntegrationApproval) {
|
|
974
|
+
throw new Error("strict integration PASS requires a structured integration-verification receipt")
|
|
975
|
+
}
|
|
653
976
|
loaded.state.integrationVerification = {
|
|
654
977
|
status,
|
|
655
978
|
at: timestamp,
|
|
656
979
|
evidence: evidenceText,
|
|
657
980
|
report: report ? String(report) : null,
|
|
658
981
|
fingerprint,
|
|
982
|
+
receipt: receipt || null,
|
|
983
|
+
}
|
|
984
|
+
if (receipt) {
|
|
985
|
+
loaded.evidence.gateReceipts ??= []
|
|
986
|
+
if (!loaded.evidence.gateReceipts.some((item) => item.id === receipt.id)) {
|
|
987
|
+
loaded.evidence.gateReceipts.push({ ...receipt, recordedAt: timestamp })
|
|
988
|
+
}
|
|
659
989
|
}
|
|
660
990
|
loaded.state.updatedAt = timestamp
|
|
661
991
|
loaded.state.status = status === "PASS" ? "ready-to-finalize" : "integration-verification"
|
|
@@ -676,6 +1006,11 @@ export async function recordIntegrationVerification(root, slug, verdict, evidenc
|
|
|
676
1006
|
|
|
677
1007
|
await writeJson(loaded.paths.evidence, loaded.evidence)
|
|
678
1008
|
await writeJson(loaded.paths.state, loaded.state)
|
|
1009
|
+
await journal(loaded.paths, "integration.verified", {
|
|
1010
|
+
verdict: status,
|
|
1011
|
+
fingerprint,
|
|
1012
|
+
receiptId: receipt?.id || null,
|
|
1013
|
+
})
|
|
679
1014
|
return loaded.state.integrationVerification
|
|
680
1015
|
})
|
|
681
1016
|
}
|
|
@@ -720,6 +1055,7 @@ export async function finalizeWork(root, slug, evidence) {
|
|
|
720
1055
|
|
|
721
1056
|
await writeJson(loaded.paths.evidence, loaded.evidence)
|
|
722
1057
|
await writeJson(loaded.paths.state, loaded.state)
|
|
1058
|
+
await journal(loaded.paths, "work.finalized", { fingerprint: currentFingerprint })
|
|
723
1059
|
return { state: loaded.state, evidence: loaded.evidence.entries.at(-1) }
|
|
724
1060
|
})
|
|
725
1061
|
}
|
|
@@ -736,7 +1072,24 @@ async function buildContextPack(loaded, taskID) {
|
|
|
736
1072
|
dependencyReports[dep] = (await readFile(file, "utf8")).slice(0, 12000)
|
|
737
1073
|
}
|
|
738
1074
|
|
|
739
|
-
const
|
|
1075
|
+
const taskText = [
|
|
1076
|
+
task.title,
|
|
1077
|
+
task.summary,
|
|
1078
|
+
...(task.acceptance || []),
|
|
1079
|
+
...(task.verification || []),
|
|
1080
|
+
].filter(Boolean).join(" ")
|
|
1081
|
+
const contextPolicy = classifyEngineeringTask(taskText, {
|
|
1082
|
+
changedFiles: taskFiles(task).length,
|
|
1083
|
+
risk: task.risk,
|
|
1084
|
+
})
|
|
1085
|
+
const contextManifest = await buildContextManifest(
|
|
1086
|
+
loaded.paths.root,
|
|
1087
|
+
task,
|
|
1088
|
+
{
|
|
1089
|
+
budget: contextPolicy.contextBudget,
|
|
1090
|
+
strategy: contextPolicy.profile?.contextStrategy || "semantic+graph+git",
|
|
1091
|
+
},
|
|
1092
|
+
).catch(() => null)
|
|
740
1093
|
const learnings = await relevantAcceptedLearnings(
|
|
741
1094
|
loaded.paths.root,
|
|
742
1095
|
[task.title, task.summary, ...(task.acceptance || [])].join(" "),
|
|
@@ -744,6 +1097,7 @@ async function buildContextPack(loaded, taskID) {
|
|
|
744
1097
|
|
|
745
1098
|
return {
|
|
746
1099
|
schemaVersion: STATE_SCHEMA,
|
|
1100
|
+
contextPolicy,
|
|
747
1101
|
slug: loaded.state.slug,
|
|
748
1102
|
task,
|
|
749
1103
|
taskBrief: path.relative(loaded.paths.root, path.join(loaded.paths.tasks, `${taskID}.md`)).replaceAll("\\", "/"),
|
|
@@ -761,10 +1115,47 @@ async function buildContextPack(loaded, taskID) {
|
|
|
761
1115
|
}
|
|
762
1116
|
}
|
|
763
1117
|
|
|
1118
|
+
export async function createPlanVerificationReceipt(root, slug, input = {}) {
|
|
1119
|
+
const loaded = await loadWork(root, slug)
|
|
1120
|
+
if (!loaded.plan) throw new Error("PLAN.json is missing")
|
|
1121
|
+
const hash = planHash(loaded.plan)
|
|
1122
|
+
return createGateReceipt({
|
|
1123
|
+
kind: "plan-verification",
|
|
1124
|
+
slug,
|
|
1125
|
+
verdict: input.verdict || "PASS",
|
|
1126
|
+
verifier: input.verifier || "ues-plan-checker",
|
|
1127
|
+
sessionID: input.sessionID || null,
|
|
1128
|
+
runId: input.runId || null,
|
|
1129
|
+
planHash: hash,
|
|
1130
|
+
evidence: input.evidence,
|
|
1131
|
+
report: input.report,
|
|
1132
|
+
})
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
export async function createIntegrationVerificationReceipt(root, slug, input = {}) {
|
|
1136
|
+
await loadWork(root, slug)
|
|
1137
|
+
return createGateReceipt({
|
|
1138
|
+
kind: "integration-verification",
|
|
1139
|
+
slug,
|
|
1140
|
+
verdict: input.verdict || "PASS",
|
|
1141
|
+
verifier: input.verifier || "ues-integration-verifier",
|
|
1142
|
+
sessionID: input.sessionID || null,
|
|
1143
|
+
runId: input.runId || null,
|
|
1144
|
+
workspaceFingerprint: workspaceFingerprint(root),
|
|
1145
|
+
evidence: input.evidence,
|
|
1146
|
+
report: input.report,
|
|
1147
|
+
})
|
|
1148
|
+
}
|
|
1149
|
+
|
|
764
1150
|
export async function contextPack(root, slug, taskID) {
|
|
765
1151
|
return buildContextPack(await loadWork(root, slug), taskID)
|
|
766
1152
|
}
|
|
767
1153
|
|
|
1154
|
+
export async function runtimeEvents(root, slug, options = {}) {
|
|
1155
|
+
const paths = workPaths(root, slug)
|
|
1156
|
+
return readRuntimeEvents(paths.events, options)
|
|
1157
|
+
}
|
|
1158
|
+
|
|
768
1159
|
export async function resumeWork(root, slug) {
|
|
769
1160
|
await recoverStaleTasks(root, slug).catch(() => ({ recovered: [] }))
|
|
770
1161
|
const loaded = await loadWork(root, slug)
|
|
@@ -773,5 +1164,6 @@ export async function resumeWork(root, slug) {
|
|
|
773
1164
|
planAnalysis: loaded.plan ? analyzePlan(loaded.plan) : null,
|
|
774
1165
|
completedEvidence: loaded.evidence.entries || [],
|
|
775
1166
|
decisions: loaded.state.decisions || [],
|
|
1167
|
+
recentEvents: await readRuntimeEvents(loaded.paths.events, { limit: 50 }),
|
|
776
1168
|
}
|
|
777
1169
|
}
|