devcontour-harness 0.0.0-stage → 0.1.0-alpha.6

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 (390) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +183 -2
  3. package/docs/architecture.md +473 -0
  4. package/docs/compatibility-versions.md +56 -0
  5. package/docs/compatibility.md +192 -0
  6. package/docs/connectors.md +323 -0
  7. package/docs/contracts.md +603 -0
  8. package/docs/integrations/hindsight.md +148 -0
  9. package/docs/quickstart.md +182 -0
  10. package/docs/release-notes/0.1.0-alpha.6.md +238 -0
  11. package/docs/review-probes.md +120 -0
  12. package/docs/review-protocol.md +251 -0
  13. package/docs/toolchains.md +167 -0
  14. package/docs/turbo-starter.md +269 -0
  15. package/docs/upgrade.md +202 -0
  16. package/examples/README.md +27 -0
  17. package/examples/external-connector/README.md +19 -0
  18. package/examples/external-connector/connector.mjs +55 -0
  19. package/examples/files-only/README.md +28 -0
  20. package/examples/files-only/context/norms/rules.md +4 -0
  21. package/examples/files-only/graph.json +23 -0
  22. package/examples/files-only/policy.json +30 -0
  23. package/examples/one-vendor/README.md +19 -0
  24. package/examples/one-vendor/policy.json +27 -0
  25. package/examples/single-task/README.md +18 -0
  26. package/examples/single-task/graph.json +27 -0
  27. package/examples/single-task/policy-same-vendor.json +55 -0
  28. package/examples/single-task/policy.json +52 -0
  29. package/examples/single-task/src/.gitkeep +0 -0
  30. package/examples/single-task/test/run.js +19 -0
  31. package/examples/toolchains/README.md +32 -0
  32. package/examples/toolchains/go/bad/label/label.go +9 -0
  33. package/examples/toolchains/go/good/label/label.go +9 -0
  34. package/examples/toolchains/go/policy.json +79 -0
  35. package/examples/toolchains/go/repo/REQUIREMENTS.md +9 -0
  36. package/examples/toolchains/go/repo/_gitignore +1 -0
  37. package/examples/toolchains/go/repo/go.mod +5 -0
  38. package/examples/toolchains/go/repo/go.sum +2 -0
  39. package/examples/toolchains/go/repo/label/label.go +9 -0
  40. package/examples/toolchains/go/repo/label/label_test.go +27 -0
  41. package/examples/toolchains/go/repo/label/version.go +9 -0
  42. package/examples/toolchains/go/task.json +25 -0
  43. package/examples/toolchains/mixed-go/bad/backend/label/label.go +9 -0
  44. package/examples/toolchains/mixed-go/bad/frontend/src/label.js +4 -0
  45. package/examples/toolchains/mixed-go/good/backend/label/label.go +9 -0
  46. package/examples/toolchains/mixed-go/good/frontend/src/label.js +4 -0
  47. package/examples/toolchains/mixed-go/policy.json +126 -0
  48. package/examples/toolchains/mixed-go/repo/REQUIREMENTS.md +9 -0
  49. package/examples/toolchains/mixed-go/repo/_gitignore +6 -0
  50. package/examples/toolchains/mixed-go/repo/backend/go.mod +3 -0
  51. package/examples/toolchains/mixed-go/repo/backend/label/label.go +9 -0
  52. package/examples/toolchains/mixed-go/repo/backend/label/label_test.go +30 -0
  53. package/examples/toolchains/mixed-go/repo/contract/check.mjs +17 -0
  54. package/examples/toolchains/mixed-go/repo/contract/label.json +9 -0
  55. package/examples/toolchains/mixed-go/repo/frontend/package.json +15 -0
  56. package/examples/toolchains/mixed-go/repo/frontend/pnpm-lock.yaml +22 -0
  57. package/examples/toolchains/mixed-go/repo/frontend/src/label.js +4 -0
  58. package/examples/toolchains/mixed-go/repo/frontend/test/label.test.js +11 -0
  59. package/examples/toolchains/mixed-go/task.json +25 -0
  60. package/examples/toolchains/mixed-python/bad/backend/src/fx_backend/__init__.py +6 -0
  61. package/examples/toolchains/mixed-python/bad/frontend/src/label.js +4 -0
  62. package/examples/toolchains/mixed-python/good/backend/src/fx_backend/__init__.py +6 -0
  63. package/examples/toolchains/mixed-python/good/frontend/src/label.js +4 -0
  64. package/examples/toolchains/mixed-python/policy.json +121 -0
  65. package/examples/toolchains/mixed-python/repo/REQUIREMENTS.md +9 -0
  66. package/examples/toolchains/mixed-python/repo/_gitignore +6 -0
  67. package/examples/toolchains/mixed-python/repo/backend/.python-version +1 -0
  68. package/examples/toolchains/mixed-python/repo/backend/pyproject.toml +19 -0
  69. package/examples/toolchains/mixed-python/repo/backend/src/fx_backend/__init__.py +6 -0
  70. package/examples/toolchains/mixed-python/repo/backend/tests/test_label.py +11 -0
  71. package/examples/toolchains/mixed-python/repo/backend/uv.lock +108 -0
  72. package/examples/toolchains/mixed-python/repo/contract/check.mjs +17 -0
  73. package/examples/toolchains/mixed-python/repo/contract/label.json +9 -0
  74. package/examples/toolchains/mixed-python/repo/frontend/package.json +15 -0
  75. package/examples/toolchains/mixed-python/repo/frontend/pnpm-lock.yaml +22 -0
  76. package/examples/toolchains/mixed-python/repo/frontend/src/label.js +4 -0
  77. package/examples/toolchains/mixed-python/repo/frontend/test/label.test.js +11 -0
  78. package/examples/toolchains/mixed-python/task.json +25 -0
  79. package/examples/toolchains/npm/bad/src/label.js +4 -0
  80. package/examples/toolchains/npm/good/src/label.js +4 -0
  81. package/examples/toolchains/npm/policy.json +82 -0
  82. package/examples/toolchains/npm/repo/REQUIREMENTS.md +9 -0
  83. package/examples/toolchains/npm/repo/_gitignore +4 -0
  84. package/examples/toolchains/npm/repo/package-lock.json +22 -0
  85. package/examples/toolchains/npm/repo/package.json +13 -0
  86. package/examples/toolchains/npm/repo/src/label.js +4 -0
  87. package/examples/toolchains/npm/repo/test/label.test.js +13 -0
  88. package/examples/toolchains/npm/task.json +25 -0
  89. package/examples/toolchains/pnpm/bad/packages/label/src/label.js +4 -0
  90. package/examples/toolchains/pnpm/good/packages/label/src/label.js +4 -0
  91. package/examples/toolchains/pnpm/policy.json +85 -0
  92. package/examples/toolchains/pnpm/repo/REQUIREMENTS.md +9 -0
  93. package/examples/toolchains/pnpm/repo/_gitignore +4 -0
  94. package/examples/toolchains/pnpm/repo/package.json +10 -0
  95. package/examples/toolchains/pnpm/repo/packages/checker/check.mjs +5 -0
  96. package/examples/toolchains/pnpm/repo/packages/checker/package.json +9 -0
  97. package/examples/toolchains/pnpm/repo/packages/label/package.json +14 -0
  98. package/examples/toolchains/pnpm/repo/packages/label/src/label.js +4 -0
  99. package/examples/toolchains/pnpm/repo/packages/label/test/label.test.js +13 -0
  100. package/examples/toolchains/pnpm/repo/pnpm-lock.yaml +29 -0
  101. package/examples/toolchains/pnpm/repo/pnpm-workspace.yaml +2 -0
  102. package/examples/toolchains/pnpm/task.json +25 -0
  103. package/examples/toolchains/python/bad/src/fx_label/__init__.py +6 -0
  104. package/examples/toolchains/python/good/src/fx_label/__init__.py +6 -0
  105. package/examples/toolchains/python/policy.json +80 -0
  106. package/examples/toolchains/python/repo/.python-version +1 -0
  107. package/examples/toolchains/python/repo/REQUIREMENTS.md +9 -0
  108. package/examples/toolchains/python/repo/_gitignore +5 -0
  109. package/examples/toolchains/python/repo/pyproject.toml +19 -0
  110. package/examples/toolchains/python/repo/src/fx_label/__init__.py +6 -0
  111. package/examples/toolchains/python/repo/tests/test_label.py +13 -0
  112. package/examples/toolchains/python/repo/uv.lock +108 -0
  113. package/examples/toolchains/python/task.json +25 -0
  114. package/examples/toolchains/turbo/bad/packages/label/src/label.js +4 -0
  115. package/examples/toolchains/turbo/good/packages/label/src/label.js +4 -0
  116. package/examples/toolchains/turbo/policy.json +110 -0
  117. package/examples/toolchains/turbo/repo/REQUIREMENTS.md +9 -0
  118. package/examples/toolchains/turbo/repo/_gitignore +4 -0
  119. package/examples/toolchains/turbo/repo/package.json +13 -0
  120. package/examples/toolchains/turbo/repo/packages/checker/check.mjs +5 -0
  121. package/examples/toolchains/turbo/repo/packages/checker/package.json +9 -0
  122. package/examples/toolchains/turbo/repo/packages/label/package.json +14 -0
  123. package/examples/toolchains/turbo/repo/packages/label/src/label.js +4 -0
  124. package/examples/toolchains/turbo/repo/packages/label/test/label.test.js +13 -0
  125. package/examples/toolchains/turbo/repo/pnpm-lock.yaml +94 -0
  126. package/examples/toolchains/turbo/repo/pnpm-workspace.yaml +2 -0
  127. package/examples/toolchains/turbo/repo/turbo.json +7 -0
  128. package/examples/toolchains/turbo/task.json +25 -0
  129. package/examples/turbo-starter/README.md +16 -0
  130. package/examples/turbo-starter/changes/issued-at-consumer-only.patch +12 -0
  131. package/examples/turbo-starter/changes/issued-at.patch +130 -0
  132. package/examples/turbo-starter/cleanup.mjs +58 -0
  133. package/examples/turbo-starter/graph.json +32 -0
  134. package/examples/turbo-starter/harness/acceptance/db-isolation.test.mjs +97 -0
  135. package/examples/turbo-starter/harness/acceptance/env-probe.test.mjs +75 -0
  136. package/examples/turbo-starter/harness/acceptance/generation.test.mjs +54 -0
  137. package/examples/turbo-starter/harness/acceptance/issued-at.test.mjs +43 -0
  138. package/examples/turbo-starter/harness/acceptance/lifecycle-probe.test.mjs +47 -0
  139. package/examples/turbo-starter/harness/compose-bridge.mjs +593 -0
  140. package/examples/turbo-starter/harness/compose.acceptance.yml +23 -0
  141. package/examples/turbo-starter/harness/env/backend.env.template +7 -0
  142. package/examples/turbo-starter/harness/env/compose.env.template +6 -0
  143. package/examples/turbo-starter/policy.json +109 -0
  144. package/examples/turbo-starter/policy.lifecycle.json +114 -0
  145. package/examples/turbo-starter/setup.mjs +352 -0
  146. package/examples/two-vendors/policy.json +30 -0
  147. package/lib/agents/cli-adapter.d.ts +48 -0
  148. package/lib/agents/cli-adapter.js +386 -0
  149. package/lib/agents/inspect.d.ts +6 -0
  150. package/lib/agents/inspect.js +46 -0
  151. package/lib/agents/probe-server.d.ts +2 -0
  152. package/lib/agents/probe-server.js +13 -0
  153. package/lib/agents/review-probe.d.ts +295 -0
  154. package/lib/agents/review-probe.js +872 -0
  155. package/lib/agents/runtime-events.d.ts +11 -0
  156. package/lib/agents/runtime-events.js +21 -0
  157. package/lib/agents/schemas.d.ts +217 -0
  158. package/lib/agents/schemas.js +124 -0
  159. package/lib/agents/usage.d.ts +8 -0
  160. package/lib/agents/usage.js +83 -0
  161. package/lib/cli.d.ts +2 -0
  162. package/lib/cli.js +491 -0
  163. package/lib/connectors/files.d.ts +108 -0
  164. package/lib/connectors/files.js +133 -0
  165. package/lib/contracts/checks.d.ts +307 -0
  166. package/lib/contracts/checks.js +181 -0
  167. package/lib/contracts/cli.d.ts +3895 -0
  168. package/lib/contracts/cli.js +664 -0
  169. package/lib/contracts/context.d.ts +135 -0
  170. package/lib/contracts/context.js +129 -0
  171. package/lib/contracts/digest.d.ts +9 -0
  172. package/lib/contracts/digest.js +22 -0
  173. package/lib/contracts/errors.d.ts +81 -0
  174. package/lib/contracts/errors.js +70 -0
  175. package/lib/contracts/events.d.ts +827 -0
  176. package/lib/contracts/events.js +258 -0
  177. package/lib/contracts/execution.d.ts +3216 -0
  178. package/lib/contracts/execution.js +872 -0
  179. package/lib/contracts/failure.d.ts +67 -0
  180. package/lib/contracts/failure.js +127 -0
  181. package/lib/contracts/index.d.ts +18 -0
  182. package/lib/contracts/index.js +18 -0
  183. package/lib/contracts/policy.d.ts +1080 -0
  184. package/lib/contracts/policy.js +544 -0
  185. package/lib/contracts/ports.d.ts +417 -0
  186. package/lib/contracts/ports.js +1 -0
  187. package/lib/contracts/primitives.d.ts +63 -0
  188. package/lib/contracts/primitives.js +73 -0
  189. package/lib/contracts/profiles.d.ts +336 -0
  190. package/lib/contracts/profiles.js +109 -0
  191. package/lib/contracts/published.d.ts +3237 -0
  192. package/lib/contracts/published.js +34 -0
  193. package/lib/contracts/result.d.ts +1280 -0
  194. package/lib/contracts/result.js +311 -0
  195. package/lib/contracts/review.d.ts +441 -0
  196. package/lib/contracts/review.js +477 -0
  197. package/lib/contracts/toolchain.d.ts +96 -0
  198. package/lib/contracts/toolchain.js +89 -0
  199. package/lib/contracts/version.d.ts +17 -0
  200. package/lib/contracts/version.js +17 -0
  201. package/lib/contracts/work.d.ts +173 -0
  202. package/lib/contracts/work.js +178 -0
  203. package/lib/control/base-move.d.ts +44 -0
  204. package/lib/control/base-move.js +90 -0
  205. package/lib/control/doctor.d.ts +50 -0
  206. package/lib/control/doctor.js +113 -0
  207. package/lib/control/harness.d.ts +1125 -0
  208. package/lib/control/harness.js +741 -0
  209. package/lib/control/provenance.d.ts +17 -0
  210. package/lib/control/provenance.js +25 -0
  211. package/lib/control/recovery.d.ts +237 -0
  212. package/lib/control/recovery.js +236 -0
  213. package/lib/control/review.d.ts +62 -0
  214. package/lib/control/review.js +178 -0
  215. package/lib/control/workflow.d.ts +247 -0
  216. package/lib/control/workflow.js +259 -0
  217. package/lib/engine/budget.d.ts +345 -0
  218. package/lib/engine/budget.js +278 -0
  219. package/lib/engine/classify.d.ts +8 -0
  220. package/lib/engine/classify.js +32 -0
  221. package/lib/engine/engine.d.ts +1527 -0
  222. package/lib/engine/engine.js +2259 -0
  223. package/lib/engine/failure.d.ts +1 -0
  224. package/lib/engine/failure.js +3 -0
  225. package/lib/engine/joint-runner.d.ts +257 -0
  226. package/lib/engine/joint-runner.js +254 -0
  227. package/lib/engine/measured.d.ts +15 -0
  228. package/lib/engine/measured.js +82 -0
  229. package/lib/engine/prompt.d.ts +66 -0
  230. package/lib/engine/prompt.js +131 -0
  231. package/lib/engine/property-recovery.d.ts +50 -0
  232. package/lib/engine/property-recovery.js +171 -0
  233. package/lib/engine/repair.d.ts +33 -0
  234. package/lib/engine/repair.js +82 -0
  235. package/lib/engine/result.d.ts +9 -0
  236. package/lib/engine/result.js +218 -0
  237. package/lib/engine/reuse.d.ts +80 -0
  238. package/lib/engine/reuse.js +102 -0
  239. package/lib/engine/runner.d.ts +360 -0
  240. package/lib/engine/runner.js +1836 -0
  241. package/lib/engine/standalone.d.ts +6 -0
  242. package/lib/engine/standalone.js +48 -0
  243. package/lib/engine/usage.d.ts +87 -0
  244. package/lib/engine/usage.js +165 -0
  245. package/lib/environment/isolation.d.ts +110 -0
  246. package/lib/environment/isolation.js +238 -0
  247. package/lib/environment/lifecycle.d.ts +40 -0
  248. package/lib/environment/lifecycle.js +197 -0
  249. package/lib/environment/live-calls.d.ts +14 -0
  250. package/lib/environment/live-calls.js +73 -0
  251. package/lib/environment/os-tools.d.ts +16 -0
  252. package/lib/environment/os-tools.js +35 -0
  253. package/lib/environment/process.d.ts +32 -0
  254. package/lib/environment/process.js +369 -0
  255. package/lib/environment/redaction.d.ts +22 -0
  256. package/lib/environment/redaction.js +102 -0
  257. package/lib/environment/resources.d.ts +31 -0
  258. package/lib/environment/resources.js +120 -0
  259. package/lib/index.d.ts +65 -0
  260. package/lib/index.js +67 -0
  261. package/lib/integrations/connector/commands.d.ts +358 -0
  262. package/lib/integrations/connector/commands.js +428 -0
  263. package/lib/integrations/connector/config.d.ts +215 -0
  264. package/lib/integrations/connector/config.js +214 -0
  265. package/lib/integrations/connector/delivery.d.ts +107 -0
  266. package/lib/integrations/connector/delivery.js +591 -0
  267. package/lib/integrations/connector/fake.d.ts +291 -0
  268. package/lib/integrations/connector/fake.js +786 -0
  269. package/lib/integrations/connector/history.d.ts +124 -0
  270. package/lib/integrations/connector/history.js +497 -0
  271. package/lib/integrations/connector/index.d.ts +16 -0
  272. package/lib/integrations/connector/index.js +16 -0
  273. package/lib/integrations/connector/redact.d.ts +19 -0
  274. package/lib/integrations/connector/redact.js +91 -0
  275. package/lib/integrations/connector/registry.d.ts +15 -0
  276. package/lib/integrations/connector/registry.js +43 -0
  277. package/lib/integrations/connector/snapshot.d.ts +131 -0
  278. package/lib/integrations/connector/snapshot.js +422 -0
  279. package/lib/integrations/connector/source.d.ts +37 -0
  280. package/lib/integrations/connector/source.js +73 -0
  281. package/lib/integrations/connector/steps.d.ts +71 -0
  282. package/lib/integrations/connector/steps.js +188 -0
  283. package/lib/integrations/connector/store.d.ts +140 -0
  284. package/lib/integrations/connector/store.js +277 -0
  285. package/lib/integrations/connector/summary.d.ts +38 -0
  286. package/lib/integrations/connector/summary.js +59 -0
  287. package/lib/integrations/connector/testing-worker.d.ts +1 -0
  288. package/lib/integrations/connector/testing-worker.js +81 -0
  289. package/lib/integrations/connector/testing.d.ts +205 -0
  290. package/lib/integrations/connector/testing.js +1615 -0
  291. package/lib/integrations/connector/transport.d.ts +240 -0
  292. package/lib/integrations/connector/transport.js +28 -0
  293. package/lib/integrations/memory/context.d.ts +120 -0
  294. package/lib/integrations/memory/context.js +239 -0
  295. package/lib/integrations/memory/index.d.ts +8 -0
  296. package/lib/integrations/memory/index.js +8 -0
  297. package/lib/integrations/memory/provider.d.ts +79 -0
  298. package/lib/integrations/memory/provider.js +22 -0
  299. package/lib/integrations/memory/sink.d.ts +56 -0
  300. package/lib/integrations/memory/sink.js +116 -0
  301. package/lib/integrations/memory/testing.d.ts +44 -0
  302. package/lib/integrations/memory/testing.js +61 -0
  303. package/lib/integrations/relay-v1/index.d.ts +122 -0
  304. package/lib/integrations/relay-v1/index.js +860 -0
  305. package/lib/integrations/relay-v2/index.d.ts +145 -0
  306. package/lib/integrations/relay-v2/index.js +752 -0
  307. package/lib/integrations/relay-v2/process.d.ts +32 -0
  308. package/lib/integrations/relay-v2/process.js +112 -0
  309. package/lib/integrations/relay-v2/protocol.d.ts +172 -0
  310. package/lib/integrations/relay-v2/protocol.js +142 -0
  311. package/lib/package.d.ts +2 -0
  312. package/lib/package.js +3 -0
  313. package/lib/profiles/resolve.d.ts +190 -0
  314. package/lib/profiles/resolve.js +273 -0
  315. package/lib/storage/document.d.ts +1234 -0
  316. package/lib/storage/document.js +80 -0
  317. package/lib/storage/sqlite.d.ts +35 -0
  318. package/lib/storage/sqlite.js +260 -0
  319. package/lib/testing/index.d.ts +5 -0
  320. package/lib/testing/index.js +5 -0
  321. package/lib/testing/store-contract.d.ts +8 -0
  322. package/lib/testing/store-contract.js +167 -0
  323. package/lib/toolchains/index.d.ts +9 -0
  324. package/lib/toolchains/index.js +9 -0
  325. package/lib/toolchains/inspect.d.ts +105 -0
  326. package/lib/toolchains/inspect.js +831 -0
  327. package/lib/toolchains/manifests.d.ts +67 -0
  328. package/lib/toolchains/manifests.js +238 -0
  329. package/lib/toolchains/probe.d.ts +28 -0
  330. package/lib/toolchains/probe.js +54 -0
  331. package/lib/toolchains/recipes.d.ts +33 -0
  332. package/lib/toolchains/recipes.js +51 -0
  333. package/lib/validate.d.ts +36 -0
  334. package/lib/validate.js +175 -0
  335. package/lib/verification/acceptance.d.ts +284 -0
  336. package/lib/verification/acceptance.js +670 -0
  337. package/lib/verification/checks.d.ts +72 -0
  338. package/lib/verification/checks.js +190 -0
  339. package/lib/verification/confirm.d.ts +125 -0
  340. package/lib/verification/confirm.js +411 -0
  341. package/lib/verification/corpus.d.ts +1303 -0
  342. package/lib/verification/corpus.js +744 -0
  343. package/lib/verification/dependencies.d.ts +66 -0
  344. package/lib/verification/dependencies.js +144 -0
  345. package/lib/verification/gates.d.ts +160 -0
  346. package/lib/verification/gates.js +600 -0
  347. package/lib/verification/joint.d.ts +63 -0
  348. package/lib/verification/joint.js +121 -0
  349. package/lib/verification/proof.d.ts +54 -0
  350. package/lib/verification/proof.js +96 -0
  351. package/lib/verification/property.d.ts +86 -0
  352. package/lib/verification/property.js +214 -0
  353. package/lib/verification/requirements.d.ts +25 -0
  354. package/lib/verification/requirements.js +69 -0
  355. package/lib/verification/self-check.d.ts +29 -0
  356. package/lib/verification/self-check.js +99 -0
  357. package/lib/verification/spike.d.ts +79 -0
  358. package/lib/verification/spike.js +124 -0
  359. package/package.json +100 -4
  360. package/profiles/expo-mobile.json +15 -0
  361. package/profiles/go-api.json +15 -0
  362. package/profiles/go-gotestsum.json +21 -0
  363. package/profiles/mobile-maestro.json +29 -0
  364. package/profiles/nest-api.json +26 -0
  365. package/profiles/next-product.json +72 -0
  366. package/profiles/node-checks.json +77 -0
  367. package/profiles/npm-ci.json +22 -0
  368. package/profiles/pnpm-frozen.json +24 -0
  369. package/profiles/python-api.json +32 -0
  370. package/profiles/python-uv.json +26 -0
  371. package/profiles/react-native.json +50 -0
  372. package/profiles/react-vite-admin.json +71 -0
  373. package/schemas/context-snapshot.schema.json +231 -0
  374. package/schemas/contract-document.schema.json +385 -0
  375. package/schemas/execution-document.schema.json +3695 -0
  376. package/schemas/execution-policy.schema.json +2196 -0
  377. package/schemas/joint-verification.schema.json +617 -0
  378. package/schemas/result-envelope.schema.json +1652 -0
  379. package/schemas/review-record.schema.json +177 -0
  380. package/schemas/run-event.schema.json +2458 -0
  381. package/schemas/run-result.schema.json +1570 -0
  382. package/schemas/stack-profile.schema.json +535 -0
  383. package/schemas/work-graph.schema.json +381 -0
  384. package/schemas/work-order.schema.json +256 -0
  385. package/skills/harness/SKILL.md +64 -0
  386. package/templates/AGENTS.md +13 -0
  387. package/templates/controller-mode/AGENTS.controller.md +58 -0
  388. package/templates/controller-mode/README.md +44 -0
  389. package/templates/graph.json +22 -0
  390. package/templates/policy.json +28 -0
