opencode-agent-skill 7.7.0 → 10.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +112 -3
  2. package/README.md +396 -281
  3. package/bin/ocskill.mjs +382 -156
  4. package/docs/DETERMINISTIC-TOOLS.md +25 -8
  5. package/docs/ENGINEERING-DESIGN.md +31 -13
  6. package/docs/EVALS.md +34 -12
  7. package/docs/NPM-PUBLISH.md +6 -6
  8. package/docs/OPENCODE-COMPAT.md +11 -8
  9. package/docs/TRACE-SCHEMA.md +15 -2
  10. package/docs/V8-INTELLIGENCE-RELIABILITY.md +206 -0
  11. package/docs/V9-SPEED-INTELLIGENCE.md +102 -0
  12. package/evals/live/tasks.json +6 -6
  13. package/evals/polyglot/fixtures/polyglot-bench/api/generated/client.ts +2 -0
  14. package/evals/polyglot/fixtures/polyglot-bench/api/openapi.json +25 -0
  15. package/evals/polyglot/fixtures/polyglot-bench/db/migrations/20260920_add_order_key.sql +1 -0
  16. package/evals/polyglot/fixtures/polyglot-bench/dotnet/OrderService.cs +8 -0
  17. package/evals/polyglot/fixtures/polyglot-bench/java/PriceService.java +5 -0
  18. package/evals/polyglot/fixtures/polyglot-bench/monorepo/package.json +6 -0
  19. package/evals/polyglot/fixtures/polyglot-bench/monorepo/packages/api/package.json +4 -0
  20. package/evals/polyglot/fixtures/polyglot-bench/monorepo/packages/web/package.json +7 -0
  21. package/evals/polyglot/fixtures/polyglot-bench/monorepo/pnpm-lock.yaml +5 -0
  22. package/evals/polyglot/fixtures/polyglot-bench/next/app/api/products/route.ts +7 -0
  23. package/evals/polyglot/fixtures/polyglot-bench/python/tenant_auth.py +4 -0
  24. package/evals/polyglot/fixtures/polyglot-bench/react-native/keyboard.ts +3 -0
  25. package/evals/polyglot/graders/polyglot-bench.mjs +101 -0
  26. package/evals/polyglot/tasks.json +54 -0
  27. package/global-config/AGENTS.md +78 -160
  28. package/global-config/agents/integration-verifier.md +1 -1
  29. package/global-config/agents/plan-checker.md +1 -1
  30. package/global-config/commands/run.md +9 -5
  31. package/global-config/plugins/ues-router/capabilities.js +4 -0
  32. package/global-config/plugins/ues-router/index.js +784 -37
  33. package/global-config/plugins/ues-router/router.js +175 -23
  34. package/global-config/plugins/ues-router/runtime-guard.js +265 -0
  35. package/global-config/skills/engineering-orchestrator/references/long-horizon.md +6 -4
  36. package/lib/aci.mjs +128 -0
  37. package/lib/benchmark-confidence.mjs +173 -0
  38. package/lib/cli-utils.mjs +41 -0
  39. package/lib/container-sandbox.mjs +102 -0
  40. package/lib/context-manifest.mjs +300 -22
  41. package/lib/control-center.mjs +36 -3
  42. package/lib/eval-ablation.mjs +104 -0
  43. package/lib/eval-order.mjs +9 -0
  44. package/lib/eval-report.mjs +11 -0
  45. package/lib/eval-telemetry.mjs +8 -2
  46. package/lib/gate-receipt.mjs +52 -0
  47. package/lib/installer.mjs +39 -25
  48. package/lib/learning-engine.mjs +236 -38
  49. package/lib/model-policy.mjs +6 -0
  50. package/lib/opencode-compat.mjs +25 -10
  51. package/lib/orchestrator-policy.mjs +195 -21
  52. package/lib/process-runner.mjs +30 -9
  53. package/lib/runtime-events.mjs +31 -0
  54. package/lib/semantic-index.mjs +318 -0
  55. package/lib/task-engine.mjs +557 -28
  56. package/lib/trajectory.mjs +89 -0
  57. package/lib/windows-shim.mjs +227 -0
  58. package/lib/worktree-sandbox.mjs +85 -3
  59. package/package.json +11 -4
  60. package/scripts/check-release-tag.mjs +22 -0
  61. package/scripts/control-center.mjs +25 -0
  62. package/scripts/eval-ablation.mjs +44 -0
  63. package/scripts/eval-live.mjs +39 -58
  64. package/scripts/eval-matrix.mjs +166 -0
  65. package/scripts/smoke-packed-install.mjs +138 -4
  66. package/scripts/smoke-plain-install.mjs +91 -0
  67. package/scripts/validate-live-suite.mjs +3 -3
  68. package/scripts/validate.mjs +27 -5
@@ -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, recoveryPolicyForAttempt } from "./orchestrator-policy.mjs"
10
13
 
11
14
  const WORK_DIR = ".ues-work"
12
- const STATE_SCHEMA = 3
15
+ const STATE_SCHEMA = 4
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({ pid: process.pid, at: now() }) + "\n",
95
+ JSON.stringify(owner) + "\n",
78
96
  "utf8",
