opencode-agent-skill 7.7.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 (206) hide show
  1. package/CHANGELOG.md +163 -0
  2. package/LICENSE +9 -0
  3. package/README.md +581 -0
  4. package/bin/ocskill.mjs +975 -0
  5. package/docs/DETERMINISTIC-TOOLS.md +88 -0
  6. package/docs/ENGINEERING-DESIGN.md +176 -0
  7. package/docs/EVALS.md +136 -0
  8. package/docs/NPM-PUBLISH.md +102 -0
  9. package/docs/OPENCODE-COMPAT.md +109 -0
  10. package/docs/RESEARCH-SOURCES.md +37 -0
  11. package/docs/TRACE-SCHEMA.md +109 -0
  12. package/docs/V7-INTELLIGENCE-RUNTIME.md +166 -0
  13. package/evals/live/fixtures/engineering-bench/package.json +1 -0
  14. package/evals/live/fixtures/engineering-bench/src/api-errors.mjs +3 -0
  15. package/evals/live/fixtures/engineering-bench/src/authz.mjs +3 -0
  16. package/evals/live/fixtures/engineering-bench/src/cache-tags.mjs +3 -0
  17. package/evals/live/fixtures/engineering-bench/src/config.mjs +3 -0
  18. package/evals/live/fixtures/engineering-bench/src/contract-consumer.mjs +6 -0
  19. package/evals/live/fixtures/engineering-bench/src/contract-producer.mjs +3 -0
  20. package/evals/live/fixtures/engineering-bench/src/dedupe.mjs +3 -0
  21. package/evals/live/fixtures/engineering-bench/src/dependency.mjs +3 -0
  22. package/evals/live/fixtures/engineering-bench/src/discount.mjs +3 -0
  23. package/evals/live/fixtures/engineering-bench/src/inventory.mjs +3 -0
  24. package/evals/live/fixtures/engineering-bench/src/migration.mjs +3 -0
  25. package/evals/live/fixtures/engineering-bench/src/money.mjs +3 -0
  26. package/evals/live/fixtures/engineering-bench/src/pagination.mjs +5 -0
  27. package/evals/live/fixtures/engineering-bench/src/path-safe.mjs +5 -0
  28. package/evals/live/fixtures/engineering-bench/src/payment.mjs +5 -0
  29. package/evals/live/fixtures/engineering-bench/src/query-sort.mjs +3 -0
  30. package/evals/live/fixtures/engineering-bench/src/react-state.mjs +8 -0
  31. package/evals/live/fixtures/engineering-bench/src/retry.mjs +3 -0
  32. package/evals/live/fixtures/engineering-bench/src/rn-platform.mjs +3 -0
  33. package/evals/live/fixtures/engineering-bench/src/upload.mjs +3 -0
  34. package/evals/live/fixtures/engineering-bench/src/webhook.mjs +3 -0
  35. package/evals/live/graders/engineering-bench.mjs +239 -0
  36. package/evals/live/tasks.json +126 -0
  37. package/evals/long/fixtures/long-horizon/package.json +1 -0
  38. package/evals/long/fixtures/long-horizon/src/auth.mjs +5 -0
  39. package/evals/long/fixtures/long-horizon/src/checkout.mjs +9 -0
  40. package/evals/long/fixtures/long-horizon/src/inventory.mjs +5 -0
  41. package/evals/long/fixtures/long-horizon/src/money.mjs +3 -0
  42. package/evals/long/fixtures/long-horizon/src/payment.mjs +7 -0
  43. package/evals/long/fixtures/long-horizon/src/product-api.mjs +10 -0
  44. package/evals/long/fixtures/long-horizon/src/product-cache.mjs +3 -0
  45. package/evals/long/fixtures/long-horizon/src/product-service.mjs +6 -0
  46. package/evals/long/fixtures/long-horizon/src/product-state.mjs +8 -0
  47. package/evals/long/fixtures/long-horizon/src/project-api.mjs +9 -0
  48. package/evals/long/fixtures/long-horizon/src/project-service.mjs +7 -0
  49. package/evals/long/fixtures/long-horizon/src/user-consumer.mjs +3 -0
  50. package/evals/long/fixtures/long-horizon/src/user-migration.mjs +7 -0
  51. package/evals/long/fixtures/long-horizon/src/user-serializer.mjs +3 -0
  52. package/evals/long/fixtures/long-horizon/src/user-validation.mjs +3 -0
  53. package/evals/long/graders/long-horizon.mjs +209 -0
  54. package/evals/long/tasks.json +36 -0
  55. package/evals/router-triggers.json +1238 -0
  56. package/evals/routing.json +321 -0
  57. package/global-config/AGENTS.md +212 -0
  58. package/global-config/agents/architect.md +38 -0
  59. package/global-config/agents/codebase-mapper.md +41 -0
  60. package/global-config/agents/critic.md +37 -0
  61. package/global-config/agents/debugger.md +36 -0
  62. package/global-config/agents/executor.md +48 -0
  63. package/global-config/agents/integration-verifier.md +43 -0
  64. package/global-config/agents/plan-checker.md +43 -0
  65. package/global-config/agents/researcher.md +30 -0
  66. package/global-config/agents/reviewer.md +30 -0
  67. package/global-config/agents/verifier.md +33 -0
  68. package/global-config/commands/audit.md +8 -0
  69. package/global-config/commands/critique.md +8 -0
  70. package/global-config/commands/debug.md +8 -0
  71. package/global-config/commands/feature.md +8 -0
  72. package/global-config/commands/fix.md +8 -0
  73. package/global-config/commands/plan.md +8 -0
  74. package/global-config/commands/research.md +8 -0
  75. package/global-config/commands/resume.md +22 -0
  76. package/global-config/commands/review.md +8 -0
  77. package/global-config/commands/run.md +29 -0
  78. package/global-config/commands/verify.md +8 -0
  79. package/global-config/plugins/ues-router/capabilities.js +20 -0
  80. package/global-config/plugins/ues-router/index.js +277 -0
  81. package/global-config/plugins/ues-router/router.js +74 -0
  82. package/global-config/plugins/ues-router/safety.js +16 -0
  83. package/global-config/skills/accessibility/SKILL.md +10 -0
  84. package/global-config/skills/accessibility/references/workflow.md +17 -0
  85. package/global-config/skills/api-contract/SKILL.md +12 -0
  86. package/global-config/skills/api-contract/references/workflow.md +17 -0
  87. package/global-config/skills/auth-security/SKILL.md +12 -0
  88. package/global-config/skills/auth-security/references/workflow.md +15 -0
  89. package/global-config/skills/bug-diagnosis/SKILL.md +28 -0
  90. package/global-config/skills/change-impact-analysis/SKILL.md +23 -0
  91. package/global-config/skills/code-review/SKILL.md +19 -0
  92. package/global-config/skills/context-engineering/SKILL.md +18 -0
  93. package/global-config/skills/context-engineering/references/large-repo.md +16 -0
  94. package/global-config/skills/database-engineering/SKILL.md +12 -0
  95. package/global-config/skills/database-engineering/references/workflow.md +15 -0
  96. package/global-config/skills/dependency-management/SKILL.md +12 -0
  97. package/global-config/skills/dependency-management/references/workflow.md +14 -0
  98. package/global-config/skills/devops-engineering/SKILL.md +10 -0
  99. package/global-config/skills/devops-engineering/references/workflow.md +11 -0
  100. package/global-config/skills/django-engineering/SKILL.md +10 -0
  101. package/global-config/skills/django-engineering/references/workflow.md +11 -0
  102. package/global-config/skills/documentation-engineering/SKILL.md +10 -0
  103. package/global-config/skills/documentation-engineering/references/workflow.md +15 -0
  104. package/global-config/skills/dotnet-engineering/SKILL.md +10 -0
  105. package/global-config/skills/dotnet-engineering/references/workflow.md +11 -0
  106. package/global-config/skills/ecommerce-engineering/SKILL.md +10 -0
  107. package/global-config/skills/ecommerce-engineering/references/workflow.md +17 -0
  108. package/global-config/skills/engineering-orchestrator/SKILL.md +29 -0
  109. package/global-config/skills/engineering-orchestrator/references/delegation.md +22 -0
  110. package/global-config/skills/engineering-orchestrator/references/evaluator-loop.md +18 -0
  111. package/global-config/skills/engineering-orchestrator/references/long-horizon.md +57 -0
  112. package/global-config/skills/engineering-orchestrator/references/model-escalation.md +19 -0
  113. package/global-config/skills/engineering-orchestrator/references/retry-policy.md +12 -0
  114. package/global-config/skills/engineering-orchestrator/references/routing.md +39 -0
  115. package/global-config/skills/engineering-orchestrator/references/verification-matrix.md +18 -0
  116. package/global-config/skills/fastapi-engineering/SKILL.md +10 -0
  117. package/global-config/skills/fastapi-engineering/references/workflow.md +13 -0
  118. package/global-config/skills/file-upload-engineering/SKILL.md +10 -0
  119. package/global-config/skills/file-upload-engineering/references/workflow.md +17 -0
  120. package/global-config/skills/flutter-engineering/SKILL.md +10 -0
  121. package/global-config/skills/flutter-engineering/references/workflow.md +13 -0
  122. package/global-config/skills/git-safety/SKILL.md +10 -0
  123. package/global-config/skills/git-safety/references/workflow.md +15 -0
  124. package/global-config/skills/implementation-engineer/SKILL.md +10 -0
  125. package/global-config/skills/implementation-engineer/references/workflow.md +14 -0
  126. package/global-config/skills/java-spring-engineering/SKILL.md +10 -0
  127. package/global-config/skills/java-spring-engineering/references/workflow.md +13 -0
  128. package/global-config/skills/long-task-state/SKILL.md +28 -0
  129. package/global-config/skills/long-task-state/references/context-ledger.md +27 -0
  130. package/global-config/skills/long-task-state/templates/STATE.md +48 -0
  131. package/global-config/skills/nestjs-engineering/SKILL.md +10 -0
  132. package/global-config/skills/nestjs-engineering/references/workflow.md +11 -0
  133. package/global-config/skills/nextjs-engineering/SKILL.md +12 -0
  134. package/global-config/skills/nextjs-engineering/references/workflow.md +15 -0
  135. package/global-config/skills/nodejs-engineering/SKILL.md +12 -0
  136. package/global-config/skills/nodejs-engineering/references/workflow.md +11 -0
  137. package/global-config/skills/payment-engineering/SKILL.md +12 -0
  138. package/global-config/skills/payment-engineering/references/workflow.md +19 -0
  139. package/global-config/skills/performance-engineering/SKILL.md +10 -0
  140. package/global-config/skills/performance-engineering/references/workflow.md +17 -0
  141. package/global-config/skills/python-engineering/SKILL.md +10 -0
  142. package/global-config/skills/python-engineering/references/workflow.md +11 -0
  143. package/global-config/skills/react-engineering/SKILL.md +14 -0
  144. package/global-config/skills/react-engineering/references/workflow.md +16 -0
  145. package/global-config/skills/react-native-engineering/SKILL.md +14 -0
  146. package/global-config/skills/react-native-engineering/references/workflow.md +16 -0
  147. package/global-config/skills/repo-explorer/SKILL.md +17 -0
  148. package/global-config/skills/research-verification/SKILL.md +18 -0
  149. package/global-config/skills/research-verification/references/source-hierarchy.md +12 -0
  150. package/global-config/skills/rest-api-design/SKILL.md +10 -0
  151. package/global-config/skills/rest-api-design/references/workflow.md +18 -0
  152. package/global-config/skills/software-architect/SKILL.md +10 -0
  153. package/global-config/skills/software-architect/references/workflow.md +18 -0
  154. package/global-config/skills/task-planner/SKILL.md +21 -0
  155. package/global-config/skills/task-planner/references/plan-schema.md +56 -0
  156. package/global-config/skills/test-driven-development/SKILL.md +22 -0
  157. package/global-config/skills/test-driven-development/references/writing-good-tests.md +21 -0
  158. package/global-config/skills/test-verification/SKILL.md +20 -0
  159. package/global-config/skills/ui-ux-engineering/SKILL.md +10 -0
  160. package/global-config/skills/ui-ux-engineering/references/workflow.md +17 -0
  161. package/global-config/skills/web-security-review/SKILL.md +10 -0
  162. package/global-config/skills/web-security-review/references/workflow.md +22 -0
  163. package/lib/context-manifest.mjs +101 -0
  164. package/lib/control-center.mjs +148 -0
  165. package/lib/eval-auth.mjs +21 -0
  166. package/lib/eval-report.mjs +72 -0
  167. package/lib/eval-telemetry.mjs +155 -0
  168. package/lib/evidence-receipt.mjs +50 -0
  169. package/lib/hermes-bridge.mjs +28 -0
  170. package/lib/ids.mjs +21 -0
  171. package/lib/installer.mjs +636 -0
  172. package/lib/learning-engine.mjs +161 -0
  173. package/lib/model-config.mjs +88 -0
  174. package/lib/model-policy.mjs +71 -0
  175. package/lib/opencode-compat.mjs +86 -0
  176. package/lib/orchestrator-policy.mjs +35 -0
  177. package/lib/process-runner.mjs +117 -0
  178. package/lib/repo-graph.mjs +135 -0
  179. package/lib/repo-inspect.mjs +264 -0
  180. package/lib/review-scope.mjs +98 -0
  181. package/lib/router-config.mjs +40 -0
  182. package/lib/task-engine.mjs +777 -0
  183. package/lib/task-graph.mjs +262 -0
  184. package/lib/update-resolver.mjs +43 -0
  185. package/lib/verification-plan.mjs +52 -0
  186. package/lib/version.mjs +47 -0
  187. package/lib/workspace-snapshot.mjs +45 -0
  188. package/lib/worktree-sandbox.mjs +59 -0
  189. package/package.json +69 -0
  190. package/scripts/check-working-tree.mjs +2 -0
  191. package/scripts/collect-evidence.mjs +2 -0
  192. package/scripts/control-center.mjs +37 -0
  193. package/scripts/detect-stack.mjs +2 -0
  194. package/scripts/detect-test-commands.mjs +2 -0
  195. package/scripts/eval-live.mjs +437 -0
  196. package/scripts/eval-report.mjs +49 -0
  197. package/scripts/eval-router.mjs +45 -0
  198. package/scripts/eval-skills.mjs +71 -0
  199. package/scripts/impact-map.mjs +4 -0
  200. package/scripts/install.mjs +61 -0
  201. package/scripts/repo-map.mjs +2 -0
  202. package/scripts/smoke-packed-install.mjs +326 -0
  203. package/scripts/syntax-check.mjs +35 -0
  204. package/scripts/uninstall.mjs +21 -0
  205. package/scripts/validate-live-suite.mjs +65 -0
  206. package/scripts/validate.mjs +133 -0