@@ -0,0 +1,741 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync, mkdirSync } from 'node:fs';
3
+ import { isAbsolute, join, resolve } from 'node:path';
4
+ import { executionDigest, API_VERSION, HarnessError, STATE_VERSION, WORKSPACE_OWNER, contextDigest, contextIssues, digestOf, workGraphDigest, } from "../contracts/index.js";
5
+ import { cliAdapter } from "../agents/cli-adapter.js";
6
+ import { Engine } from "../engine/engine.js";
7
+ import { budgetOverview } from "../engine/budget.js";
8
+ import { runResult } from "../engine/result.js";
9
+ import { repairDecision } from "../engine/repair.js";
10
+ import { Runner } from "../engine/runner.js";
11
+ import { standaloneHost } from "../engine/standalone.js";
12
+ import { HARNESS_VERSION } from "../package.js";
13
+ import { SqliteExecutionStore } from "../storage/sqlite.js";
14
+ import { validateInputs } from "../validate.js";
15
+ import { WorkflowQueue } from "./workflow.js";
16
+ import { technicalReview } from "./review.js";
17
+ import { policySourceChanged } from "./provenance.js";
18
+ import { JointRunner } from "../engine/joint-runner.js";
19
+ import { git } from "../environment/process.js";
20
+ import { analyzeRecovery, applyCases } from "./recovery.js";
21
+ import { declarationProblems } from "../verification/checks.js";
22
+ import { spikeOf, spikesOf } from "../verification/spike.js";
23
+ import { staleArtifacts } from "../verification/confirm.js";
24
+ const stateFile = (dir) => join(dir, 'state.sqlite');
25
+ /**
26
+ * Доменные операции harness — общие для библиотеки и CLI. Здесь нет
27
+ * операции «передать PASS» или «принять без проверок»: приёмка возможна
28
+ * только через движок после evidence обеих фаз.
29
+ */
30
+ export class Harness {
31
+ options;
32
+ engine;
33
+ runner;
34
+ store;
35
+ policy;
36
+ /** Техническая очередь: сохранённые стадии и ограниченное восстановление графа. */
37
+ workflows;
38
+ /** Совместная проверка комбинации репозиториев. */
39
+ joint;
40
+ adapters;
41
+ static open(options) {
42
+ if (options.readOnly && !existsSync(stateFile(options.stateDir)))
43
+ throw new HarnessError('NOT_FOUND', 'Каталог состояния не найден: ' + options.stateDir);
44
+ return new Harness(options);
45
+ }
46
+ constructor(options) {
47
+ this.options = options;
48
+ this.policy = options.policy;
49
+ if (!options.readOnly)
50
+ mkdirSync(options.stateDir, { recursive: true });
51
+ this.store = new SqliteExecutionStore(stateFile(options.stateDir), 'devcontour-harness@' + HARNESS_VERSION,
52
+ // Только чтение состояния: процесс не становится писателем (meta, PRAGMA и данные прежние).
53
+ { readOnly: Boolean(options.readOnlyState) });
54
+ const source = options.policySource;
55
+ this.engine = new Engine({
56
+ host: standaloneHost(this.store, this.policy),
57
+ policy: this.policy,
58
+ ...(source ? { provenance: () => policySourceChanged(source) } : {}),
59
+ });
60
+ this.workflows = new WorkflowQueue(this.store, this.policy, () => this.engine.clock());
61
+ this.adapters = options.adapters ?? {
62
+ codex: cliAdapter('codex', { toolProfiles: this.policy.toolProfiles }),
63
+ claude: cliAdapter('claude', { toolProfiles: this.policy.toolProfiles }),
64
+ };
65
+ this.runner = new Runner({
66
+ engine: this.engine,
67
+ stateDir: options.stateDir,
68
+ baseDir: options.baseDir,
69
+ adapters: this.adapters,
70
+ context: (work) => this.contextOf(work.graphId),
71
+ ...(options.environment ? { environment: options.environment } : {}),
72
+ ...(options.resourceDatabase ? { resourceDatabase: options.resourceDatabase } : {}),
73
+ ...(options.hooks ? { hooks: options.hooks } : {}),
74
+ });
75
+ this.joint = new JointRunner({
76
+ engine: this.engine,
77
+ stateDir: options.stateDir,
78
+ baseDir: options.baseDir,
79
+ ...(options.environment ? { environment: options.environment } : {}),
80
+ ...(options.resourceDatabase ? { resourceDatabase: options.resourceDatabase } : {}),
81
+ });
82
+ }
83
+ close() {
84
+ this.store.close();
85
+ }
86
+ /* ---------------------------------------------------------------- */
87
+ /* Совместная проверка и применение политики */
88
+ /* ---------------------------------------------------------------- */
89
+ /**
90
+ * Совместная проверка набора принятых поручений на текущих target ref.
91
+ * Возвращает итог (`passed` или `failed` с причиной); продуктовое принятие
92
+ * комбинации решает хост.
93
+ */
94
+ verifyJoint(workIds, options = {}) {
95
+ return this.joint.verify(workIds, options);
96
+ }
97
+ /**
98
+ * Актуальность результата совместной проверки набора и свежесть его
99
+ * manifest: SHA каждого репозитория сравнивается с текущим target ref.
100
+ * Свежесть — наблюдение, а не запрет: решение о принятии — за хостом.
101
+ */
102
+ async jointResult(workIds) {
103
+ const result = this.engine.jointResult(workIds);
104
+ const manifest = result.verification?.manifest;
105
+ if (!manifest)
106
+ return result;
107
+ const freshness = await Promise.all(this.policy.repositories.map(async (repo) => {
108
+ const head = await git(this.repoPath(repo.path), 'rev-parse', repo.targetRef).catch(() => undefined);
109
+ return {
110
+ repositoryId: repo.id,
111
+ sha: manifest[repo.id].sha,
112
+ ...(head ? { head } : {}),
113
+ fresh: head === manifest[repo.id].sha,
114
+ };
115
+ }));
116
+ return { ...result, freshness };
117
+ }
118
+ /** Запросить применение политики с digest `policyDigest(next)`; держит новые выдачи. */
119
+ requestReconfigure(operation, digest) {
120
+ this.engine.operationPause(operation);
121
+ return this.engine.requestReconfigure(operation, digest);
122
+ }
123
+ /** Применить запрос политикой этого процесса (после drain), см. `Engine.applyReconfigure`. */
124
+ applyReconfigure(by = 'harness') {
125
+ return this.engine.applyReconfigure(by);
126
+ }
127
+ policyState() {
128
+ return this.engine.policyState();
129
+ }
130
+ /**
131
+ * Довести незавершённую очистку закончившейся попытки: повторить teardown по
132
+ * сохранённым receipt окружений и убрать её рабочие деревья и снимки.
133
+ */
134
+ cleanup(runId) {
135
+ return this.runner.finishCleanup(runId);
136
+ }
137
+ repoPath(path) {
138
+ return isAbsolute(path) ? path : resolve(this.options.baseDir, path);
139
+ }
140
+ /* ---------------------------------------------------------------- */
141
+ /* Ревью плана и контракта */
142
+ /* ---------------------------------------------------------------- */
143
+ /**
144
+ * Снимок контекста для ревью плана: без нужного графу источника — отказ
145
+ * `NOT_READY`, нарушения снимка (digest, размер) — `INVALID_INPUT`.
146
+ * Поручения без контекста — снимок не нужен.
147
+ */
148
+ planContext(graph, context) {
149
+ const needed = [...new Set(graph.orders.flatMap((o) => o.context))];
150
+ if (!context) {
151
+ if (needed.length)
152
+ throw new HarnessError('NOT_READY', 'Поручения требуют контекст, снимок не передан: ' + needed.join(', '));
153
+ return undefined;
154
+ }
155
+ const issues = contextIssues(context);
156
+ if (issues.length)
157
+ throw new HarnessError('INVALID_INPUT', 'Снимок контекста: ' + issues.join('; '));
158
+ const absent = needed.filter((id) => !context.sources.some((s) => s.id === id));
159
+ if (absent.length)
160
+ throw new HarnessError('NOT_READY', 'В снимке нет нужного контекста: ' + absent.join(', '));
161
+ return context;
162
+ }
163
+ /**
164
+ * Раунд технического ревью предмета (граф или контракт) с конкретным
165
+ * digest. Тот же digest и автор, уже согласованные последним раундом, —
166
+ * тот же раунд без вызова модели. Reviewer — по правилу вендора политики;
167
+ * недопустимый автор — `REVIEWER_SAME_VENDOR` до вызова. Reviewer называет
168
+ * состояние каждой прежней открытой находки; история не переписывается.
169
+ */
170
+ async review(input) {
171
+ const subject = input.kind === 'plan' ? input.graph.id : input.contract.id;
172
+ const digest = input.kind === 'plan' ? workGraphDigest(input.graph) : digestOf(input.contract);
173
+ // Структурные пропуски договора проверки и незакреплённые файлы — отказ до
174
+ // вызова модели: reviewer оценивает достаточность, а не наличие полей.
175
+ if (input.kind === 'contract')
176
+ await this.assertContract(input.contract);
177
+ // Закреплённый контекст плана: проверенный снимок с каждым нужным
178
+ // источником, иначе — отказ до вызова; его digest — часть основания раунда.
179
+ const context = input.kind === 'plan' ? this.planContext(input.graph, input.context) : undefined;
180
+ const repo = this.policy.repositories[0];
181
+ // Номер следующего раунда — от последнего записанного, а не от числа записей:
182
+ // раунды хоста до harness сдвигают нумерацию, и каталог по числу записей
183
+ // совпал бы с каталогом прежнего раунда.
184
+ const round = (this.engine.reviews(input.kind, subject).at(-1)?.round ?? 0) + 1;
185
+ return technicalReview(this.engine, this.policy, this.adapters, {
186
+ kind: input.kind,
187
+ subject,
188
+ digest,
189
+ author: input.author,
190
+ instructions: [
191
+ input.kind === 'plan'
192
+ ? 'You independently review a plan: a graph of executable work orders. Check that each order is complete, testable, scoped and that dependencies are right. Do not edit files.'
193
+ : 'You independently review a contract document. Check that every obligation is feasible and measurable. Do not edit files.',
194
+ JSON.stringify(input.kind === 'plan' ? input.graph : input.contract, null, 2),
195
+ input.kind === 'contract' ? this.spikeNote(input.contract) : '',
196
+ ],
197
+ ...(context ? { context } : {}),
198
+ ...(input.kind === 'contract' ? { contract: input.contract } : {}),
199
+ repositoryId: repo.id,
200
+ cwd: this.repoPath(repo.path),
201
+ artifactDir: join(this.options.stateDir, 'reviews', `${input.kind}-${subject}-${round}`),
202
+ denied: [this.options.stateDir],
203
+ // Вызов под учётом: о графе — расход группы и её поручений, о контракте — общий.
204
+ meta: {
205
+ subjectId: subject,
206
+ subjectKind: input.kind === 'plan' ? 'group' : 'other',
207
+ // Расход ревью плана закрепляется за графом и его поручениями из входа:
208
+ // граф может быть ещё не зарегистрирован, а состав — измениться позже.
209
+ ...(input.kind === 'plan'
210
+ ? { group: { id: subject, workIds: input.graph.orders.map((o) => o.id) } }
211
+ : {}),
212
+ },
213
+ maxRounds: this.policy.maxAttempts * 2,
214
+ ...(this.options.environment ? { environment: this.options.environment } : {}),
215
+ });
216
+ }
217
+ /**
218
+ * Эксперименты, на которые ссылаются тяжёлые обязательства: reviewer читает
219
+ * фактический результат, а не пересказ автора.
220
+ */
221
+ spikeNote(contract) {
222
+ const ids = new Set(contract.checks?.obligations.flatMap((o) => o.feasibility?.spike ?? []));
223
+ if (!ids.size)
224
+ return '';
225
+ const repositoryId = contract.repositoryId ?? this.policy.repositories[0].id;
226
+ const spikes = this.engine.read((v) => spikesOf(v, repositoryId)).filter((x) => ids.has(x.id));
227
+ return ('Feasibility spikes referenced by hard obligations (pinned by digest). For obligations marked hard, read the spike: a timeout is inconclusive for that method and budget — it neither proves hardness or impossibility nor allows weakening the obligation; success on a sample does not prove a bound for every input. Spikes: ' +
228
+ JSON.stringify(spikes));
229
+ }
230
+ async assertContract(contract) {
231
+ const repo = this.policy.repositories.find((r) => r.id === (contract.repositoryId ?? '')) ??
232
+ (contract.repositoryId ? undefined : this.policy.repositories[0]);
233
+ if (!repo)
234
+ throw new HarnessError('INVALID_INPUT', 'Репозиторий не объявлен: ' + contract.repositoryId);
235
+ const problems = contract.checks
236
+ ? declarationProblems(contract.checks, {
237
+ gates: repo.gates,
238
+ repositoryId: repo.id,
239
+ artifacts: (contract.artifacts ?? []).map((a) => a.path),
240
+ // Ссылка тяжёлого обязательства закрепляет digest эксперимента этого репозитория.
241
+ spike: (id) => this.engine.read((v) => spikeOf(v, repo.id, id)),
242
+ })
243
+ : [];
244
+ const stale = await staleArtifacts(this.repoPath(repo.path), repo.targetRef, contract.artifacts ?? []);
245
+ if (stale.length)
246
+ problems.push('закреплённые файлы не совпадают с целевой веткой ' +
247
+ repo.targetRef +
248
+ ': ' +
249
+ stale.join(', '));
250
+ if (problems.length)
251
+ throw new HarnessError('INVALID_INPUT', 'Договор проверки неполон: ' + problems.join('; '));
252
+ }
253
+ /* ---------------------------------------------------------------- */
254
+ /* Корпус подтверждённых случаев */
255
+ /* ---------------------------------------------------------------- */
256
+ /** Корпус: принятые версии, решения, отложенные принятия. */
257
+ get corpus() {
258
+ return this.runner.corpus;
259
+ }
260
+ /**
261
+ * Ограниченный эксперимент выполнимости до реализации тяжёлого обязательства.
262
+ * Таймаут — `inconclusive`: не доказательство невыполнимости и не основание
263
+ * ослаблять обязательство.
264
+ */
265
+ spike(input) {
266
+ return this.runner.spike(input);
267
+ }
268
+ /** Эксперименты выполнимости репозитория (по умолчанию — всех). */
269
+ spikes(repositoryId) {
270
+ return this.engine.read((v) => spikesOf(v, repositoryId));
271
+ }
272
+ /** Подтвердить пример закреплённым эталоном; подтверждённый — новая версия корпуса. */
273
+ submitCase(input) {
274
+ return this.runner.submitCase(input);
275
+ }
276
+ /** Ручное техническое подтверждение принятия версии корпуса (operator mode). */
277
+ approveAdoption(suiteId, pendingId) {
278
+ return this.corpus.approveAdoption(suiteId, pendingId);
279
+ }
280
+ /** Решение эпизода по свойству человеком: CAS по версии записи. */
281
+ decideRecovery(workId, recoveryId, decision, version) {
282
+ return this.engine.recoveryDecide(workId, recoveryId, { ...decision, source: { kind: 'operator' }, decidedAt: new Date().toISOString() }, version);
283
+ }
284
+ /** Согласование оператором (approvalMode: operator) графа, digest которого прошёл ревью. */
285
+ approve(subject, digest) {
286
+ const graph = this.engine.read((v) => v.graph(subject));
287
+ if (!graph)
288
+ throw new HarnessError('NOT_FOUND', 'Граф не найден: ' + subject);
289
+ if (graph.digest !== digest)
290
+ throw new HarnessError('APPROVAL_STALE', 'Digest не совпадает с зарегистрированным графом');
291
+ const before = graph.approval?.digest === digest;
292
+ this.engine.approve(subject, 'operator', { bindContext: true });
293
+ const at = this.engine.read((v) => v.graph(subject).approval.at);
294
+ return { subject, digest, at, changed: !before };
295
+ }
296
+ /* ---------------------------------------------------------------- */
297
+ /* Работа */
298
+ /* ---------------------------------------------------------------- */
299
+ /**
300
+ * Действующий контекст графа — снимок, связанный с его согласованием, а не
301
+ * последний переданный: отклонённый или ещё не согласованный снимок не
302
+ * исполняется.
303
+ */
304
+ contextOf(graphId) {
305
+ const digest = this.engine.read((v) => v.graph(graphId)?.approval?.context?.digest);
306
+ if (!digest)
307
+ return undefined;
308
+ return this.engine.read((v) => v.local('context-snapshots', WORKSPACE_OWNER, digest));
309
+ }
310
+ /**
311
+ * Зарегистрировать граф (идемпотентно по редакции), в режиме `agent`
312
+ * провести ревью плана, если его нет, согласовать и вести очередь до
313
+ * простоя. Результаты доставляются получателю с идемпотентным ключом.
314
+ */
315
+ async run(input) {
316
+ // Обязательный контекст — одинаково для библиотеки и CLI: без снимка нужных источников граф не исполняется.
317
+ const needed = [...new Set(input.graph.orders.flatMap((o) => o.context))];
318
+ if (needed.length && !input.context)
319
+ throw new HarnessError('NOT_READY', 'Поручения требуют контекст, снимок не передан: ' + needed.join(', '));
320
+ validateInputs({
321
+ graph: input.graph,
322
+ policy: this.policy,
323
+ ...(input.context ? { context: input.context } : {}),
324
+ });
325
+ const repos = new Map(this.policy.repositories.map((r) => [r.id, r]));
326
+ if (input.source && input.source.id !== input.graph.source.connector)
327
+ throw new HarnessError('INVALID_INPUT', `Граф из источника ${input.graph.source.connector}, передан источник ${input.source.id}`);
328
+ // Решения источника собираются до транзакции регистрации: порт асинхронный.
329
+ const trusted = new Set();
330
+ for (const p of input.graph.prerequisites)
331
+ if (p.receipt.kind === 'trusted' && input.source && (await input.source.trusts(p)))
332
+ trusted.add(p.id);
333
+ // Атрибуция прежних записей вызовов закрепляется до смены состава графа.
334
+ this.engine.pinLegacyCalls();
335
+ const registered = this.engine.register(input.graph, {
336
+ trusted: (p) => trusted.has(p.id),
337
+ reachable: (p) => {
338
+ const repo = repos.get(p.repositoryId);
339
+ if (!repo)
340
+ return false;
341
+ try {
342
+ // Синхронная проверка: предпосылка доступна в целевой ветке.
343
+ execFileSync('git', ['merge-base', '--is-ancestor', p.resultSha, repo.targetRef], {
344
+ cwd: this.repoPath(repo.path),
345
+ stdio: 'ignore',
346
+ });
347
+ return true;
348
+ }
349
+ catch {
350
+ return false;
351
+ }
352
+ },
353
+ });
354
+ const graph = registered.graph;
355
+ if (input.context) {
356
+ const issues = contextIssues(input.context);
357
+ if (issues.length)
358
+ throw new HarnessError('INVALID_INPUT', 'Снимок контекста: ' + issues.join('; '));
359
+ }
360
+ // Согласование связано с контекстом, который видел reviewer плана: снимок
361
+ // хранится по digest, но действующим становится только вместе с
362
+ // согласованием. Другой снимок при том же графе требует нового раунда.
363
+ const contextKey = input.context ? contextDigest(input.context) : null;
364
+ const contextChanged = (graph.approval?.context?.digest ?? null) !== contextKey;
365
+ if (!graph.approval ||
366
+ graph.approval.digest !== graph.digest ||
367
+ graph.policyDigest !== executionDigest(this.policy) ||
368
+ contextChanged) {
369
+ if (this.policy.approvalMode === 'agent') {
370
+ const record = await this.review({
371
+ kind: 'plan',
372
+ graph: input.graph,
373
+ author: input.author,
374
+ ...(input.context ? { context: input.context } : {}),
375
+ });
376
+ if (!record.approved)
377
+ throw new HarnessError('REVIEW_REJECTED', 'Ревью плана не одобрило граф: ' + record.summary);
378
+ this.engine.approve(graph.id, 'agent', { bindContext: true });
379
+ }
380
+ else if (!this.engine.read((v) => v.graph(graph.id)?.approval) || contextChanged)
381
+ throw new HarnessError('APPROVAL_REQUIRED', 'approvalMode: operator — выполните review-plan и approve' +
382
+ (contextChanged ? ' (контекст изменился после ревью плана)' : ''));
383
+ }
384
+ await this.runner.init();
385
+ // Разрешение начать работу снимает только паузу «очередь ещё не запускали»
386
+ // (без причины) и штатную остановку: это не решения о работе. Пауза
387
+ // человека, отказа runtime, применения политики и проверки базы остаётся
388
+ // до явного продолжения.
389
+ if (this.engine.read((v) => v.control().paused &&
390
+ (!v.control().pauseReason || v.control().pauseReason === 'shutdown')))
391
+ this.engine.resume();
392
+ // В режиме agent работа над графом — задание технической очереди: его
393
+ // стадия сохраняется, а упавшие поручения чинятся в пределах бюджета.
394
+ // Оператор решает сам: автоматических повторов нет.
395
+ const job = this.policy.approvalMode === 'agent' &&
396
+ !input.once &&
397
+ (input.author === 'codex' || input.author === 'claude')
398
+ ? this.workflows.start({ kind: 'graph', id: graph.id, authorRuntime: input.author })
399
+ : undefined;
400
+ try {
401
+ if (input.once)
402
+ await this.runner.step();
403
+ else if (job)
404
+ await this.advance(job.key, graph.id);
405
+ else
406
+ await this.runner.drain();
407
+ }
408
+ finally {
409
+ this.runner.release();
410
+ }
411
+ if (input.sink)
412
+ await this.deliverAll(graph.id, input.sink);
413
+ return {
414
+ graphId: graph.id,
415
+ digest: graph.digest,
416
+ resumed: registered.resumed,
417
+ ...(job ? { workflow: this.workflows.view(job.key) } : {}),
418
+ works: this.engine.read((v) => v.works(graph.id).map((w) => ({
419
+ id: w.id,
420
+ status: w.status,
421
+ ...(w.activeRunId ? { runId: w.activeRunId } : {}),
422
+ ...(w.resultSha ? { resultSha: w.resultSha } : {}),
423
+ blockers: this.engine.blockers(w, v),
424
+ }))),
425
+ };
426
+ }
427
+ /**
428
+ * Вести задание графа: стадия согласования уже пройдена синхронно; стадия
429
+ * исполнения — очередь до простоя, затем решение о восстановлении по
430
+ * записанному состоянию. Повтор назначается и списывается одной
431
+ * транзакцией; остановка записывает причину в задание.
432
+ */
433
+ async advance(key, graphId) {
434
+ for (;;) {
435
+ await this.runner.drain();
436
+ const job = this.workflows.claim(key);
437
+ if (!job)
438
+ return;
439
+ const timer = setInterval(() => {
440
+ try {
441
+ this.workflows.heartbeat(job);
442
+ }
443
+ catch {
444
+ /* Утраченное владение заданием проверит следующая запись. */
445
+ }
446
+ }, Math.max(1000, this.policy.leaseMs / 3));
447
+ try {
448
+ if (job.stage === 0) {
449
+ this.workflows.finish(job);
450
+ continue;
451
+ }
452
+ if ((await this.recoverProperties(job, graphId)) || this.repair(job, graphId)) {
453
+ this.workflows.finish(job, true);
454
+ continue;
455
+ }
456
+ const done = this.engine.read((v) => v.works(graphId).every((w) => w.status === 'done'));
457
+ this.workflows.finish(job, !done);
458
+ return;
459
+ }
460
+ catch (error) {
461
+ this.workflows.fail(job, error instanceof Error ? error.message : String(error));
462
+ return;
463
+ }
464
+ finally {
465
+ clearInterval(timer);
466
+ }
467
+ }
468
+ }
469
+ /**
470
+ * Эпизоды повторяющихся отказов по свойству. Возвращает true, когда
471
+ * разрешённое действие назначено и его результата нужно дождаться;
472
+ * останавливается с причиной там, где автоматического действия нет.
473
+ * Бюджет восстановлений и разбор эпизода учитываются одной транзакцией до
474
+ * вызова модели: рестарт и повтор бесплатного вызова не получают.
475
+ */
476
+ async recoverProperties(job, graphId) {
477
+ const works = this.engine.read((v) => v.works(graphId).filter((w) => w.recovery && w.status !== 'cancelled'));
478
+ for (const work of works) {
479
+ const r = work.recovery;
480
+ switch (r.status) {
481
+ case 'awaiting-lead': {
482
+ // Пауза человека, наступившая после старта работы, — его решение:
483
+ // разбор её не обходит.
484
+ const control = this.engine.read((v) => v.control());
485
+ if (control.paused && control.pauseReason === 'operator')
486
+ throw new Error(`${work.id}: разбор свойства ${r.property} ждёт снятия паузы человеком`);
487
+ if ((job.repairs ?? 0) >= this.policy.repairBudget)
488
+ throw new Error(`${work.id}: исчерпан бюджет восстановлений; разбор свойства ${r.property} — решение человека`);
489
+ const reserved = this.workflows.repair(job, `recovery:${r.property}`, () => this.engine.recoveryReserve(work.id, r.id));
490
+ if (reserved.status !== 'analyzing')
491
+ throw new Error(`${work.id}: ${reserved.reason}`);
492
+ let decision;
493
+ try {
494
+ decision = await analyzeRecovery({
495
+ engine: this.engine,
496
+ policy: this.policy,
497
+ adapters: this.adapters,
498
+ stateDir: this.options.stateDir,
499
+ cwd: this.repoPath(this.policy.repositories.find((x) => x.id === work.order.repositoryId).path),
500
+ ...(this.options.environment ? { environment: this.options.environment } : {}),
501
+ }, this.engine.read((v) => v.work(work.id)), job.authorRuntime, AbortSignal.timeout(this.policy.runTimeoutMs + 5000));
502
+ }
503
+ catch (error) {
504
+ this.engine.recoveryInterrupted(work.id, r.id, 'Разбор ведущего не дал решения: ' +
505
+ (error instanceof Error ? error.message : String(error)));
506
+ throw error;
507
+ }
508
+ this.workflows.guard(job);
509
+ const decided = this.engine.recoveryDecide(work.id, r.id, decision);
510
+ if (decided.status === 'applying')
511
+ await this.applyRecovery(work.id);
512
+ return this.afterDecision(work.id);
513
+ }
514
+ case 'analyzing':
515
+ // Разбор оборвался без решения (рестарт): вызов уже учтён.
516
+ this.engine.recoveryInterrupted(work.id, r.id, 'Разбор ведущего прерван до решения; повторный автоматический вызов не выполняется.');
517
+ throw new Error(`${work.id}: разбор прерван до решения; нужен ведущий или человек`);
518
+ case 'applying':
519
+ // Добавление случаев идемпотентно по ключам эпизода.
520
+ await this.applyRecovery(work.id);
521
+ return this.afterDecision(work.id);
522
+ case 'retry':
523
+ case 'retrying':
524
+ return this.afterDecision(work.id);
525
+ default:
526
+ throw new Error(`${work.id}: ${r.reason ?? 'разбор свойства ' + r.property + ' ждёт решения'}`);
527
+ }
528
+ }
529
+ return false;
530
+ }
531
+ async applyRecovery(workId) {
532
+ const work = this.engine.read((v) => v.work(workId));
533
+ try {
534
+ this.engine.recoveryApplied(workId, work.recovery.id, await applyCases(this.runner, work));
535
+ }
536
+ catch (error) {
537
+ this.engine.recoveryInterrupted(workId, work.recovery.id, 'Добавление случаев не завершилось: ' +
538
+ (error instanceof Error ? error.message : String(error)));
539
+ throw error;
540
+ }
541
+ }
542
+ afterDecision(workId) {
543
+ const work = this.engine.read((v) => v.work(workId));
544
+ const r = work.recovery;
545
+ if (!r)
546
+ return false;
547
+ if (r.status === 'retry' && ['failed', 'cancelled'].includes(work.status)) {
548
+ if (work.status === 'cancelled')
549
+ throw new Error(`${workId}: поручение отменено человеком`);
550
+ this.engine.retry(workId);
551
+ return true;
552
+ }
553
+ if (r.status === 'retry' || r.status === 'retrying')
554
+ return true;
555
+ throw new Error(`${workId}: ${r.reason ?? r.status}`);
556
+ }
557
+ /**
558
+ * Восстановление упавшего поручения группы. Решение и его исполнение — одна
559
+ * транзакция: между чтением и записью никто не успеет отменить поручение.
560
+ * Возвращает true, когда повтор назначен; остановка — исключение с причиной.
561
+ */
562
+ repair(job, graphId) {
563
+ const decision = this.store.transact('task.retry', (tx) => {
564
+ const decided = repairDecision(tx, tx.works(graphId), {
565
+ budgetLeft: this.policy.repairBudget - (job.repairs ?? 0),
566
+ maxAttempts: this.policy.maxAttempts,
567
+ });
568
+ if (!decided || decided.action === 'stop')
569
+ return decided;
570
+ this.workflows.repair(job, `repair:${decided.kind}`, () => {
571
+ this.engine.retry(decided.workId);
572
+ // Повтор признал причину разовой: своя причина паузы снимается. Выдача
573
+ // возвращается, только когда не осталось ни одной; пауза человека
574
+ // не снимается вовсе.
575
+ if (decided.blocksQueue)
576
+ this.engine.clearFailure([decided.kind]);
577
+ });
578
+ return decided;
579
+ });
580
+ if (!decision)
581
+ return false;
582
+ if (decision.action === 'stop')
583
+ throw new Error(decision.reason);
584
+ return true;
585
+ }
586
+ status(graphId) {
587
+ return this.engine.read((v) => ({
588
+ stateVersion: STATE_VERSION,
589
+ control: v.control(),
590
+ ...(v.controller() ? { controller: v.controller() } : {}),
591
+ graphs: v
592
+ .graphs()
593
+ .filter((g) => !graphId || g.id === graphId)
594
+ .map((g) => ({
595
+ id: g.id,
596
+ digest: g.digest,
597
+ source: g.graph.source,
598
+ approved: g.approval?.digest === g.digest,
599
+ })),
600
+ works: v.works(graphId).map((w) => ({
601
+ id: w.id,
602
+ graphId: w.graphId,
603
+ status: w.status,
604
+ attempt: w.attempt,
605
+ ...(w.activeRunId ? { activeRunId: w.activeRunId } : {}),
606
+ ...(w.resultSha ? { resultSha: w.resultSha } : {}),
607
+ blockers: this.engine.blockers(w, v),
608
+ })),
609
+ ...((budgets) => (budgets ? { budgets } : {}))(budgetOverview(this.engine.budgetContext(), v)),
610
+ }));
611
+ }
612
+ events(after = 0, limit = 100) {
613
+ const events = this.store.events(after, Math.min(Math.max(limit, 1), 500));
614
+ return { events, next: events.at(-1)?.seq ?? after };
615
+ }
616
+ /** Результат завершённой попытки поручения (последней или `runId`) и состояние его доставки. */
617
+ result(workId, runId) {
618
+ return this.engine.read((v) => {
619
+ const work = v.work(workId);
620
+ if (!work)
621
+ throw new HarnessError('NOT_FOUND', 'Поручение не найдено: ' + workId);
622
+ const attempt = v
623
+ .attempts(workId)
624
+ .filter((a) => a.status !== 'active' && (!runId || a.id === runId))
625
+ .at(-1);
626
+ if (!attempt)
627
+ throw new HarnessError('NOT_FOUND', 'У поручения нет завершённой попытки: ' + workId);
628
+ return {
629
+ apiVersion: API_VERSION,
630
+ result: runResult(attempt, HARNESS_VERSION),
631
+ delivery: v
632
+ .locals('delivery', WORKSPACE_OWNER)
633
+ .map((d) => d.value)
634
+ .filter((d) => d.key.endsWith(':' + attempt.id)),
635
+ };
636
+ });
637
+ }
638
+ /**
639
+ * Доставка результата поручения получателю. Ключ — получатель, поручение и
640
+ * попытка: повтор не создаёт второй записи и не вызывает моделей или
641
+ * интеграции. Конфликт редакции записывается, результат её не закрывает.
642
+ */
643
+ async deliver(workId, sink) {
644
+ const envelope = this.result(workId);
645
+ const key = `${sink.id}:${workId}:${envelope.result.runId}`;
646
+ const prior = this.engine.read((v) => v.local('delivery', WORKSPACE_OWNER, key));
647
+ if (prior?.status === 'delivered')
648
+ return this.result(workId);
649
+ const base = prior ?? {
650
+ sink: sink.id,
651
+ key,
652
+ revision: envelope.result.source.revision,
653
+ status: 'pending',
654
+ attempts: 0,
655
+ updatedAt: new Date().toISOString(),
656
+ };
657
+ const save = (state, type) => this.store.transact('delivery.recorded', (tx) => {
658
+ // Подтверждённую доставку не перезаписывает поздний итог другого
659
+ // процесса (например, отказ «владелец занят» при параллельной доставке).
660
+ if (state.status !== 'delivered' &&
661
+ tx.local('delivery', WORKSPACE_OWNER, key)?.status === 'delivered')
662
+ return;
663
+ tx.putLocal('delivery', WORKSPACE_OWNER, key, state);
664
+ const payload = {
665
+ sink: state.sink,
666
+ key,
667
+ revision: state.revision,
668
+ attempts: state.attempts,
669
+ };
670
+ if (type === 'delivery.delivered')
671
+ tx.emit({
672
+ type,
673
+ workId,
674
+ payload: { ...payload, ...(state.receipt ? { receipt: state.receipt } : {}) },
675
+ });
676
+ else if (type === 'delivery.failed')
677
+ tx.emit({ type, workId, payload: { ...payload, error: state.lastError ?? 'ошибка' } });
678
+ else if (type === 'delivery.uncertain')
679
+ tx.emit({
680
+ type,
681
+ workId,
682
+ payload: {
683
+ ...payload,
684
+ operation: state.operation ?? 'unknown',
685
+ reason: state.lastError ?? 'исход не доказан',
686
+ },
687
+ });
688
+ else if (type === 'delivery.conflict')
689
+ tx.emit({
690
+ type,
691
+ workId,
692
+ payload: { ...payload, currentRevision: state.currentRevision ?? '?' },
693
+ });
694
+ else
695
+ tx.emit({ type, workId, payload });
696
+ });
697
+ if (!prior)
698
+ save(base, 'delivery.pending');
699
+ const attempt = { ...base, attempts: base.attempts + 1, updatedAt: new Date().toISOString() };
700
+ try {
701
+ const outcome = await sink.deliver({ key, result: envelope.result });
702
+ if (outcome.status === 'delivered')
703
+ save({
704
+ ...attempt,
705
+ status: 'delivered',
706
+ ...(outcome.receipt ? { receipt: outcome.receipt.slice(0, 1000) } : {}),
707
+ }, 'delivery.delivered');
708
+ else if (outcome.status === 'uncertain')
709
+ // Неизвестный исход — не успех и не отказ: повтор не посылает мутацию
710
+ // до явного разбора получателем (reconcile).
711
+ save({
712
+ ...attempt,
713
+ status: 'uncertain',
714
+ operation: outcome.operation.slice(0, 300),
715
+ lastError: ('Исход не доказан: ' + outcome.reason).slice(0, 4000),
716
+ }, 'delivery.uncertain');
717
+ else
718
+ save({
719
+ ...attempt,
720
+ status: 'conflict',
721
+ currentRevision: outcome.currentRevision.slice(0, 200),
722
+ }, 'delivery.conflict');
723
+ }
724
+ catch (error) {
725
+ save({
726
+ ...attempt,
727
+ status: 'failed',
728
+ lastError: String(error instanceof Error ? error.message : error).slice(0, 4000),
729
+ }, 'delivery.failed');
730
+ }
731
+ return this.result(workId);
732
+ }
733
+ async deliverAll(graphId, sink) {
734
+ const ids = this.engine.read((v) => v
735
+ .works(graphId)
736
+ .filter((w) => v.attempts(w.id).some((a) => a.status !== 'active'))
737
+ .map((w) => w.id));
738
+ for (const id of ids)
739
+ await this.deliver(id, sink);
740
+ }
741
+ }