79
- ).catch(() => {})
80
- return
97
+ )
98
+ return owner
81
99
  } catch (error) {
82
100
  if (error?.code !== "EEXIST") throw error
83
- const info = await stat(paths.lock).catch(() => null)
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
- await rm(paths.lock, { recursive: true, force: true }).catch(() => {})
86
- continue
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
- await rm(paths.lock, { recursive: true, force: true }).catch(() => {})
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 createHash("sha256").update("non-git:" + root).digest("hex")
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)
@@ -163,6 +284,7 @@ export async function initWork(root, slug, goal) {
163
284
  tasks: {},
164
285
  decisions: [],
165
286
  blockers: [],
287
+ checkpoint: null,
166
288
  nextAction: "Complete SPEC.md, create PLAN.json, then run ocskill work plan.",
167
289
  })
168
290
  await writeJson(paths.evidence, {
@@ -171,7 +293,9 @@ export async function initWork(root, slug, goal) {
171
293
  updatedAt: createdAt,
172
294
  entries: [],
173
295
  receipts: [],
296
+ gateReceipts: [],
174
297
  })
298
+ await journal(paths, "work.init", { goal: cleanGoal })
175
299
 
176
300
  return { ...paths, slug, goal: cleanGoal }
177
301
  }
@@ -185,6 +309,24 @@ export async function loadWork(root, slug) {
185
309
  return { paths, state, plan, evidence }
186
310
  }
187
311
 
312
+ function evidencePolicyForPlan(plan) {
313
+ const tasks = Array.isArray(plan?.tasks) ? plan.tasks : []
314
+ const highRisk = tasks.some((task) => ["high", "critical"].includes(task?.risk))
315
+ const longHorizon = tasks.length > 1 || plan?.mode === "long-horizon" || plan?.longHorizon === true
316
+ return {
317
+ schemaVersion: 1,
318
+ strict: highRisk || longHorizon,
319
+ receiptRequiredForTasks: highRisk || longHorizon,
320
+ structuredPlanApproval: highRisk || longHorizon,
321
+ structuredIntegrationApproval: highRisk || longHorizon,
322
+ reason: highRisk ? "high-risk-plan" : longHorizon ? "long-horizon-plan" : "standard-plan",
323
+ }
324
+ }
325
+
326
+ function currentEvidencePolicy(state, plan) {
327
+ return state?.evidencePolicy || evidencePolicyForPlan(plan)
328
+ }
329
+
188
330
  function taskBrief(task) {
189
331
  const files = taskFiles(task)
190
332
  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 +388,22 @@ export async function importPlan(root, slug, planInput) {
246
388
  planHash: hash,
247
389
  planApproval: { status: "pending", planHash: hash, at: updatedAt, evidence: null },
248
390
  integrationVerification: null,
391
+ checkpoint: null,
392
+ evidencePolicy: evidencePolicyForPlan(plan),
249
393
  tasks: stateTasks,
250
394
  nextAction: "Run ues-plan-checker, then record PASS with 'ocskill work approve-plan'.",
251
395
  }
252
396
  await writeJson(loaded.paths.state, state)
397
+ await journal(loaded.paths, "plan.imported", {
398
+ planHash: hash,
399
+ taskCount: plan.tasks.length,
400
+ })
253
401
  return { plan, analysis, state, paths: loaded.paths }
254
402
  })
255
403
  }