@@ -0,0 +1,777 @@
1
+ import { existsSync } from "node:fs"
2
+ import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises"
3
+ import { spawnSync } from "node:child_process"
4
+ import { createHash, randomUUID } from "node:crypto"
5
+ import path from "node:path"
6
+ import { analyzePlan, taskFiles, validatePlan } from "./task-graph.mjs"
7
+ import { buildContextManifest } from "./context-manifest.mjs"
8
+ import { relevantAcceptedLearnings } from "./learning-engine.mjs"
9
+ import { validateVerificationReceipt } from "./evidence-receipt.mjs"
10
+
11
+ const WORK_DIR = ".ues-work"
12
+ const STATE_SCHEMA = 3
13
+ const LOCK_TIMEOUT_MS = 30_000
14
+ const LOCK_STALE_MS = 120_000
15
+ const DEFAULT_TASK_LEASE_MS = 10 * 60_000
16
+
17
+ function now() {
18
+ return new Date().toISOString()
19
+ }
20
+
21
+ function sleep(ms) {
22
+ return new Promise((resolve) => setTimeout(resolve, ms))
23
+ }
24
+
25
+ export function validWorkSlug(value) {
26
+ return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(String(value || ""))
27
+ }
28
+
29
+ export function workPaths(root, slug) {
30
+ root = path.resolve(root)
31
+ if (!validWorkSlug(slug)) throw new Error("work slug must use lowercase letters, numbers and dashes")
32
+ const dir = path.join(root, WORK_DIR, slug)
33
+ return {
34
+ root,
35
+ dir,
36
+ spec: path.join(dir, "SPEC.md"),
37
+ plan: path.join(dir, "PLAN.json"),
38
+ state: path.join(dir, "STATE.json"),
39
+ evidence: path.join(dir, "EVIDENCE.json"),
40
+ tasks: path.join(dir, "tasks"),
41
+ reports: path.join(dir, "reports"),
42
+ lock: path.join(dir, ".state-lock"),
43
+ }
44
+ }
45
+
46
+ async function readJson(file, fallback = null) {
47
+ try {
48
+ return JSON.parse(await readFile(file, "utf8"))
49
+ } catch (error) {
50
+ if (error?.code === "ENOENT") return fallback
51
+ throw error
52
+ }
53
+ }
54
+
55
+ async function atomicWrite(file, content) {
56
+ const temp = file + "." + process.pid + "." + Date.now() + ".tmp"
57
+ await writeFile(temp, content, "utf8")
58
+ try {
59
+ await rename(temp, file)
60
+ } catch (error) {
61
+ await rm(temp, { force: true }).catch(() => {})
62
+ throw error
63
+ }
64
+ }
65
+
66
+ async function writeJson(file, value) {
67
+ await atomicWrite(file, JSON.stringify(value, null, 2) + "\n")
68
+ }
69
+
70
+ async function acquireWorkLock(paths) {
71
+ const started = Date.now()
72
+ while (true) {
73
+ try {
74
+ await mkdir(paths.lock)
75
+ await writeFile(
76
+ path.join(paths.lock, "owner.json"),
77
+ JSON.stringify({ pid: process.pid, at: now() }) + "\n",
78
+ "utf8",
79
+ ).catch(() => {})
80
+ return
81
+ } catch (error) {
82
+ if (error?.code !== "EEXIST") throw error
83
+ const info = await stat(paths.lock).catch(() => null)
84
+ if (info && Date.now() - info.mtimeMs > LOCK_STALE_MS) {
85
+ await rm(paths.lock, { recursive: true, force: true }).catch(() => {})
86
+ continue
87
+ }
88
+ if (Date.now() - started >= LOCK_TIMEOUT_MS) {
89
+ throw new Error(`timed out waiting for UES state lock: ${paths.lock}`)
90
+ }
91
+ await sleep(25)
92
+ }
93
+ }
94
+ }
95
+
96
+ async function withWorkLock(root, slug, fn) {
97
+ const paths = workPaths(root, slug)
98
+ await acquireWorkLock(paths)
99
+ try {
100
+ return await fn(paths)
101
+ } finally {
102
+ await rm(paths.lock, { recursive: true, force: true }).catch(() => {})
103
+ }
104
+ }
105
+
106
+ export function planHash(plan) {
107
+ return createHash("sha256").update(JSON.stringify(plan)).digest("hex")
108
+ }
109
+
110
+ function gitCapture(root, args) {
111
+ return spawnSync("git", args, {
112
+ cwd: root,
113
+ encoding: "utf8",
114
+ maxBuffer: 16 * 1024 * 1024,
115
+ })
116
+ }
117
+
118
+ export function workspaceFingerprint(root) {
119
+ root = path.resolve(root)
120
+ const inside = gitCapture(root, ["rev-parse", "--is-inside-work-tree"])
121
+ if (inside.status !== 0) {
122
+ return createHash("sha256").update("non-git:" + root).digest("hex")
123
+ }
124
+
125
+ const parts = []
126
+ const commands = [
127
+ ["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"],
131
+ ]
132
+ for (const args of commands) {
133
+ const result = gitCapture(root, args)
134
+ parts.push(result.status === 0 ? result.stdout : "ERROR:" + (result.stderr || result.stdout || ""))
135
+ }
136
+ return createHash("sha256").update(parts.join("\n---UES-FP---\n")).digest("hex")
137
+ }
138
+
139
+ export async function initWork(root, slug, goal) {
140
+ const paths = workPaths(root, slug)
141
+ if (existsSync(paths.state)) throw new Error(`work '${slug}' already exists`)
142
+
143
+ await mkdir(paths.tasks, { recursive: true })
144
+ await mkdir(paths.reports, { recursive: true })
145
+ const createdAt = now()
146
+ const cleanGoal = String(goal || "").trim() || "Define the requested engineering outcome."
147
+
148
+ await atomicWrite(
149
+ paths.spec,
150
+ `# ${slug}\n\n## Goal\n\n${cleanGoal}\n\n## Acceptance criteria\n\n- Replace this line with observable acceptance criteria before execution.\n\n## Constraints\n\n- Preserve unrelated user changes.\n`,
151
+ )
152
+ await writeJson(paths.state, {
153
+ schemaVersion: STATE_SCHEMA,
154
+ slug,
155
+ goal: cleanGoal,
156
+ status: "planning",
157
+ createdAt,
158
+ updatedAt: createdAt,
159
+ planImportedAt: null,
160
+ planHash: null,
161
+ planApproval: null,
162
+ integrationVerification: null,
163
+ tasks: {},
164
+ decisions: [],
165
+ blockers: [],
166
+ nextAction: "Complete SPEC.md, create PLAN.json, then run ocskill work plan.",
167
+ })
168
+ await writeJson(paths.evidence, {
169
+ schemaVersion: STATE_SCHEMA,
170
+ slug,
171
+ updatedAt: createdAt,
172
+ entries: [],
173
+ receipts: [],
174
+ })
175
+
176
+ return { ...paths, slug, goal: cleanGoal }
177
+ }
178
+
179
+ export async function loadWork(root, slug) {
180
+ const paths = workPaths(root, slug)
181
+ const state = await readJson(paths.state)
182
+ if (!state) throw new Error(`work '${slug}' does not exist`)
183
+ const plan = await readJson(paths.plan)
184
+ const evidence = await readJson(paths.evidence, { schemaVersion: STATE_SCHEMA, slug, entries: [] })
185
+ return { paths, state, plan, evidence }
186
+ }
187
+
188
+ function taskBrief(task) {
189
+ const files = taskFiles(task)
190
+ 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`
191
+ }
192
+
193
+ function planIsApproved(state, plan) {
194
+ if (!plan) return false
195
+ const hash = state.planHash || planHash(plan)
196
+ return state.planApproval?.status === "passed" && state.planApproval?.planHash === hash
197
+ }
198
+
199
+ export async function importPlan(root, slug, planInput) {
200
+ const plan = typeof planInput === "string"
201
+ ? JSON.parse(await readFile(path.resolve(planInput), "utf8"))
202
+ : planInput
203
+
204
+ const validation = validatePlan(plan)
205
+ if (!validation.valid) {
206
+ const error = new Error("plan validation failed")
207
+ error.validation = validation
208
+ throw error
209
+ }
210
+
211
+ const analysis = analyzePlan(plan)
212
+ return withWorkLock(root, slug, async () => {
213
+ const loaded = await loadWork(root, slug)
214
+ if (Object.values(loaded.state.tasks || {}).some((record) => record.status === "running")) {
215
+ throw new Error("cannot replace PLAN.json while a task is running")
216
+ }
217
+
218
+ await writeJson(loaded.paths.plan, plan)
219
+ const hash = planHash(plan)
220
+ const stateTasks = {}
221
+ for (const task of plan.tasks) {
222
+ stateTasks[task.id] = {
223
+ status: "pending",
224
+ attempts: 0,
225
+ startedAt: null,
226
+ completedAt: null,
227
+ lastFailure: null,
228
+ report: null,
229
+ runId: null,
230
+ owner: null,
231
+ heartbeatAt: null,
232
+ leaseExpiresAt: null,
233
+ evidenceStrength: null,
234
+ }
235
+ await atomicWrite(path.join(loaded.paths.tasks, `${task.id}.md`), taskBrief(task))
236
+ }
237
+
238
+ const updatedAt = now()
239
+ const state = {
240
+ ...loaded.state,
241
+ schemaVersion: STATE_SCHEMA,
242
+ goal: plan.goal,
243
+ status: "awaiting-plan-approval",
244
+ updatedAt,
245
+ planImportedAt: updatedAt,
246
+ planHash: hash,
247
+ planApproval: { status: "pending", planHash: hash, at: updatedAt, evidence: null },
248
+ integrationVerification: null,
249
+ tasks: stateTasks,
250
+ nextAction: "Run ues-plan-checker, then record PASS with 'ocskill work approve-plan'.",
251
+ }
252
+ await writeJson(loaded.paths.state, state)
253
+ return { plan, analysis, state, paths: loaded.paths }
254
+ })
255
+ }
256
+
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")
260
+
261
+ return withWorkLock(root, slug, async () => {
262
+ const loaded = await loadWork(root, slug)
263
+ if (!loaded.plan) throw new Error("PLAN.json is missing")
264
+ const hash = planHash(loaded.plan)
265
+ if (loaded.state.planHash && loaded.state.planHash !== hash) {
266
+ throw new Error("PLAN.json changed after import; re-import it before approval")
267
+ }
268
+
269
+ const timestamp = now()
270
+ loaded.state.schemaVersion = STATE_SCHEMA
271
+ loaded.state.planHash = hash
272
+ loaded.state.planApproval = {
273
+ status: "passed",
274
+ planHash: hash,
275
+ at: timestamp,
276
+ evidence: evidenceText,
277
+ }
278
+ loaded.state.status = "ready"
279
+ loaded.state.updatedAt = timestamp
280
+ const next = readyTasks(loaded.plan, loaded.state)
281
+ loaded.state.nextAction = next.length
282
+ ? `Execute ready task(s): ${next.join(", ")}`
283
+ : "No executable task; inspect dependencies and blockers."
284
+ await writeJson(loaded.paths.state, loaded.state)
285
+ return { planHash: hash, approval: loaded.state.planApproval, ready: next }
286
+ })
287
+ }
288
+
289
+ function taskByID(plan, id) {
290
+ const task = plan?.tasks?.find((item) => item.id === id)
291
+ if (!task) throw new Error(`unknown task '${id}'`)
292
+ return task
293
+ }
294
+
295
+ function completedSet(state) {
296
+ return new Set(Object.entries(state.tasks || {}).filter(([, value]) => value.status === "completed").map(([id]) => id))
297
+ }
298
+
299
+ export function readyTasks(plan, state) {
300
+ if (!plan || !planIsApproved(state, plan)) return []
301
+ const completed = completedSet(state)
302
+ const runningFiles = new Set()
303
+ for (const task of plan.tasks) {
304
+ if (state.tasks?.[task.id]?.status === "running") {
305
+ for (const file of taskFiles(task)) runningFiles.add(file)
306
+ }
307
+ }
308
+
309
+ const ready = []
310
+ for (const task of plan.tasks) {
311
+ const status = state.tasks?.[task.id]?.status
312
+ if (!["pending", "failed"].includes(status)) continue
313
+ if (!(task.dependsOn || []).every((dep) => completed.has(dep))) continue
314
+ const files = taskFiles(task)
315
+ if (files.length === 0 && runningFiles.size > 0) continue
316
+ if (files.some((file) => runningFiles.has(file))) continue
317
+ ready.push(task.id)
318
+ }
319
+ return ready
320
+ }
321
+
322
+ export async function workStatus(root, slug) {
323
+ const loaded = await loadWork(root, slug)
324
+ const counts = { pending: 0, running: 0, completed: 0, failed: 0 }
325
+ for (const value of Object.values(loaded.state.tasks || {})) {
326
+ if (Object.hasOwn(counts, value.status)) counts[value.status] += 1
327
+ }
328
+ const running = Object.entries(loaded.state.tasks || {})
329
+ .filter(([, value]) => value.status === "running")
330
+ .map(([taskID, value]) => ({
331
+ taskID,
332
+ runId: value.runId || null,
333
+ attempts: value.attempts || 0,
334
+ heartbeatAt: value.heartbeatAt || null,
335
+ leaseExpiresAt: value.leaseExpiresAt || null,
336
+ }))
337
+ const taskEntries = Object.values(loaded.state.tasks || {})
338
+ const receiptBacked = taskEntries.filter((value) => value.evidenceStrength === "receipt-backed").length
339
+ const completed = taskEntries.filter((value) => value.status === "completed").length
340
+
341
+ return {
342
+ slug,
343
+ root: loaded.paths.root,
344
+ dir: loaded.paths.dir,
345
+ status: loaded.state.status,
346
+ goal: loaded.state.goal,
347
+ counts,
348
+ ready: readyTasks(loaded.plan, loaded.state),
349
+ running,
350
+ blockers: loaded.state.blockers || [],
351
+ planApproval: loaded.state.planApproval || null,
352
+ integrationVerification: loaded.state.integrationVerification || null,
353
+ evidence: {
354
+ receiptBacked,
355
+ completed,
356
+ coverage: completed ? receiptBacked / completed : 0,
357
+ receipts: (loaded.evidence.receipts || []).length,
358
+ },
359
+ nextAction: loaded.state.nextAction,
360
+ updatedAt: loaded.state.updatedAt,
361
+ }
362
+ }
363
+
364
+ export async function startTask(root, slug, taskID, options = {}) {
365
+ const result = await withWorkLock(root, slug, async () => {
366
+ const loaded = await loadWork(root, slug)
367
+ if (!loaded.plan) throw new Error("PLAN.json is missing; import a valid plan first")
368
+ if (!planIsApproved(loaded.state, loaded.plan)) {
369
+ throw new Error("plan is not approved; run ues-plan-checker and 'ocskill work approve-plan' first")
370
+ }
371
+
372
+ const task = taskByID(loaded.plan, taskID)
373
+ const record = loaded.state.tasks?.[taskID]
374
+ if (!record) throw new Error(`task '${taskID}' is not tracked in STATE.json`)
375
+ if (record.status === "completed") throw new Error(`task '${taskID}' is already completed`)
376
+ if (!readyTasks(loaded.plan, loaded.state).includes(taskID)) {
377
+ throw new Error(`task '${taskID}' is not ready; dependencies or file-overlap constraints are unresolved`)
378
+ }
379
+
380
+ const timestamp = now()
381
+ const leaseMs = Math.max(30_000, Number(options.leaseMs || DEFAULT_TASK_LEASE_MS))
382
+ record.status = "running"
383
+ record.attempts += 1
384
+ record.startedAt = timestamp
385
+ record.lastFailure = null
386
+ record.runId = randomUUID()
387
+ record.owner = {
388
+ pid: Number(options.pid || process.pid),
389
+ sessionID: options.sessionID || null,
390
+ }
391
+ record.heartbeatAt = timestamp
392
+ record.leaseExpiresAt = new Date(Date.now() + leaseMs).toISOString()
393
+ record.evidenceStrength = null
394
+ loaded.state.integrationVerification = null
395
+ loaded.state.status = "executing"
396
+ loaded.state.updatedAt = timestamp
397
+ loaded.state.nextAction = `Complete ${taskID}, write its report, then record fresh verification evidence.`
398
+ await writeJson(loaded.paths.state, loaded.state)
399
+ return { task, record: { ...record } }
400
+ })
401
+
402
+ return { ...result, contextPack: await contextPack(root, slug, taskID) }
403
+ }
404
+
405
+
406
+ function assertRunFence(record, options = {}) {
407
+ const runId = options.runId || null
408
+ if (runId && record.runId && runId !== record.runId) {
409
+ throw new Error("task run fence mismatch; the executor lease is stale")
410
+ }
411
+ }
412
+
413
+ export async function heartbeatTask(root, slug, taskID, runId, options = {}) {
414
+ return withWorkLock(root, slug, async () => {
415
+ const loaded = await loadWork(root, slug)
416
+ const record = loaded.state.tasks?.[taskID]
417
+ if (!record) throw new Error(`task '${taskID}' is not tracked`)
418
+ if (record.status !== "running") throw new Error(`task '${taskID}' is not running`)
419
+ assertRunFence(record, { runId })
420
+ const leaseMs = Math.max(30_000, Number(options.leaseMs || DEFAULT_TASK_LEASE_MS))
421
+ const timestamp = now()
422
+ record.heartbeatAt = timestamp
423
+ record.leaseExpiresAt = new Date(Date.now() + leaseMs).toISOString()
424
+ loaded.state.updatedAt = timestamp
425
+ await writeJson(loaded.paths.state, loaded.state)
426
+ return { taskID, runId: record.runId, heartbeatAt: record.heartbeatAt, leaseExpiresAt: record.leaseExpiresAt }
427
+ })
428
+ }
429
+
430
+ export async function recoverStaleTasks(root, slug, options = {}) {
431
+ return withWorkLock(root, slug, async () => {
432
+ const loaded = await loadWork(root, slug)
433
+ const timestamp = now()
434
+ const recovered = []
435
+ for (const [taskID, record] of Object.entries(loaded.state.tasks || {})) {
436
+ if (record.status !== "running") continue
437
+ const expires = Date.parse(record.leaseExpiresAt || "")
438
+ const stale = options.force || !Number.isFinite(expires) || expires <= Date.now()
439
+ if (!stale) continue
440
+ record.status = "failed"
441
+ record.lastFailure = "stale executor lease recovered after interruption"
442
+ record.runId = null
443
+ record.owner = null
444
+ record.heartbeatAt = null
445
+ record.leaseExpiresAt = null
446
+ recovered.push(taskID)
447
+ }
448
+ if (recovered.length) {
449
+ loaded.state.integrationVerification = null
450
+ loaded.state.status = "executing"
451
+ loaded.state.updatedAt = timestamp
452
+ loaded.state.nextAction = `Recovered stale task(s): ${recovered.join(", ")}. Re-diagnose and retry from fresh evidence.`
453
+ await writeJson(loaded.paths.state, loaded.state)
454
+ }
455
+ return { recovered, state: loaded.state }
456
+ })
457
+ }
458
+
459
+ export async function recordVerificationReceipt(root, slug, taskID, receipt) {
460
+ const validation = validateVerificationReceipt(receipt)
461
+ if (!validation.valid) {
462
+ const error = new Error("invalid verification receipt")
463
+ error.validation = validation
464
+ throw error
465
+ }
466
+
467
+ return withWorkLock(root, slug, async () => {
468
+ const loaded = await loadWork(root, slug)
469
+ const record = loaded.state.tasks?.[taskID]
470
+ if (!record) throw new Error(`task '${taskID}' is not tracked`)
471
+ if (record.status !== "running") throw new Error(`task '${taskID}' must be running to record verification`)
472
+ assertRunFence(record, { runId: receipt.runId })
473
+ loaded.evidence.receipts ??= []
474
+ const normalized = {
475
+ ...receipt,
476
+ task: taskID,
477
+ runId: record.runId,
478
+ recordedAt: now(),
479
+ }
480
+ loaded.evidence.receipts.push(normalized)
481
+ loaded.evidence.schemaVersion = STATE_SCHEMA
482
+ loaded.evidence.updatedAt = normalized.recordedAt
483
+ await writeJson(loaded.paths.evidence, loaded.evidence)
484
+ return normalized
485
+ })
486
+ }
487
+
488
+ export async function completeTask(root, slug, taskID, options = {}) {
489
+ const evidenceText = String(options.evidence || "").trim()
490
+ if (!evidenceText) throw new Error("completion requires non-empty fresh evidence")
491
+
492
+ return withWorkLock(root, slug, async () => {
493
+ const loaded = await loadWork(root, slug)
494
+ if (!loaded.plan) throw new Error("PLAN.json is missing")
495
+ const task = taskByID(loaded.plan, taskID)
496
+ const record = loaded.state.tasks?.[taskID]
497
+ if (!record) throw new Error(`task '${taskID}' is not tracked`)
498
+ if (record.status !== "running") throw new Error(`task '${taskID}' must be running before it can complete`)
499
+ assertRunFence(record, options)
500
+
501
+ const timestamp = now()
502
+ const reportPath = path.join(loaded.paths.reports, `${taskID}.md`)
503
+ if (options.report) await atomicWrite(reportPath, String(options.report).trimEnd() + "\n")
504
+ else if (!existsSync(reportPath)) {
505
+ await atomicWrite(reportPath, `# ${taskID} report\n\nCompleted with evidence recorded in EVIDENCE.json.\n`)
506
+ }
507
+
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
+ record.status = "completed"
511
+ record.completedAt = timestamp
512
+ record.report = path.relative(loaded.paths.root, reportPath).replaceAll("\\", "/")
513
+ record.evidenceStrength = successfulReceipts.length ? "receipt-backed" : "narrative"
514
+ record.runId = null
515
+ record.owner = null
516
+ record.heartbeatAt = null
517
+ record.leaseExpiresAt = null
518
+
519
+ loaded.evidence.entries.push({
520
+ task: taskID,
521
+ at: timestamp,
522
+ evidence: evidenceText,
523
+ verification: task.verification,
524
+ evidenceStrength: record.evidenceStrength,
525
+ receiptIDs: successfulReceipts.map((item) => item.id),
526
+ })
527
+ loaded.evidence.schemaVersion = STATE_SCHEMA
528
+ loaded.evidence.updatedAt = timestamp
529
+
530
+ const allComplete = loaded.plan.tasks.every((item) => loaded.state.tasks[item.id]?.status === "completed")
531
+ const ready = allComplete ? [] : readyTasks(loaded.plan, loaded.state)
532
+ loaded.state.integrationVerification = null
533
+ loaded.state.status = allComplete ? "integration-verification" : "executing"
534
+ loaded.state.updatedAt = timestamp
535
+ loaded.state.nextAction = allComplete
536
+ ? "Run ues-integration-verifier, then record its verdict with 'ocskill work verify-integration'."
537
+ : ready.length
538
+ ? `Execute ready task(s): ${ready.join(", ")}`
539
+ : "Resolve blockers or failed dependencies before continuing."
540
+
541
+ await writeJson(loaded.paths.evidence, loaded.evidence)
542
+ await writeJson(loaded.paths.state, loaded.state)
543
+ return { task, state: loaded.state, ready }
544
+ })
545
+ }
546
+
547
+ export async function failTask(root, slug, taskID, reason, options = {}) {
548
+ const cleanReason = String(reason || "").trim()
549
+ if (!cleanReason) throw new Error("failure reason is required")
550
+
551
+ return withWorkLock(root, slug, async () => {
552
+ const loaded = await loadWork(root, slug)
553
+ if (!loaded.plan) throw new Error("PLAN.json is missing")
554
+ taskByID(loaded.plan, taskID)
555
+ const record = loaded.state.tasks?.[taskID]
556
+ if (!record) throw new Error(`task '${taskID}' is not tracked`)
557
+ assertRunFence(record, options)
558
+
559
+ const timestamp = now()
560
+ record.status = "failed"
561
+ record.lastFailure = cleanReason
562
+ record.runId = null
563
+ record.owner = null
564
+ record.heartbeatAt = null
565
+ record.leaseExpiresAt = null
566
+ loaded.state.integrationVerification = null
567
+ loaded.state.updatedAt = timestamp
568
+ loaded.state.status = "executing"
569
+ loaded.state.nextAction = record.attempts >= 3
570
+ ? `Re-plan or escalate ${taskID}; it has failed ${record.attempts} attempt(s).`
571
+ : `Re-diagnose ${taskID} from fresh evidence before retrying.`
572
+ await writeJson(loaded.paths.state, loaded.state)
573
+ return { taskID, attempts: record.attempts, state: loaded.state }
574
+ })
575
+ }
576
+
577
+ export async function addDecision(root, slug, decision) {
578
+ const text = String(decision || "").trim()
579
+ if (!text) throw new Error("decision text is required")
580
+
581
+ return withWorkLock(root, slug, async () => {
582
+ const loaded = await loadWork(root, slug)
583
+ loaded.state.decisions ??= []
584
+ const timestamp = now()
585
+ loaded.state.decisions.push({ at: timestamp, text })
586
+ loaded.state.updatedAt = timestamp
587
+ await writeJson(loaded.paths.state, loaded.state)
588
+ return loaded.state
589
+ })
590
+ }
591
+
592
+ export async function addBlocker(root, slug, blocker) {
593
+ const text = String(blocker || "").trim()
594
+ if (!text) throw new Error("blocker text is required")
595
+
596
+ return withWorkLock(root, slug, async () => {
597
+ const loaded = await loadWork(root, slug)
598
+ loaded.state.blockers ??= []
599
+ if (!loaded.state.blockers.includes(text)) loaded.state.blockers.push(text)
600
+ loaded.state.integrationVerification = null
601
+ loaded.state.updatedAt = now()
602
+ loaded.state.nextAction = "Resolve blocker: " + text
603
+ await writeJson(loaded.paths.state, loaded.state)
604
+ return loaded.state
605
+ })
606
+ }
607
+
608
+ export async function resolveBlocker(root, slug, blocker) {
609
+ const text = String(blocker || "").trim()
610
+ if (!text) throw new Error("blocker text is required")
611
+
612
+ return withWorkLock(root, slug, async () => {
613
+ const loaded = await loadWork(root, slug)
614
+ loaded.state.blockers = (loaded.state.blockers || []).filter((item) => item !== text)
615
+ loaded.state.integrationVerification = null
616
+ loaded.state.updatedAt = now()
617
+ const ready = readyTasks(loaded.plan, loaded.state)
618
+ loaded.state.nextAction = loaded.state.blockers.length
619
+ ? "Resolve remaining blocker(s) before continuing."
620
+ : ready.length
621
+ ? `Execute ready task(s): ${ready.join(", ")}`
622
+ : loaded.state.status === "integration-verification"
623
+ ? "Run integration verification and record a fresh verdict."
624
+ : loaded.state.nextAction
625
+ await writeJson(loaded.paths.state, loaded.state)
626
+ return loaded.state
627
+ })
628
+ }
629
+
630
+ export async function recordIntegrationVerification(root, slug, verdict, evidence, report = null) {
631
+ const status = String(verdict || "").trim().toUpperCase()
632
+ if (!["PASS", "FAIL", "PARTIAL"].includes(status)) {
633
+ throw new Error("integration verdict must be PASS, FAIL, or PARTIAL")
634
+ }
635
+ const evidenceText = String(evidence || "").trim()
636
+ if (!evidenceText) throw new Error("integration verification requires non-empty evidence")
637
+
638
+ return withWorkLock(root, slug, async () => {
639
+ const loaded = await loadWork(root, slug)
640
+ if (!loaded.plan) throw new Error("PLAN.json is missing")
641
+ const incomplete = loaded.plan.tasks
642
+ .filter((task) => loaded.state.tasks?.[task.id]?.status !== "completed")
643
+ .map((task) => task.id)
644
+ if (incomplete.length) {
645
+ throw new Error("cannot record integration verification with incomplete tasks: " + incomplete.join(", "))
646
+ }
647
+ if (status === "PASS" && loaded.state.blockers?.length) {
648
+ throw new Error("cannot record integration PASS while blockers remain")
649
+ }
650
+
651
+ const timestamp = now()
652
+ const fingerprint = workspaceFingerprint(loaded.paths.root)
653
+ loaded.state.integrationVerification = {
654
+ status,
655
+ at: timestamp,
656
+ evidence: evidenceText,
657
+ report: report ? String(report) : null,
658
+ fingerprint,
659
+ }
660
+ loaded.state.updatedAt = timestamp
661
+ loaded.state.status = status === "PASS" ? "ready-to-finalize" : "integration-verification"
662
+ loaded.state.nextAction = status === "PASS"
663
+ ? "Integration PASS recorded. Finalize only if the workspace fingerprint is unchanged."
664
+ : "Resolve integration findings, re-run affected tasks/checks, then verify integration again."
665
+
666
+ loaded.evidence.entries.push({
667
+ task: "__integration_verification__",
668
+ at: timestamp,
669
+ evidence: evidenceText,
670
+ verdict: status,
671
+ fingerprint,
672
+ verification: ["fresh cross-task integration and acceptance verification"],
673
+ })
674
+ loaded.evidence.schemaVersion = STATE_SCHEMA
675
+ loaded.evidence.updatedAt = timestamp
676
+
677
+ await writeJson(loaded.paths.evidence, loaded.evidence)
678
+ await writeJson(loaded.paths.state, loaded.state)
679
+ return loaded.state.integrationVerification
680
+ })
681
+ }
682
+
683
+ export async function finalizeWork(root, slug, evidence) {
684
+ const evidenceText = String(evidence || "").trim()
685
+ if (!evidenceText) throw new Error("finalization requires fresh integration evidence")
686
+
687
+ return withWorkLock(root, slug, async () => {
688
+ const loaded = await loadWork(root, slug)
689
+ if (!loaded.plan) throw new Error("PLAN.json is missing")
690
+ if (loaded.state.blockers?.length) throw new Error("cannot finalize while blockers remain")
691
+ const incomplete = loaded.plan.tasks
692
+ .filter((task) => loaded.state.tasks?.[task.id]?.status !== "completed")
693
+ .map((task) => task.id)
694
+ if (incomplete.length) throw new Error("cannot finalize with incomplete tasks: " + incomplete.join(", "))
695
+
696
+ const verification = loaded.state.integrationVerification
697
+ if (verification?.status !== "PASS") {
698
+ throw new Error("cannot finalize without a recorded integration PASS")
699
+ }
700
+ const currentFingerprint = workspaceFingerprint(loaded.paths.root)
701
+ if (verification.fingerprint !== currentFingerprint) {
702
+ throw new Error("workspace changed after integration PASS; re-run integration verification before finalizing")
703
+ }
704
+
705
+ const timestamp = now()
706
+ loaded.evidence.entries.push({
707
+ task: "__integration__",
708
+ at: timestamp,
709
+ evidence: evidenceText,
710
+ fingerprint: currentFingerprint,
711
+ verification: ["cross-task integration and final acceptance verification"],
712
+ })
713
+ loaded.evidence.schemaVersion = STATE_SCHEMA
714
+ loaded.evidence.updatedAt = timestamp
715
+
716
+ loaded.state.status = "completed"
717
+ loaded.state.completedAt = timestamp
718
+ loaded.state.updatedAt = timestamp
719
+ loaded.state.nextAction = "Work item completed; preserve evidence and report any remaining limitations accurately."
720
+
721
+ await writeJson(loaded.paths.evidence, loaded.evidence)
722
+ await writeJson(loaded.paths.state, loaded.state)
723
+ return { state: loaded.state, evidence: loaded.evidence.entries.at(-1) }
724
+ })
725
+ }
726
+
727
+ async function buildContextPack(loaded, taskID) {
728
+ if (!loaded.plan) throw new Error("PLAN.json is missing")
729
+ const task = taskByID(loaded.plan, taskID)
730
+ const spec = await readFile(loaded.paths.spec, "utf8").catch(() => "")
731
+ const dependencyReports = {}
732
+
733
+ for (const dep of task.dependsOn || []) {
734
+ const file = path.join(loaded.paths.reports, `${dep}.md`)
735
+ if (!existsSync(file)) continue
736
+ dependencyReports[dep] = (await readFile(file, "utf8")).slice(0, 12000)
737
+ }
738
+
739
+ const contextManifest = await buildContextManifest(loaded.paths.root, task, { budget: 24_000 }).catch(() => null)
740
+ const learnings = await relevantAcceptedLearnings(
741
+ loaded.paths.root,
742
+ [task.title, task.summary, ...(task.acceptance || [])].join(" "),
743
+ ).catch(() => [])
744
+
745
+ return {
746
+ schemaVersion: STATE_SCHEMA,
747
+ slug: loaded.state.slug,
748
+ task,
749
+ taskBrief: path.relative(loaded.paths.root, path.join(loaded.paths.tasks, `${taskID}.md`)).replaceAll("\\", "/"),
750
+ spec: spec.slice(0, 24000),
751
+ dependencyReports,
752
+ decisions: loaded.state.decisions || [],
753
+ blockers: loaded.state.blockers || [],
754
+ planApproval: loaded.state.planApproval || null,
755
+ contextManifest,
756
+ acceptedLearnings: learnings,
757
+ workingState: {
758
+ status: loaded.state.status,
759
+ task: loaded.state.tasks?.[taskID],
760
+ },
761
+ }
762
+ }
763
+
764
+ export async function contextPack(root, slug, taskID) {
765
+ return buildContextPack(await loadWork(root, slug), taskID)
766
+ }
767
+
768
+ export async function resumeWork(root, slug) {
769
+ await recoverStaleTasks(root, slug).catch(() => ({ recovered: [] }))
770
+ const loaded = await loadWork(root, slug)
771
+ return {
772
+ status: await workStatus(root, slug),
773
+ planAnalysis: loaded.plan ? analyzePlan(loaded.plan) : null,
774
+ completedEvidence: loaded.evidence.entries || [],
775
+ decisions: loaded.state.decisions || [],
776
+ }
777
+ }