256
404
 
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")
405
+ export async function approvePlan(root, slug, evidence, options = {}) {
406
+ const evidenceText = String(evidence || options.receipt?.evidence || "").trim()
260
407
 
261
408
  return withWorkLock(root, slug, async () => {
262
409
  const loaded = await loadWork(root, slug)
@@ -266,14 +413,43 @@ export async function approvePlan(root, slug, evidence) {
266
413
  throw new Error("PLAN.json changed after import; re-import it before approval")
267
414
  }
268
415
 
416
+ const policy = currentEvidencePolicy(loaded.state, loaded.plan)
417
+ let receipt = options.receipt || null
418
+ if (receipt) {
419
+ const validation = validateGateReceipt(receipt)
420
+ if (!validation.valid) {
421
+ const error = new Error("invalid plan verification receipt")
422
+ error.validation = validation
423
+ throw error
424
+ }
425
+ if (receipt.kind !== "plan-verification") throw new Error("plan approval requires a plan-verification receipt")
426
+ if (receipt.slug !== slug) throw new Error("plan verification receipt slug mismatch")
427
+ if (receipt.verdict !== "PASS") throw new Error("plan verification receipt must record PASS")
428
+ if (receipt.planHash !== hash) throw new Error("plan verification receipt planHash mismatch")
429
+ } else if (policy.structuredPlanApproval) {
430
+ throw new Error("strict plan approval requires a structured plan-verification receipt")
431
+ }
432
+
433
+ if (!evidenceText) throw new Error("plan approval requires non-empty plan-checker evidence")
434
+
269
435
  const timestamp = now()
270
436
  loaded.state.schemaVersion = STATE_SCHEMA
271
437
  loaded.state.planHash = hash
438
+ loaded.state.evidencePolicy = policy
272
439
  loaded.state.planApproval = {
273
440
  status: "passed",
274
441
  planHash: hash,
275
442
  at: timestamp,
276
443
  evidence: evidenceText,
444
+ receipt: receipt || null,
445
+ }
446
+ if (receipt) {
447
+ loaded.evidence.gateReceipts ??= []
448
+ if (!loaded.evidence.gateReceipts.some((item) => item.id === receipt.id)) {
449
+ loaded.evidence.gateReceipts.push({ ...receipt, recordedAt: timestamp })
450
+ }
451
+ loaded.evidence.updatedAt = timestamp
452
+ await writeJson(loaded.paths.evidence, loaded.evidence)
277
453
  }
278
454
  loaded.state.status = "ready"
279
455
  loaded.state.updatedAt = timestamp
@@ -282,6 +458,12 @@ export async function approvePlan(root, slug, evidence) {
282
458
  ? `Execute ready task(s): ${next.join(", ")}`
283
459
  : "No executable task; inspect dependencies and blockers."
284
460
  await writeJson(loaded.paths.state, loaded.state)
461
+ await journal(loaded.paths, "plan.approved", {
462
+ planHash: hash,
463
+ ready: next,
464
+ structured: Boolean(receipt),
465
+ receiptId: receipt?.id || null,
466
+ })
285
467
  return { planHash: hash, approval: loaded.state.planApproval, ready: next }
286
468
  })
287
469
  }
@@ -309,7 +491,7 @@ export function readyTasks(plan, state) {
309
491
  const ready = []
310
492
  for (const task of plan.tasks) {
311
493
  const status = state.tasks?.[task.id]?.status
312
- if (!["pending", "failed"].includes(status)) continue
494
+ if (!["pending", "failed", "retryable"].includes(status)) continue
313
495
  if (!(task.dependsOn || []).every((dep) => completed.has(dep))) continue
314
496
  const files = taskFiles(task)
315
497
  if (files.length === 0 && runningFiles.size > 0) continue
@@ -321,7 +503,7 @@ export function readyTasks(plan, state) {
321
503
 
322
504
  export async function workStatus(root, slug) {
323
505
  const loaded = await loadWork(root, slug)
324
- const counts = { pending: 0, running: 0, completed: 0, failed: 0 }
506
+ const counts = { pending: 0, running: 0, completed: 0, failed: 0, retryable: 0 }
325
507
  for (const value of Object.values(loaded.state.tasks || {})) {
326
508
  if (Object.hasOwn(counts, value.status)) counts[value.status] += 1
327
509
  }
@@ -331,6 +513,9 @@ export async function workStatus(root, slug) {
331
513
  taskID,
332
514
  runId: value.runId || null,
333
515
  attempts: value.attempts || 0,
516
+ sessionID: value.owner?.sessionID || null,
517
+ executionDir: value.owner?.executionDir || null,
518
+ sandboxDir: value.owner?.sandboxDir || null,
334
519
  heartbeatAt: value.heartbeatAt || null,
335
520
  leaseExpiresAt: value.leaseExpiresAt || null,
336
521
  }))
@@ -355,8 +540,10 @@ export async function workStatus(root, slug) {
355
540
  completed,
356
541
  coverage: completed ? receiptBacked / completed : 0,
357
542
  receipts: (loaded.evidence.receipts || []).length,
543
+ gateReceipts: (loaded.evidence.gateReceipts || []).length,
358
544
  },
359
545
  nextAction: loaded.state.nextAction,
546
+ checkpoint: loaded.state.checkpoint || null,
360
547
  updatedAt: loaded.state.updatedAt,
361
548
  }
362
549
  }
@@ -396,6 +583,13 @@ export async function startTask(root, slug, taskID, options = {}) {
396
583
  loaded.state.updatedAt = timestamp
397
584
  loaded.state.nextAction = `Complete ${taskID}, write its report, then record fresh verification evidence.`
398
585
  await writeJson(loaded.paths.state, loaded.state)
586
+ await journal(loaded.paths, "task.started", {
587
+ task: taskID,
588
+ runId: record.runId,
589
+ attempt: record.attempts,
590
+ owner: record.owner,
591
+ leaseExpiresAt: record.leaseExpiresAt,
592
+ })
399
593
  return { task, record: { ...record } }
400
594
  })
401
595
 
@@ -405,11 +599,46 @@ export async function startTask(root, slug, taskID, options = {}) {
405
599
 
406
600
  function assertRunFence(record, options = {}) {
407
601
  const runId = options.runId || null
408
- if (runId && record.runId && runId !== record.runId) {
602
+ if (record.runId && !runId) {
603
+ throw new Error("task run fence requires the active runId")
604
+ }
605
+ if (runId && record.runId !== runId) {
409
606
  throw new Error("task run fence mismatch; the executor lease is stale")
410
607
  }
411
608
  }
412
609
 
610
+ export async function attachTaskSession(root, slug, taskID, runId, sessionID, options = {}) {
611
+ const value = String(sessionID || "").trim()
612
+ if (!value) throw new Error("sessionID is required")
613
+ return withWorkLock(root, slug, async () => {
614
+ const loaded = await loadWork(root, slug)
615
+ const record = loaded.state.tasks?.[taskID]
616
+ if (!record) throw new Error(`task '${taskID}' is not tracked`)
617
+ if (record.status !== "running") throw new Error(`task '${taskID}' is not running`)
618
+ assertRunFence(record, { runId })
619
+ record.owner ??= { pid: process.pid, sessionID: null }
620
+ record.owner.sessionID = value
621
+ record.owner.executionDir = options.executionDir ? path.resolve(options.executionDir) : null
622
+ record.owner.sandboxDir = options.sandboxDir ? path.resolve(options.sandboxDir) : null
623
+ loaded.state.updatedAt = now()
624
+ await writeJson(loaded.paths.state, loaded.state)
625
+ await journal(loaded.paths, "task.session-attached", {
626
+ task: taskID,
627
+ runId: record.runId,
628
+ sessionID: value,
629
+ executionDir: record.owner.executionDir,
630
+ sandboxDir: record.owner.sandboxDir,
631
+ })
632
+ return {
633
+ taskID,
634
+ runId: record.runId,
635
+ sessionID: value,
636
+ executionDir: record.owner.executionDir,
637
+ sandboxDir: record.owner.sandboxDir,
638
+ }
639
+ })
640
+ }
641
+
413
642
  export async function heartbeatTask(root, slug, taskID, runId, options = {}) {
414
643
  return withWorkLock(root, slug, async () => {
415
644
  const loaded = await loadWork(root, slug)
@@ -423,10 +652,54 @@ export async function heartbeatTask(root, slug, taskID, runId, options = {}) {
423
652
  record.leaseExpiresAt = new Date(Date.now() + leaseMs).toISOString()
424
653
  loaded.state.updatedAt = timestamp
425
654
  await writeJson(loaded.paths.state, loaded.state)
655
+ await journal(loaded.paths, "task.heartbeat", {
656
+ task: taskID,
657
+ runId: record.runId,
658
+ heartbeatAt: record.heartbeatAt,
659
+ leaseExpiresAt: record.leaseExpiresAt,
660
+ })
426
661
  return { taskID, runId: record.runId, heartbeatAt: record.heartbeatAt, leaseExpiresAt: record.leaseExpiresAt }
427
662
  })
428
663
  }
429
664
 
665
+ export async function recoverTask(root, slug, taskID, options = {}) {
666
+ return withWorkLock(root, slug, async () => {
667
+ const loaded = await loadWork(root, slug)
668
+ const record = loaded.state.tasks?.[taskID]
669
+ if (!record) throw new Error(`task '${taskID}' is not tracked`)
670
+ if (record.status !== "running") throw new Error(`task '${taskID}' is not running`)
671
+
672
+ const expires = Date.parse(record.leaseExpiresAt || "")
673
+ const stale = options.force || !Number.isFinite(expires) || expires <= Date.now()
674
+ if (!stale) throw new Error(`task '${taskID}' lease is still active`)
675
+
676
+ const timestamp = now()
677
+ const previousOwner = record.owner ? { ...record.owner } : null
678
+ const previousRunId = record.runId || null
679
+ record.status = "retryable"
680
+ record.lastFailure = String(options.reason || "stale executor lease recovered after interruption")
681
+ record.lastOwner = previousOwner
682
+ record.lastRunId = previousRunId
683
+ record.runId = null
684
+ record.owner = null
685
+ record.heartbeatAt = null
686
+ record.leaseExpiresAt = null
687
+
688
+ loaded.state.integrationVerification = null
689
+ loaded.state.status = "executing"
690
+ loaded.state.updatedAt = timestamp
691
+ loaded.state.nextAction = `Recovered stale task ${taskID}. Re-diagnose and retry from fresh evidence.`
692
+ await writeJson(loaded.paths.state, loaded.state)
693
+ await journal(loaded.paths, "task.recovered", {
694
+ tasks: [taskID],
695
+ previousRunId,
696
+ previousOwner,
697
+ forced: Boolean(options.force),
698
+ })
699
+ return { recovered: [taskID], previousRunId, previousOwner, state: loaded.state }
700
+ })
701
+ }
702
+
430
703
  export async function recoverStaleTasks(root, slug, options = {}) {
431
704
  return withWorkLock(root, slug, async () => {
432
705
  const loaded = await loadWork(root, slug)
@@ -437,8 +710,12 @@ export async function recoverStaleTasks(root, slug, options = {}) {
437
710
  const expires = Date.parse(record.leaseExpiresAt || "")
438
711
  const stale = options.force || !Number.isFinite(expires) || expires <= Date.now()
439
712
  if (!stale) continue
440
- record.status = "failed"
713
+ const previousOwner = record.owner ? { ...record.owner } : null
714
+ const previousRunId = record.runId || null
715
+ record.status = "retryable"
441
716
  record.lastFailure = "stale executor lease recovered after interruption"
717
+ record.lastOwner = previousOwner
718
+ record.lastRunId = previousRunId
442
719
  record.runId = null
443
720
  record.owner = null
444
721
  record.heartbeatAt = null
@@ -451,6 +728,7 @@ export async function recoverStaleTasks(root, slug, options = {}) {
451
728
  loaded.state.updatedAt = timestamp
452
729
  loaded.state.nextAction = `Recovered stale task(s): ${recovered.join(", ")}. Re-diagnose and retry from fresh evidence.`
453
730
  await writeJson(loaded.paths.state, loaded.state)
731
+ await journal(loaded.paths, "task.recovered", { tasks: recovered })
454
732
  }
455
733
  return { recovered, state: loaded.state }
456
734
  })
@@ -481,6 +759,12 @@ export async function recordVerificationReceipt(root, slug, taskID, receipt) {
481
759
  loaded.evidence.schemaVersion = STATE_SCHEMA
482
760
  loaded.evidence.updatedAt = normalized.recordedAt
483
761
  await writeJson(loaded.paths.evidence, loaded.evidence)
762
+ await journal(loaded.paths, "verification.receipt", {
763
+ task: taskID,
764
+ runId: record.runId,
765
+ receiptId: normalized.id,
766
+ passed: normalized.passed,
767
+ })
484
768
  return normalized
485
769
  })
486
770
  }
@@ -499,18 +783,31 @@ export async function completeTask(root, slug, taskID, options = {}) {
499
783
  assertRunFence(record, options)
500
784
 
501
785
  const timestamp = now()
786
+ const receipts = (loaded.evidence.receipts || []).filter((item) => item.task === taskID && (!record.runId || item.runId === record.runId))
787
+ const successfulReceipts = receipts.filter((item) => item.passed)
788
+ const evidencePolicy = currentEvidencePolicy(loaded.state, loaded.plan)
789
+ const currentFingerprint = workspaceFingerprint(loaded.paths.root)
790
+ const freshSuccessfulReceipts = successfulReceipts.filter(
791
+ (item) => item.workspaceAfter && item.workspaceAfter === currentFingerprint,
792
+ )
793
+ if (evidencePolicy.receiptRequiredForTasks && freshSuccessfulReceipts.length === 0) {
794
+ throw new Error("strict task completion requires a successful verification receipt for the active run and current workspace fingerprint")
795
+ }
796
+
797
+ const acceptedReceipts = evidencePolicy.receiptRequiredForTasks
798
+ ? freshSuccessfulReceipts
799
+ : successfulReceipts
800
+
502
801
  const reportPath = path.join(loaded.paths.reports, `${taskID}.md`)
503
802
  if (options.report) await atomicWrite(reportPath, String(options.report).trimEnd() + "\n")
504
803
  else if (!existsSync(reportPath)) {
505
804
  await atomicWrite(reportPath, `# ${taskID} report\n\nCompleted with evidence recorded in EVIDENCE.json.\n`)
506
805
  }
507
806
 
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
807
  record.status = "completed"
511
808
  record.completedAt = timestamp
512
809
  record.report = path.relative(loaded.paths.root, reportPath).replaceAll("\\", "/")
513
- record.evidenceStrength = successfulReceipts.length ? "receipt-backed" : "narrative"
810
+ record.evidenceStrength = acceptedReceipts.length ? "receipt-backed" : "narrative"
514
811
  record.runId = null
515
812
  record.owner = null
516
813
  record.heartbeatAt = null
@@ -522,7 +819,7 @@ export async function completeTask(root, slug, taskID, options = {}) {
522
819
  evidence: evidenceText,
523
820
  verification: task.verification,
524
821
  evidenceStrength: record.evidenceStrength,
525
- receiptIDs: successfulReceipts.map((item) => item.id),
822
+ receiptIDs: acceptedReceipts.map((item) => item.id),
526
823
  })
527
824
  loaded.evidence.schemaVersion = STATE_SCHEMA
528
825
  loaded.evidence.updatedAt = timestamp
@@ -540,6 +837,11 @@ export async function completeTask(root, slug, taskID, options = {}) {
540
837
 
541
838
  await writeJson(loaded.paths.evidence, loaded.evidence)
542
839
  await writeJson(loaded.paths.state, loaded.state)
840
+ await journal(loaded.paths, "task.completed", {
841
+ task: taskID,
842
+ evidenceStrength: record.evidenceStrength,
843
+ receiptIDs: acceptedReceipts.map((item) => item.id),
844
+ })
543
845
  return { task, state: loaded.state, ready }
544
846
  })
545
847
  }
@@ -554,6 +856,7 @@ export async function failTask(root, slug, taskID, reason, options = {}) {
554
856
  taskByID(loaded.plan, taskID)
555
857
  const record = loaded.state.tasks?.[taskID]
556
858
  if (!record) throw new Error(`task '${taskID}' is not tracked`)
859
+ if (record.status !== "running") throw new Error(`task '${taskID}' must be running before it can fail`)
557
860
  assertRunFence(record, options)
558
861
 
559
862
  const timestamp = now()
@@ -570,6 +873,11 @@ export async function failTask(root, slug, taskID, reason, options = {}) {
570
873
  ? `Re-plan or escalate ${taskID}; it has failed ${record.attempts} attempt(s).`
571
874
  : `Re-diagnose ${taskID} from fresh evidence before retrying.`
572
875
  await writeJson(loaded.paths.state, loaded.state)
876
+ await journal(loaded.paths, "task.failed", {
877
+ task: taskID,
878
+ attempts: record.attempts,
879
+ reason: cleanReason,
880
+ })
573
881
  return { taskID, attempts: record.attempts, state: loaded.state }
574
882
  })
575
883
  }
@@ -627,12 +935,12 @@ export async function resolveBlocker(root, slug, blocker) {
627
935
  })
628
936
  }
629
937
 
630
- export async function recordIntegrationVerification(root, slug, verdict, evidence, report = null) {
938
+ export async function recordIntegrationVerification(root, slug, verdict, evidence, report = null, options = {}) {
631
939
  const status = String(verdict || "").trim().toUpperCase()
632
940
  if (!["PASS", "FAIL", "PARTIAL"].includes(status)) {
633
941
  throw new Error("integration verdict must be PASS, FAIL, or PARTIAL")
634
942
  }
635
- const evidenceText = String(evidence || "").trim()
943
+ const evidenceText = String(evidence || options.receipt?.evidence || "").trim()
636
944
  if (!evidenceText) throw new Error("integration verification requires non-empty evidence")
637
945
 
638
946
  return withWorkLock(root, slug, async () => {
@@ -650,12 +958,37 @@ export async function recordIntegrationVerification(root, slug, verdict, evidenc
650
958
 
651
959
  const timestamp = now()
652
960
  const fingerprint = workspaceFingerprint(loaded.paths.root)
961
+ const policy = currentEvidencePolicy(loaded.state, loaded.plan)
962
+ const receipt = options.receipt || null
963
+ if (receipt) {
964
+ const validation = validateGateReceipt(receipt)
965
+ if (!validation.valid) {
966
+ const error = new Error("invalid integration verification receipt")
967
+ error.validation = validation
968
+ throw error
969
+ }
970
+ if (receipt.kind !== "integration-verification") throw new Error("integration verification requires an integration-verification receipt")
971
+ if (receipt.slug !== slug) throw new Error("integration verification receipt slug mismatch")
972
+ if (receipt.verdict !== status) throw new Error("integration verification receipt verdict mismatch")
973
+ if (receipt.workspaceFingerprint !== fingerprint) {
974
+ throw new Error("integration verification receipt workspace fingerprint mismatch")
975
+ }
976
+ } else if (status === "PASS" && policy.structuredIntegrationApproval) {
977
+ throw new Error("strict integration PASS requires a structured integration-verification receipt")
978
+ }
653
979
  loaded.state.integrationVerification = {
654
980
  status,
655
981
  at: timestamp,
656
982
  evidence: evidenceText,
657
983
  report: report ? String(report) : null,
658
984
  fingerprint,
985
+ receipt: receipt || null,
986
+ }
987
+ if (receipt) {
988
+ loaded.evidence.gateReceipts ??= []
989
+ if (!loaded.evidence.gateReceipts.some((item) => item.id === receipt.id)) {
990
+ loaded.evidence.gateReceipts.push({ ...receipt, recordedAt: timestamp })
991
+ }
659
992
  }
660
993
  loaded.state.updatedAt = timestamp
661
994
  loaded.state.status = status === "PASS" ? "ready-to-finalize" : "integration-verification"
@@ -676,6 +1009,11 @@ export async function recordIntegrationVerification(root, slug, verdict, evidenc
676
1009
 
677
1010
  await writeJson(loaded.paths.evidence, loaded.evidence)
678
1011
  await writeJson(loaded.paths.state, loaded.state)
1012
+ await journal(loaded.paths, "integration.verified", {
1013
+ verdict: status,
1014
+ fingerprint,
1015
+ receiptId: receipt?.id || null,
1016
+ })
679
1017
  return loaded.state.integrationVerification
680
1018
  })
681
1019
  }
@@ -720,6 +1058,7 @@ export async function finalizeWork(root, slug, evidence) {
720
1058
 
721
1059
  await writeJson(loaded.paths.evidence, loaded.evidence)
722
1060
  await writeJson(loaded.paths.state, loaded.state)
1061
+ await journal(loaded.paths, "work.finalized", { fingerprint: currentFingerprint })
723
1062
  return { state: loaded.state, evidence: loaded.evidence.entries.at(-1) }
724
1063
  })
725
1064
  }
@@ -736,7 +1075,34 @@ async function buildContextPack(loaded, taskID) {
736
1075
  dependencyReports[dep] = (await readFile(file, "utf8")).slice(0, 12000)
737
1076
  }
738
1077
 
739
- const contextManifest = await buildContextManifest(loaded.paths.root, task, { budget: 24_000 }).catch(() => null)
1078
+ const taskText = [
1079
+ task.title,
1080
+ task.summary,
1081
+ ...(task.acceptance || []),
1082
+ ...(task.verification || []),
1083
+ ].filter(Boolean).join(" ")
1084
+ const contextPolicy = classifyEngineeringTask(taskText, {
1085
+ changedFiles: taskFiles(task).length,
1086
+ risk: task.risk,
1087
+ })
1088
+ const attempt = Math.max(1, Number(loaded.state.tasks?.[taskID]?.attempts || 1))
1089
+ const recovery = recoveryPolicyForAttempt(contextPolicy, attempt)
1090
+ const effectiveContextPolicy = {
1091
+ ...contextPolicy,
1092
+ contextBudget: recovery.contextBudget,
1093
+ maxSkills: recovery.maxSkills,
1094
+ effectiveContextBudget: recovery.contextBudget,
1095
+ effectiveMaxSkills: recovery.maxSkills,
1096
+ recovery,
1097
+ }
1098
+ const contextManifest = await buildContextManifest(
1099
+ loaded.paths.root,
1100
+ task,
1101
+ {
1102
+ budget: recovery.contextBudget,
1103
+ strategy: recovery.contextStrategy,
1104
+ },
1105
+ ).catch(() => null)
740
1106
  const learnings = await relevantAcceptedLearnings(
741
1107
  loaded.paths.root,
742
1108
  [task.title, task.summary, ...(task.acceptance || [])].join(" "),
@@ -744,6 +1110,8 @@ async function buildContextPack(loaded, taskID) {
744
1110
 
745
1111
  return {
746
1112
  schemaVersion: STATE_SCHEMA,
1113
+ contextPolicy: effectiveContextPolicy,
1114
+ attempt,
747
1115
  slug: loaded.state.slug,
748
1116
  task,
749
1117
  taskBrief: path.relative(loaded.paths.root, path.join(loaded.paths.tasks, `${taskID}.md`)).replaceAll("\\", "/"),
@@ -757,14 +1125,174 @@ async function buildContextPack(loaded, taskID) {
757
1125
  workingState: {
758
1126
  status: loaded.state.status,
759
1127
  task: loaded.state.tasks?.[taskID],
1128
+ checkpoint: loaded.state.checkpoint || null,
760
1129
  },
761
1130
  }
762
1131
  }
763
1132
 
1133
+ export async function createPlanVerificationReceipt(root, slug, input = {}) {
1134
+ const loaded = await loadWork(root, slug)
1135
+ if (!loaded.plan) throw new Error("PLAN.json is missing")
1136
+ const hash = planHash(loaded.plan)
1137
+ return createGateReceipt({
1138
+ kind: "plan-verification",
1139
+ slug,
1140
+ verdict: input.verdict || "PASS",
1141
+ verifier: input.verifier || "ues-plan-checker",
1142
+ sessionID: input.sessionID || null,
1143
+ runId: input.runId || null,
1144
+ planHash: hash,
1145
+ evidence: input.evidence,
1146
+ report: input.report,
1147
+ })
1148
+ }
1149
+
1150
+ export async function createIntegrationVerificationReceipt(root, slug, input = {}) {
1151
+ await loadWork(root, slug)
1152
+ return createGateReceipt({
1153
+ kind: "integration-verification",
1154
+ slug,
1155
+ verdict: input.verdict || "PASS",
1156
+ verifier: input.verifier || "ues-integration-verifier",
1157
+ sessionID: input.sessionID || null,
1158
+ runId: input.runId || null,
1159
+ workspaceFingerprint: workspaceFingerprint(root),
1160
+ evidence: input.evidence,
1161
+ report: input.report,
1162
+ })
1163
+ }
1164
+
764
1165
  export async function contextPack(root, slug, taskID) {
765
1166
  return buildContextPack(await loadWork(root, slug), taskID)
766
1167
  }
767
1168
 
1169
+ function checkpointEvidencePointers(evidence = {}) {
1170
+ const pointers = []
1171
+ for (const entry of (evidence.entries || []).slice(-12)) {
1172
+ pointers.push({
1173
+ kind: "evidence-entry",
1174
+ task: entry.task || null,
1175
+ at: entry.at || entry.recordedAt || null,
1176
+ runId: entry.runId || null,
1177
+ })
1178
+ }
1179
+ for (const receipt of (evidence.receipts || []).slice(-12)) {
1180
+ pointers.push({
1181
+ kind: "verification-receipt",
1182
+ id: receipt.id || null,
1183
+ task: receipt.task || null,
1184
+ runId: receipt.runId || null,
1185
+ verdict: receipt.verdict || null,
1186
+ })
1187
+ }
1188
+ for (const receipt of (evidence.gateReceipts || []).slice(-8)) {
1189
+ pointers.push({
1190
+ kind: "gate-receipt",
1191
+ id: receipt.id || null,
1192
+ receiptKind: receipt.kind || null,
1193
+ verdict: receipt.verdict || null,
1194
+ })
1195
+ }
1196
+ return pointers.slice(-24)
1197
+ }
1198
+
1199
+ function checkpointNextAction(loaded, taskID, runId) {
1200
+ const record = taskID ? loaded.state.tasks?.[taskID] : null
1201
+ if (record?.status === "running") {
1202
+ return {
1203
+ type: "continue-task",
1204
+ taskId: taskID,
1205
+ runId: runId || record.runId || null,
1206
+ instruction: "Continue the active scoped task from durable evidence; execute the next required tool/action before narrative summary.",
1207
+ }
1208
+ }
1209
+
1210
+ const ready = readyTasks(loaded.plan, loaded.state)
1211
+ if (ready.length) {
1212
+ const nextTask = ready[0]
1213
+ return {
1214
+ type: "dispatch-task",
1215
+ taskId: nextTask,
1216
+ attempt: Number(loaded.state.tasks?.[nextTask]?.attempts || 0) + 1,
1217
+ instruction: "Dispatch the next ready task in a fresh executor from .ues-work.",
1218
+ }
1219
+ }
1220
+
1221
+ return {
1222
+ type: "inspect-state",
1223
+ instruction: loaded.state.nextAction || "Inspect durable work state and choose the next deterministic action.",
1224
+ }
1225
+ }
1226
+
1227
+ export async function checkpointWork(root, slug, options = {}) {
1228
+ return withWorkLock(root, slug, async () => {
1229
+ const loaded = await loadWork(root, slug)
1230
+ const taskID = String(options.taskID || "").trim() ||
1231
+ Object.entries(loaded.state.tasks || {}).find(([, value]) => value.status === "running")?.[0] ||
1232
+ null
1233
+ const record = taskID ? loaded.state.tasks?.[taskID] : null
1234
+ if (taskID && !record) throw new Error(`task '${taskID}' is not tracked`)
1235
+ if (record?.status === "running") assertRunFence(record, { runId: options.runId || null })
1236
+
1237
+ const timestamp = now()
1238
+ const checkpoint = {
1239
+ schemaVersion: 1,
1240
+ reason: String(options.reason || "runtime-checkpoint"),
1241
+ createdAt: timestamp,
1242
+ currentTaskId: taskID,
1243
+ runId: record?.runId || options.runId || null,
1244
+ planHash: loaded.plan ? planHash(loaded.plan) : loaded.state.planHash || null,
1245
+ nextAction: checkpointNextAction(loaded, taskID, record?.runId || options.runId || null),
1246
+ workspaceFingerprint: workspaceFingerprint(loaded.paths.root),
1247
+ evidencePointers: checkpointEvidencePointers(loaded.evidence),
1248
+ resumedAt: null,
1249
+ }
1250
+
1251
+ loaded.state.schemaVersion = STATE_SCHEMA
1252
+ loaded.state.checkpoint = checkpoint
1253
+ loaded.state.updatedAt = timestamp
1254
+ await writeJson(loaded.paths.state, loaded.state)
1255
+ await journal(loaded.paths, "work.checkpoint", {
1256
+ task: taskID,
1257
+ runId: checkpoint.runId,
1258
+ planHash: checkpoint.planHash,
1259
+ reason: checkpoint.reason,
1260
+ nextAction: checkpoint.nextAction,
1261
+ })
1262
+ return checkpoint
1263
+ })
1264
+ }
1265
+
1266
+ export async function markCheckpointResumed(root, slug, options = {}) {
1267
+ return withWorkLock(root, slug, async () => {
1268
+ const loaded = await loadWork(root, slug)
1269
+ const checkpoint = loaded.state.checkpoint
1270
+ if (!checkpoint) return null
1271
+ if (options.taskID && checkpoint.currentTaskId && checkpoint.currentTaskId !== options.taskID) {
1272
+ throw new Error("checkpoint task mismatch")
1273
+ }
1274
+ if (checkpoint.runId && options.runId && checkpoint.runId !== options.runId) {
1275
+ throw new Error("checkpoint runId mismatch")
1276
+ }
1277
+ const timestamp = now()
1278
+ checkpoint.resumedAt = timestamp
1279
+ checkpoint.resumeReason = String(options.reason || "deterministic-action-observed")
1280
+ loaded.state.updatedAt = timestamp
1281
+ await writeJson(loaded.paths.state, loaded.state)
1282
+ await journal(loaded.paths, "work.checkpoint-resumed", {
1283
+ task: checkpoint.currentTaskId || null,
1284
+ runId: checkpoint.runId || null,
1285
+ reason: checkpoint.resumeReason,
1286
+ })
1287
+ return checkpoint
1288
+ })
1289
+ }
1290
+
1291
+ export async function runtimeEvents(root, slug, options = {}) {
1292
+ const paths = workPaths(root, slug)
1293
+ return readRuntimeEvents(paths.events, options)
1294
+ }
1295
+
768
1296
  export async function resumeWork(root, slug) {
769
1297
  await recoverStaleTasks(root, slug).catch(() => ({ recovered: [] }))
770
1298
  const loaded = await loadWork(root, slug)
@@ -773,5 +1301,6 @@ export async function resumeWork(root, slug) {
773
1301
  planAnalysis: loaded.plan ? analyzePlan(loaded.plan) : null,
774
1302
  completedEvidence: loaded.evidence.entries || [],
775
1303
  decisions: loaded.state.decisions || [],
1304
+ recentEvents: await readRuntimeEvents(loaded.paths.events, { limit: 50 }),
776
1305
  }
777
1306
  }