adispatchd 0.0.0-stage → 0.1.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 (396) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -2
  3. package/dist/api/errors.d.ts +27 -0
  4. package/dist/api/errors.js +80 -0
  5. package/dist/api/errors.js.map +1 -0
  6. package/dist/api/index.d.ts +10 -0
  7. package/dist/api/index.js +11 -0
  8. package/dist/api/index.js.map +1 -0
  9. package/dist/api/logs.d.ts +30 -0
  10. package/dist/api/logs.js +88 -0
  11. package/dist/api/logs.js.map +1 -0
  12. package/dist/api/serialize.d.ts +65 -0
  13. package/dist/api/serialize.js +135 -0
  14. package/dist/api/serialize.js.map +1 -0
  15. package/dist/api/server.d.ts +89 -0
  16. package/dist/api/server.js +751 -0
  17. package/dist/api/server.js.map +1 -0
  18. package/dist/api/sse.d.ts +90 -0
  19. package/dist/api/sse.js +225 -0
  20. package/dist/api/sse.js.map +1 -0
  21. package/dist/app.d.ts +173 -0
  22. package/dist/app.js +488 -0
  23. package/dist/app.js.map +1 -0
  24. package/dist/cli/config-validate.d.ts +6 -0
  25. package/dist/cli/config-validate.js +39 -0
  26. package/dist/cli/config-validate.js.map +1 -0
  27. package/dist/cli/control.d.ts +31 -0
  28. package/dist/cli/control.js +186 -0
  29. package/dist/cli/control.js.map +1 -0
  30. package/dist/cli/http-client.d.ts +57 -0
  31. package/dist/cli/http-client.js +174 -0
  32. package/dist/cli/http-client.js.map +1 -0
  33. package/dist/cli/index.d.ts +14 -0
  34. package/dist/cli/index.js +14 -0
  35. package/dist/cli/index.js.map +1 -0
  36. package/dist/cli/init-templates.d.ts +20 -0
  37. package/dist/cli/init-templates.js +275 -0
  38. package/dist/cli/init-templates.js.map +1 -0
  39. package/dist/cli/init.d.ts +16 -0
  40. package/dist/cli/init.js +107 -0
  41. package/dist/cli/init.js.map +1 -0
  42. package/dist/cli/main.d.ts +2 -0
  43. package/dist/cli/main.js +25 -0
  44. package/dist/cli/main.js.map +1 -0
  45. package/dist/cli/program.d.ts +102 -0
  46. package/dist/cli/program.js +133 -0
  47. package/dist/cli/program.js.map +1 -0
  48. package/dist/cli/service.d.ts +27 -0
  49. package/dist/cli/service.js +252 -0
  50. package/dist/cli/service.js.map +1 -0
  51. package/dist/cli/start.d.ts +22 -0
  52. package/dist/cli/start.js +133 -0
  53. package/dist/cli/start.js.map +1 -0
  54. package/dist/cli/workflow-dry-run.d.ts +13 -0
  55. package/dist/cli/workflow-dry-run.js +144 -0
  56. package/dist/cli/workflow-dry-run.js.map +1 -0
  57. package/dist/config/field-catalog.d.ts +33 -0
  58. package/dist/config/field-catalog.js +172 -0
  59. package/dist/config/field-catalog.js.map +1 -0
  60. package/dist/config/filter-schema.d.ts +15 -0
  61. package/dist/config/filter-schema.js +110 -0
  62. package/dist/config/filter-schema.js.map +1 -0
  63. package/dist/config/index.d.ts +15 -0
  64. package/dist/config/index.js +16 -0
  65. package/dist/config/index.js.map +1 -0
  66. package/dist/config/issues.d.ts +27 -0
  67. package/dist/config/issues.js +37 -0
  68. package/dist/config/issues.js.map +1 -0
  69. package/dist/config/loader.d.ts +32 -0
  70. package/dist/config/loader.js +250 -0
  71. package/dist/config/loader.js.map +1 -0
  72. package/dist/config/manager.d.ts +153 -0
  73. package/dist/config/manager.js +276 -0
  74. package/dist/config/manager.js.map +1 -0
  75. package/dist/config/model.d.ts +334 -0
  76. package/dist/config/model.js +2 -0
  77. package/dist/config/model.js.map +1 -0
  78. package/dist/config/normalize.d.ts +20 -0
  79. package/dist/config/normalize.js +200 -0
  80. package/dist/config/normalize.js.map +1 -0
  81. package/dist/config/parameters.d.ts +37 -0
  82. package/dist/config/parameters.js +71 -0
  83. package/dist/config/parameters.js.map +1 -0
  84. package/dist/config/public.d.ts +6 -0
  85. package/dist/config/public.js +18 -0
  86. package/dist/config/public.js.map +1 -0
  87. package/dist/config/schemas.d.ts +538 -0
  88. package/dist/config/schemas.js +321 -0
  89. package/dist/config/schemas.js.map +1 -0
  90. package/dist/config/snapshot.d.ts +38 -0
  91. package/dist/config/snapshot.js +57 -0
  92. package/dist/config/snapshot.js.map +1 -0
  93. package/dist/config/validate.d.ts +33 -0
  94. package/dist/config/validate.js +346 -0
  95. package/dist/config/validate.js.map +1 -0
  96. package/dist/core/clock.d.ts +59 -0
  97. package/dist/core/clock.js +133 -0
  98. package/dist/core/clock.js.map +1 -0
  99. package/dist/core/duration.d.ts +19 -0
  100. package/dist/core/duration.js +93 -0
  101. package/dist/core/duration.js.map +1 -0
  102. package/dist/core/errors.d.ts +62 -0
  103. package/dist/core/errors.js +72 -0
  104. package/dist/core/errors.js.map +1 -0
  105. package/dist/core/ids.d.ts +57 -0
  106. package/dist/core/ids.js +88 -0
  107. package/dist/core/ids.js.map +1 -0
  108. package/dist/core/index.d.ts +13 -0
  109. package/dist/core/index.js +14 -0
  110. package/dist/core/index.js.map +1 -0
  111. package/dist/core/logger.d.ts +100 -0
  112. package/dist/core/logger.js +207 -0
  113. package/dist/core/logger.js.map +1 -0
  114. package/dist/core/paths.d.ts +84 -0
  115. package/dist/core/paths.js +138 -0
  116. package/dist/core/paths.js.map +1 -0
  117. package/dist/core/template.d.ts +136 -0
  118. package/dist/core/template.js +261 -0
  119. package/dist/core/template.js.map +1 -0
  120. package/dist/core/types.d.ts +544 -0
  121. package/dist/core/types.js +172 -0
  122. package/dist/core/types.js.map +1 -0
  123. package/dist/core/worker-protocol.d.ts +102 -0
  124. package/dist/core/worker-protocol.js +75 -0
  125. package/dist/core/worker-protocol.js.map +1 -0
  126. package/dist/executors/classify.d.ts +25 -0
  127. package/dist/executors/classify.js +54 -0
  128. package/dist/executors/classify.js.map +1 -0
  129. package/dist/executors/dispatcher.d.ts +35 -0
  130. package/dist/executors/dispatcher.js +68 -0
  131. package/dist/executors/dispatcher.js.map +1 -0
  132. package/dist/executors/env.d.ts +43 -0
  133. package/dist/executors/env.js +66 -0
  134. package/dist/executors/env.js.map +1 -0
  135. package/dist/executors/event-parser.d.ts +41 -0
  136. package/dist/executors/event-parser.js +106 -0
  137. package/dist/executors/event-parser.js.map +1 -0
  138. package/dist/executors/index.d.ts +12 -0
  139. package/dist/executors/index.js +13 -0
  140. package/dist/executors/index.js.map +1 -0
  141. package/dist/executors/log-capture.d.ts +57 -0
  142. package/dist/executors/log-capture.js +155 -0
  143. package/dist/executors/log-capture.js.map +1 -0
  144. package/dist/executors/process-identity.d.ts +124 -0
  145. package/dist/executors/process-identity.js +283 -0
  146. package/dist/executors/process-identity.js.map +1 -0
  147. package/dist/executors/shell-executor.d.ts +35 -0
  148. package/dist/executors/shell-executor.js +478 -0
  149. package/dist/executors/shell-executor.js.map +1 -0
  150. package/dist/executors/stream-redactor.d.ts +40 -0
  151. package/dist/executors/stream-redactor.js +89 -0
  152. package/dist/executors/stream-redactor.js.map +1 -0
  153. package/dist/executors/workspace.d.ts +10 -0
  154. package/dist/executors/workspace.js +47 -0
  155. package/dist/executors/workspace.js.map +1 -0
  156. package/dist/filters/evaluate.d.ts +77 -0
  157. package/dist/filters/evaluate.js +187 -0
  158. package/dist/filters/evaluate.js.map +1 -0
  159. package/dist/filters/fields.d.ts +35 -0
  160. package/dist/filters/fields.js +75 -0
  161. package/dist/filters/fields.js.map +1 -0
  162. package/dist/filters/index.d.ts +11 -0
  163. package/dist/filters/index.js +12 -0
  164. package/dist/filters/index.js.map +1 -0
  165. package/dist/filters/iso-date.d.ts +10 -0
  166. package/dist/filters/iso-date.js +42 -0
  167. package/dist/filters/iso-date.js.map +1 -0
  168. package/dist/filters/predicate.d.ts +24 -0
  169. package/dist/filters/predicate.js +178 -0
  170. package/dist/filters/predicate.js.map +1 -0
  171. package/dist/filters/priority.d.ts +51 -0
  172. package/dist/filters/priority.js +91 -0
  173. package/dist/filters/priority.js.map +1 -0
  174. package/dist/filters/pushdown.d.ts +12 -0
  175. package/dist/filters/pushdown.js +52 -0
  176. package/dist/filters/pushdown.js.map +1 -0
  177. package/dist/filters/shell-check.d.ts +110 -0
  178. package/dist/filters/shell-check.js +304 -0
  179. package/dist/filters/shell-check.js.map +1 -0
  180. package/dist/identities/github-app.d.ts +55 -0
  181. package/dist/identities/github-app.js +160 -0
  182. package/dist/identities/github-app.js.map +1 -0
  183. package/dist/identities/index.d.ts +6 -0
  184. package/dist/identities/index.js +7 -0
  185. package/dist/identities/index.js.map +1 -0
  186. package/dist/identities/jwt.d.ts +24 -0
  187. package/dist/identities/jwt.js +56 -0
  188. package/dist/identities/jwt.js.map +1 -0
  189. package/dist/lifecycle/context.d.ts +106 -0
  190. package/dist/lifecycle/context.js +170 -0
  191. package/dist/lifecycle/context.js.map +1 -0
  192. package/dist/lifecycle/engine.d.ts +185 -0
  193. package/dist/lifecycle/engine.js +551 -0
  194. package/dist/lifecycle/engine.js.map +1 -0
  195. package/dist/lifecycle/errors.d.ts +48 -0
  196. package/dist/lifecycle/errors.js +71 -0
  197. package/dist/lifecycle/errors.js.map +1 -0
  198. package/dist/lifecycle/index.d.ts +9 -0
  199. package/dist/lifecycle/index.js +9 -0
  200. package/dist/lifecycle/index.js.map +1 -0
  201. package/dist/lifecycle/performers.d.ts +66 -0
  202. package/dist/lifecycle/performers.js +179 -0
  203. package/dist/lifecycle/performers.js.map +1 -0
  204. package/dist/notifications/index.d.ts +6 -0
  205. package/dist/notifications/index.js +7 -0
  206. package/dist/notifications/index.js.map +1 -0
  207. package/dist/notifications/service.d.ts +115 -0
  208. package/dist/notifications/service.js +364 -0
  209. package/dist/notifications/service.js.map +1 -0
  210. package/dist/persistence/actions.d.ts +101 -0
  211. package/dist/persistence/actions.js +196 -0
  212. package/dist/persistence/actions.js.map +1 -0
  213. package/dist/persistence/blocks.d.ts +85 -0
  214. package/dist/persistence/blocks.js +140 -0
  215. package/dist/persistence/blocks.js.map +1 -0
  216. package/dist/persistence/cron-firings.d.ts +55 -0
  217. package/dist/persistence/cron-firings.js +70 -0
  218. package/dist/persistence/cron-firings.js.map +1 -0
  219. package/dist/persistence/database.d.ts +60 -0
  220. package/dist/persistence/database.js +124 -0
  221. package/dist/persistence/database.js.map +1 -0
  222. package/dist/persistence/errors.d.ts +39 -0
  223. package/dist/persistence/errors.js +67 -0
  224. package/dist/persistence/errors.js.map +1 -0
  225. package/dist/persistence/events.d.ts +72 -0
  226. package/dist/persistence/events.js +83 -0
  227. package/dist/persistence/events.js.map +1 -0
  228. package/dist/persistence/index.d.ts +18 -0
  229. package/dist/persistence/index.js +19 -0
  230. package/dist/persistence/index.js.map +1 -0
  231. package/dist/persistence/instance-lock.d.ts +26 -0
  232. package/dist/persistence/instance-lock.js +117 -0
  233. package/dist/persistence/instance-lock.js.map +1 -0
  234. package/dist/persistence/lock-keys.d.ts +16 -0
  235. package/dist/persistence/lock-keys.js +41 -0
  236. package/dist/persistence/lock-keys.js.map +1 -0
  237. package/dist/persistence/notifications.d.ts +113 -0
  238. package/dist/persistence/notifications.js +213 -0
  239. package/dist/persistence/notifications.js.map +1 -0
  240. package/dist/persistence/persistence.d.ts +46 -0
  241. package/dist/persistence/persistence.js +42 -0
  242. package/dist/persistence/persistence.js.map +1 -0
  243. package/dist/persistence/retention.d.ts +46 -0
  244. package/dist/persistence/retention.js +68 -0
  245. package/dist/persistence/retention.js.map +1 -0
  246. package/dist/persistence/runs.d.ts +215 -0
  247. package/dist/persistence/runs.js +417 -0
  248. package/dist/persistence/runs.js.map +1 -0
  249. package/dist/persistence/schema.d.ts +16 -0
  250. package/dist/persistence/schema.js +183 -0
  251. package/dist/persistence/schema.js.map +1 -0
  252. package/dist/persistence/sqlite.d.ts +49 -0
  253. package/dist/persistence/sqlite.js +131 -0
  254. package/dist/persistence/sqlite.js.map +1 -0
  255. package/dist/runs/controller.d.ts +307 -0
  256. package/dist/runs/controller.js +1078 -0
  257. package/dist/runs/controller.js.map +1 -0
  258. package/dist/runs/errors.d.ts +20 -0
  259. package/dist/runs/errors.js +26 -0
  260. package/dist/runs/errors.js.map +1 -0
  261. package/dist/runs/event-bus.d.ts +106 -0
  262. package/dist/runs/event-bus.js +105 -0
  263. package/dist/runs/event-bus.js.map +1 -0
  264. package/dist/runs/execution.d.ts +39 -0
  265. package/dist/runs/execution.js +146 -0
  266. package/dist/runs/execution.js.map +1 -0
  267. package/dist/runs/index.d.ts +12 -0
  268. package/dist/runs/index.js +13 -0
  269. package/dist/runs/index.js.map +1 -0
  270. package/dist/runs/retention.d.ts +64 -0
  271. package/dist/runs/retention.js +232 -0
  272. package/dist/runs/retention.js.map +1 -0
  273. package/dist/runs/workspace.d.ts +20 -0
  274. package/dist/runs/workspace.js +51 -0
  275. package/dist/runs/workspace.js.map +1 -0
  276. package/dist/scheduler/checks.d.ts +41 -0
  277. package/dist/scheduler/checks.js +111 -0
  278. package/dist/scheduler/checks.js.map +1 -0
  279. package/dist/scheduler/cron.d.ts +32 -0
  280. package/dist/scheduler/cron.js +83 -0
  281. package/dist/scheduler/cron.js.map +1 -0
  282. package/dist/scheduler/dry-run.d.ts +42 -0
  283. package/dist/scheduler/dry-run.js +167 -0
  284. package/dist/scheduler/dry-run.js.map +1 -0
  285. package/dist/scheduler/errors.d.ts +22 -0
  286. package/dist/scheduler/errors.js +22 -0
  287. package/dist/scheduler/errors.js.map +1 -0
  288. package/dist/scheduler/format.d.ts +3 -0
  289. package/dist/scheduler/format.js +73 -0
  290. package/dist/scheduler/format.js.map +1 -0
  291. package/dist/scheduler/index.d.ts +13 -0
  292. package/dist/scheduler/index.js +14 -0
  293. package/dist/scheduler/index.js.map +1 -0
  294. package/dist/scheduler/planner.d.ts +88 -0
  295. package/dist/scheduler/planner.js +255 -0
  296. package/dist/scheduler/planner.js.map +1 -0
  297. package/dist/scheduler/scheduler.d.ts +160 -0
  298. package/dist/scheduler/scheduler.js +680 -0
  299. package/dist/scheduler/scheduler.js.map +1 -0
  300. package/dist/scheduler/state.d.ts +26 -0
  301. package/dist/scheduler/state.js +25 -0
  302. package/dist/scheduler/state.js.map +1 -0
  303. package/dist/scheduler/types.d.ts +197 -0
  304. package/dist/scheduler/types.js +3 -0
  305. package/dist/scheduler/types.js.map +1 -0
  306. package/dist/service/index.d.ts +6 -0
  307. package/dist/service/index.js +7 -0
  308. package/dist/service/index.js.map +1 -0
  309. package/dist/service/install.d.ts +93 -0
  310. package/dist/service/install.js +250 -0
  311. package/dist/service/install.js.map +1 -0
  312. package/dist/service/unit.d.ts +50 -0
  313. package/dist/service/unit.js +171 -0
  314. package/dist/service/unit.js.map +1 -0
  315. package/dist/sources/github/adapter.d.ts +99 -0
  316. package/dist/sources/github/adapter.js +336 -0
  317. package/dist/sources/github/adapter.js.map +1 -0
  318. package/dist/sources/github/fields.d.ts +8 -0
  319. package/dist/sources/github/fields.js +72 -0
  320. package/dist/sources/github/fields.js.map +1 -0
  321. package/dist/sources/github/http.d.ts +100 -0
  322. package/dist/sources/github/http.js +288 -0
  323. package/dist/sources/github/http.js.map +1 -0
  324. package/dist/sources/github/index.d.ts +6 -0
  325. package/dist/sources/github/index.js +7 -0
  326. package/dist/sources/github/index.js.map +1 -0
  327. package/dist/sources/github/pushdown.d.ts +14 -0
  328. package/dist/sources/github/pushdown.js +47 -0
  329. package/dist/sources/github/pushdown.js.map +1 -0
  330. package/dist/sources/github/refs.d.ts +17 -0
  331. package/dist/sources/github/refs.js +61 -0
  332. package/dist/sources/github/refs.js.map +1 -0
  333. package/dist/sources/index.d.ts +6 -0
  334. package/dist/sources/index.js +7 -0
  335. package/dist/sources/index.js.map +1 -0
  336. package/dist/ui/index.d.ts +12 -0
  337. package/dist/ui/index.js +28 -0
  338. package/dist/ui/index.js.map +1 -0
  339. package/dist/ui/public/app.css +239 -0
  340. package/dist/ui/public/favicon.svg +1 -0
  341. package/dist/ui/public/index.html +27 -0
  342. package/dist/ui/public/js/client/api.js +68 -0
  343. package/dist/ui/public/js/client/app.js +146 -0
  344. package/dist/ui/public/js/client/dom.js +34 -0
  345. package/dist/ui/public/js/client/page.js +9 -0
  346. package/dist/ui/public/js/client/pages/actions.js +35 -0
  347. package/dist/ui/public/js/client/pages/blocked.js +58 -0
  348. package/dist/ui/public/js/client/pages/config.js +126 -0
  349. package/dist/ui/public/js/client/pages/dashboard.js +175 -0
  350. package/dist/ui/public/js/client/pages/run.js +367 -0
  351. package/dist/ui/public/js/client/pages/runs.js +72 -0
  352. package/dist/ui/public/js/client/pages/views.js +80 -0
  353. package/dist/ui/public/js/client/pages/workflows.js +291 -0
  354. package/dist/ui/public/js/client/types.js +1 -0
  355. package/dist/ui/public/js/client/ui.js +152 -0
  356. package/dist/ui/public/js/shared/dom.js +77 -0
  357. package/dist/ui/public/js/shared/events.js +88 -0
  358. package/dist/ui/public/js/shared/format.js +213 -0
  359. package/dist/ui/public/js/shared/logs.js +136 -0
  360. package/dist/ui/public/js/shared/params.js +90 -0
  361. package/dist/ui/public/js/shared/route.js +36 -0
  362. package/dist/ui/public/js/shared/sort.js +70 -0
  363. package/dist/ui/public/js/shared/sse.js +193 -0
  364. package/dist/ui/public/js/shared/throttle.js +65 -0
  365. package/dist/views/index.d.ts +6 -0
  366. package/dist/views/index.js +7 -0
  367. package/dist/views/index.js.map +1 -0
  368. package/dist/views/service.d.ts +131 -0
  369. package/dist/views/service.js +264 -0
  370. package/dist/views/service.js.map +1 -0
  371. package/docs/api.md +239 -0
  372. package/docs/configuration-reference.md +1785 -0
  373. package/docs/cron-semantics.md +96 -0
  374. package/docs/manual-tests/github-fixture/config.yaml +30 -0
  375. package/docs/manual-tests/github-fixture/helpers.sh +93 -0
  376. package/docs/manual-tests/github-fixture/identities.yaml +18 -0
  377. package/docs/manual-tests/github-fixture/scripts/test-worker.sh +100 -0
  378. package/docs/manual-tests/github-fixture/sources.yaml +13 -0
  379. package/docs/manual-tests/github-fixture/views/dependencies.yaml +17 -0
  380. package/docs/manual-tests/github-fixture/views/needs-maintainer.yaml +17 -0
  381. package/docs/manual-tests/github-fixture/workflows/coder-test.yaml +76 -0
  382. package/docs/manual-tests/github-fixture/workflows/commenter-test.yaml +63 -0
  383. package/docs/manual-tests/github-fixture/workflows/reviewer-test.yaml +60 -0
  384. package/docs/manual-tests/github.md +1308 -0
  385. package/docs/manual-tests/process-supervision.md +64 -0
  386. package/docs/manual-tests/systemd.md +68 -0
  387. package/docs/operations.md +271 -0
  388. package/docs/worker-protocol.md +145 -0
  389. package/examples/config/config.yaml +35 -0
  390. package/examples/config/identities.yaml +18 -0
  391. package/examples/config/sources.yaml +13 -0
  392. package/examples/config/views/needs-maintainer.yaml +15 -0
  393. package/examples/config/workflows/coder-project-a.yaml +86 -0
  394. package/examples/config/workflows/deploy.yaml +15 -0
  395. package/examples/config/workflows/nightly-maintenance.yaml +13 -0
  396. package/package.json +75 -4
@@ -0,0 +1,1785 @@
1
+ # Konfigurationsreferenz
2
+
3
+ > **Verbindlichkeit.** Dieses Dokument ist die maßgebliche Referenz für alle Dateien im `config-dir`. Es **ersetzt
4
+ > die vorgeschlagenen Dateiformate** der Spezifikation ([spec-v0.1.md](spec-v0.1.md) §5, §6, §17, §18 und die
5
+ > YAML-Beispiele dort). Alles hier ist aus dem Code abgeleitet (`src/config/`, `src/core/`, `src/filters/`,
6
+ > `src/sources/github/`, `src/scheduler/`, `src/runs/`, `src/lifecycle/`, `src/executors/`,
7
+ > `src/notifications/`, `src/views/`). Weicht etwas vom Code ab, ist das ein Fehler dieses Dokuments.
8
+ >
9
+ > **Abgesichert durch Tests:** Alle YAML-Blöcke mit Kopfzeile `# file: <pfad>` werden von
10
+ > `src/config/reference-doc.test.ts` je Beispiel-Set (`# example: <name>`) in ein temporäres config-dir geschrieben
11
+ > und mit dem echten Loader validiert. Blöcke mit `# invalid` müssen die unter `# expect:` dokumentierte
12
+ > Fehlermeldung liefern. Kopfzeilen: `# example:` (Set-Name), `# extends:` (übernimmt zuerst alle Dateien eines
13
+ > anderen Sets), `# file:` (Zielpfad relativ zum config-dir), `# invalid`, `# expect:` / `# expect-warning:`
14
+ > (Teilstring von `<datei>: <pfad>: <meldung>`). Blöcke ohne `# file:` sind Ausschnitte und werden nicht geprüft.
15
+
16
+ ## Inhalt
17
+
18
+ 1. [Überblick](#1-überblick)
19
+ 2. [Gemeinsame Datentypen](#2-gemeinsame-datentypen)
20
+ 3. [Die Dateien](#3-die-dateien) – [config.yaml](#31-configyaml), [sources.yaml](#32-sourcesyaml),
21
+ [identities.yaml](#33-identitiesyaml), [workflows/*.yaml](#34-workflowsyaml), [views/*.yaml](#35-viewsyaml),
22
+ [Grenzen](#36-bekannte-grenzen-v1)
23
+ 4. [Filter-Sprache](#4-filter-sprache)
24
+ 5. [Platzhalter-Referenz](#5-platzhalter-referenz)
25
+ 6. [Auswertungsablauf](#6-auswertungsablauf)
26
+ 7. [Vollständige Beispiele](#7-vollständige-beispiele)
27
+ 8. [Häufige Validierungsfehler](#8-häufige-validierungsfehler)
28
+ 9. [Checkliste „neuen Workflow anlegen“](#9-checkliste-neuen-workflow-anlegen)
29
+
30
+ ---
31
+
32
+ ## 1. Überblick
33
+
34
+ ### 1.1 Verzeichnisstruktur
35
+
36
+ ```text
37
+ config-dir/
38
+ config.yaml globale Einstellungen optional (fehlt → Warnung, Defaults)
39
+ sources.yaml externe Quellen (V1: GitHub-Repositories) optional (fehlt → keine Sources)
40
+ identities.yaml GitHub-App-Identitäten optional (fehlt → keine Identities)
41
+ workflows/*.yaml ein Workflow pro Datei optional (fehlt → keine Workflows)
42
+ views/*.yaml eine View pro Datei optional
43
+ scripts/… frei; Skripte werden über Pfade referenziert (siehe §2.4)
44
+ ```
45
+
46
+ `adispatch init --config-dir D --data-dir D` legt eine kommentierte, gültige Beispielkonfiguration an
47
+ (`src/cli/init-templates.ts`): `config.yaml`, `sources.yaml`, `identities.yaml`, `workflows/hello.yaml`
48
+ (manuell, aktiv), `workflows/coder.yaml` und `workflows/nightly-maintenance.yaml` (beide `enabled: false`),
49
+ `views/needs-maintainer.yaml` und Beispielskripte unter `scripts/`. `init` überschreibt nie bestehende Dateien.
50
+
51
+ Das `data-dir` (SQLite, Logs, `runs/<id>/context.json|result.json`, Default-Workspaces) ist **keine**
52
+ Konfiguration und wird hier nur erwähnt, wo es Defaults bestimmt.
53
+
54
+ ### 1.2 Dateinamensregeln
55
+
56
+ | Regel | Verhalten |
57
+ |---|---|
58
+ | Top-Level-Dateien | exakt `config.yaml`, `sources.yaml`, `identities.yaml`. `.yml`-Varianten werden **nicht** gelesen. |
59
+ | `workflows/`, `views/` | alle Einträge, deren Name auf `.yaml` **oder** `.yml` endet und nicht mit `.` beginnt, alphabetisch sortiert. |
60
+ | Unterordner | werden **nicht** rekursiv gelesen (`workflows/alt/x.yaml` wird ignoriert). Ein *Verzeichnis* namens `x.yaml` ist ein Fehler („Not a regular file“). |
61
+ | Versteckte Dateien | `.foo.yaml` wird ignoriert (z. B. Editor-Backups). Andere Endungen (`x.yaml.bak`, `x.json`) ebenfalls. |
62
+ | Dateiname ↔ `id` | Maßgeblich ist das Feld `id`. Weicht der Dateiname (ohne Endung) davon ab, gibt es nur eine **Warnung** (nur für Workflows). Doppelte `id` über zwei Dateien ist ein **Fehler** in der alphabetisch späteren Datei. |
63
+ | Größe | max. 1 MiB pro Datei, sonst Fehler. |
64
+
65
+ ### 1.3 Kodierung und YAML-Dialekt
66
+
67
+ - Dateien werden als **UTF-8** gelesen (Parser `yaml`, YAML **1.2** Core-Schema).
68
+ - Folge von YAML 1.2: `yes`/`no`/`on`/`off` sind **Strings**, keine Booleans. Schreibe `true`/`false`.
69
+ (`enabled: yes` ist daher ein Typfehler.)
70
+ - Doppelte Schlüssel in einem Mapping sind ein Fehler („Map keys must be unique“).
71
+ - Anker/Aliase sind erlaubt (max. 100 Alias-Auflösungen pro Datei).
72
+ - Die oberste Ebene jeder Datei muss ein Mapping sein. Eine leere Datei gilt als leeres Mapping
73
+ (für `config.yaml` also „alle Defaults“; für einen Workflow fehlen dann Pflichtfelder).
74
+ - **Alle Objekte sind strikt:** unbekannte Schlüssel (Tippfehler) sind Fehler: `Unknown key(s): "…"`.
75
+ - Zahlen sind YAML-Zahlen. Wo ein String erwartet wird (Dauer, Größe, `args`), ist eine unquotierte Zahl ein
76
+ Typfehler. Ausnahme: Werte in `env`-Maps dürfen Zahl/Boolean sein und werden zu Strings (`PORT: 8080` → `"8080"`).
77
+
78
+ ### 1.4 Wie geladen und validiert wird
79
+
80
+ `config validate`, `start` und `config reload` verwenden denselben Loader (`src/config/loader.ts`):
81
+
82
+ 1. config-dir muss existieren und ein Verzeichnis sein.
83
+ 2. Alle Dateien werden gelesen und geparst (YAML-Syntaxfehler mit Zeilennummer).
84
+ 3. Jede Datei wird gegen ihr Schema geprüft (`src/config/schemas.ts`: Struktur, Typen, Dauern, Größen, Defaults).
85
+ Ist `config.yaml` ungültig, werden Workflows trotzdem gegen die **Default**-Globalwerte weitergeprüft.
86
+ 4. Semantische Prüfung über alle Dateien (`src/config/validate.ts`): Referenzen, eindeutige IDs, Platzhalter je
87
+ Kontext, Widersprüche, Cron/Zeitzone, Parameter-Defaults, Feldnamen/Operatoren/Werttypen in Filtern und Views
88
+ gegen den statischen Feldkatalog des Adaptertyps (`src/config/field-catalog.ts`, §4.3).
89
+ 5. **Alle** Probleme aller Dateien werden gesammelt (nicht beim ersten abgebrochen), sortiert nach Datei, Zeile,
90
+ Pfad, und als `datei:zeile: pfad: meldung` ausgegeben. Warnungen (`warning:`) verhindern das Laden nicht.
91
+
92
+ Folgefehler sind möglich: Ist z. B. `identities.yaml` ungültig, meldet `sources.yaml` zusätzlich
93
+ `Unknown identity "…"`. Den ersten Fehler zuerst beheben.
94
+
95
+ Der Loader liest **keine** Umgebungsvariablen. `config validate` prüft also nicht, ob `private_key_env`,
96
+ `url_env` oder `headers_env` gesetzt sind, und es prüft weder Skriptpfade auf Existenz noch App-Rechte oder die
97
+ Erreichbarkeit von GitHub. Feldnamen werden offline gegen den statischen Feldkatalog geprüft (§4.3).
98
+ Exit-Code: 0 = gültig (ggf. mit Warnungen), 1 = ungültig.
99
+
100
+ ### 1.5 Wann Änderungen wirken
101
+
102
+ Konfigurationsänderungen wirken **nur** durch einen manuellen Reload (`adispatch config reload`,
103
+ `POST /api/v1/config/reload`, UI „Reload“) oder einen Neustart. Es gibt keine Dateiüberwachung.
104
+
105
+ - **Atomar:** Der Reload lädt alles neu. Nur wenn **kein Fehler** auftritt, wird die neue Konfiguration aktiv;
106
+ sonst bleibt die bisherige vollständig aktiv (HTTP 422 `config_invalid` mit allen Fehlern).
107
+ - **Laufende Runs** verwenden den beim Reservieren gespeicherten Snapshot ihres Workflows (inkl. `execute`, Hooks,
108
+ `workspace`, effektiver Action-Retry-Werte und der Source-Konfiguration). Ein Reload ändert laufende Runs und
109
+ deren spätere Hooks nicht; auch manuelle Retries von Actions nutzen den Snapshot.
110
+ - Nach einem erfolgreichen Reload läuft sofort ein Planungsschritt.
111
+
112
+ | Bereich | Wirkung nach erfolgreichem Reload |
113
+ |---|---|
114
+ | `server.host`, `server.port` | erst nach Neustart |
115
+ | `server.allowed_hosts` | sofort (nächster Request) |
116
+ | `scheduler.polling_interval` | für den nächsten Timer (nach dem laufenden Planungsschritt) |
117
+ | `scheduler.max_concurrent` | sofort |
118
+ | `workspace.*`, `lifecycle.action_retry` | für neu reservierte Runs (Snapshot) |
119
+ | `retention.*` | ab dem nächsten Retention-Durchlauf; `cleanup_interval` bestimmt den Timer nach jedem Durchlauf |
120
+ | `notifications.*` | sofort für neue Benachrichtigungen; offene Zustellungen an entfernte Kanäle scheitern endgültig |
121
+ | `sources.yaml`, `identities.yaml` | Adapter/Credentials werden neu aufgebaut, wenn sich etwas geändert hat; entfernte bleiben für laufende Runs verfügbar |
122
+ | Workflows | sofort für Planung/Cron; geänderte Cron-Ausdrücke planen ab „jetzt“; Cache der Workflow-Conditions wird geleert |
123
+ | Views | sofort; View-Cache wird verworfen |
124
+ | Umgebungsvariablen (Keys, Webhook-URLs, Header) | gehören zum Prozess, nicht zur Konfiguration → Änderung erfordert **Neustart** |
125
+
126
+ **Runtime-Overrides** (UI bzw. `PATCH /api/v1/workflows/:id/runtime`, `PATCH /api/v1/scheduler/runtime`):
127
+ pro Workflow `enabled`, `priority`, `maxConcurrent`, global `maxConcurrent`. Sie werden **nicht persistiert**
128
+ (Neustart = weg), gehören nicht zum Run-Snapshot und wirken nur auf das Scheduling. Beim Reload:
129
+
130
+ - ein Workflow-Override wird **pro Feld** zurückgesetzt, wenn sich genau dieser konfigurierte Wert geändert hat;
131
+ andere Änderungen am Workflow lassen ihn bestehen;
132
+ - ein entfernter Workflow verliert alle Overrides;
133
+ - das globale Override wird zurückgesetzt, wenn sich `scheduler.max_concurrent` geändert hat;
134
+ - die Antwort nennt betroffene Workflows in `resetOverrides` (`"*"` = global).
135
+
136
+ ---
137
+
138
+ ## 2. Gemeinsame Datentypen
139
+
140
+ ### 2.1 Dauer
141
+
142
+ Syntax: `<zahl><einheit>` – genau **eine** Zahl und **eine** Einheit, optional umgebende Leerzeichen.
143
+
144
+ | Einheit | Bedeutung |
145
+ |---|---|
146
+ | `ms` | Millisekunden |
147
+ | `s` | Sekunden |
148
+ | `m` | Minuten |
149
+ | `h` | Stunden |
150
+ | `d` | Tage (24 h) |
151
+
152
+ - Dezimalzahlen sind erlaubt, wenn das Ergebnis ganze Millisekunden ergibt: `1.5h`, `0.5s` ✓, `0.0001s` ✗.
153
+ - Nicht erlaubt: zusammengesetzte Formen (`1h30m` → `90m`), Leerzeichen zwischen Zahl und Einheit (`6 h`),
154
+ negative Werte, fehlende Einheit, Großbuchstaben (`30S`), unquotierte Zahlen (`timeout: 30`).
155
+ - `0s` ist syntaktisch gültig, aber nur dort erlaubt, wo 0 einen Sinn hat (Retry-`delay`, `cancel_grace`,
156
+ `cache`, `retention.*_runs`). **> 0** verlangen: `execute.timeout.max_duration`, `execute.timeout.idle_timeout`,
157
+ `timeout` von Conditions, `shell_filter` und `shell`-Actions. **≥ 1 s** verlangen: `scheduler.polling_interval`
158
+ und `retention.cleanup_interval` (ein Null-Intervall würde die Schleife ohne Pause laufen lassen).
159
+ - Wo `never` erlaubt ist (`retention.*_runs`), bedeutet es „nie löschen“.
160
+
161
+ ### 2.2 Größe
162
+
163
+ Syntax: `<zahl>[ ]<einheit>`, Einheit ohne Beachtung der Groß-/Kleinschreibung: `B`, `KB`, `MB`, `GB`, `KiB`,
164
+ `MiB`, `GiB`. **Alle** Einheiten sind binär (`1 KB = 1024 B`, `100MB = 104 857 600 B`). Dezimalzahlen erlaubt
165
+ (abgerundet auf ganze Bytes). Eine Zahl ohne Einheit ist ungültig.
166
+
167
+ ### 2.3 IDs und Namen
168
+
169
+ | Art | Regel | Verwendung |
170
+ |---|---|---|
171
+ | Konfigurations-ID | `^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$` – Buchstaben, Ziffern, `_`, `-`; beginnt mit Buchstabe/Ziffer; 1–64 Zeichen; Groß-/Kleinschreibung zählt | Workflow-`id`, View-`id`, Source- und Identity-Schlüssel, Action-`id`, Kanal-`id`, alle Referenzen darauf |
172
+ | Parametername | `^[A-Za-z_][A-Za-z0-9_-]*$` | Schlüssel unter `parameters`, `{parameters.<name>}` |
173
+ | Env-Variablenname | `^[A-Za-z_][A-Za-z0-9_]*$` | Schlüssel in `env`/`secrets`, `private_key_env`, `url_env`, Werte in `headers_env` |
174
+ | Präfix `ADISPATCH_` | in `env`/`secrets` verboten (Groß-/Kleinschreibung egal) | reserviert für das Worker-Protokoll |
175
+ | HTTP-Header-Name | `^[A-Za-z0-9-]+$` | Schlüssel in `headers_env` |
176
+
177
+ Source-IDs sind Teil des globalen Item-Schlüssels `<source-id>:<item-id>` (z. B. `project_a:123`), der für Locks,
178
+ Retry-Blocks, Run-Historie und Views benutzt wird. **Umbenennen einer Source trennt die Historie.**
179
+
180
+ ### 2.4 Pfade und Kommandos
181
+
182
+ | Feld | Auflösung |
183
+ |---|---|
184
+ | `workspace.base_dir` | absolut bleibt; relativ → relativ zum **config-dir** (nicht zum Arbeitsverzeichnis). Default `<data-dir>/workspaces`. |
185
+ | `command` in `execute`, `conditions.*`, `shell_filter`, `shell`-Actions | absolut → normalisiert; enthält `/` (`scripts/x.sh`, `./x.sh`, `../x`) → relativ zum **config-dir**; ohne `/` (`bash`, `gh`) → bleibt Name für die **PATH-Suche** zur Laufzeit (PATH der Minimalumgebung, §6.11). |
186
+ | `execute.working_directory` | Platzhalter-Template im Modus `path` (§2.5); muss mit `/` oder `{workspace.base_dir}` beginnen. |
187
+
188
+ Es gibt **keine Shell**: `command` ist genau ein ausführbares Programm, Argumente kommen ausschließlich aus `args`
189
+ (Liste, ein Element = ein Argument, keine Wortzerlegung, keine Globs, keine `$VAR`-Expansion).
190
+ `command: 'test -f /x'` ist falsch (würde als Pfad `config-dir/test -f /x` aufgelöst). Für Pipes etc.:
191
+ `command: /bin/sh`, `args: ['-c', '…']` – dann liegt die Quotierung beim Autor. `command` ist **kein**
192
+ Template; Platzhalter in `command` werden nicht ersetzt. Existenz/Ausführbarkeit wird erst zur Laufzeit geprüft
193
+ (→ `start_error` bzw. Prüffehler).
194
+
195
+ ### 2.5 Platzhalter-Syntax
196
+
197
+ - Form `{name}`, z. B. `{run.id}`, `{parameters.version}`; Leerzeichen innerhalb der Klammern werden ignoriert.
198
+ - Nur die feste Liste aus §5 ist erlaubt; jeder andere Name ist ein Konfigurationsfehler
199
+ (`Unknown placeholder "{…}"`), ebenso ein Platzhalter außerhalb seines Kontexts
200
+ (`… is not available in this context`) und ein nicht deklarierter Parameter (`Parameter "x" is not declared`).
201
+ - **Escaping:** `{{` ergibt `{`, `}}` ergibt `}`. Ein einzelnes `}` oder ein nicht geschlossenes `{` ist ein
202
+ Syntaxfehler. Beispiel: `'{{"run":"{run.id}"}}'` → `{"run":"run-2026…"}`.
203
+ - Templates sind nur folgende Felder: `execute.working_directory` (Modus `path`), `execute.args[]`,
204
+ `execute.env.*`, `conditions.workflow[].args[]`, `conditions.item[].args[]`, `source.comment.message`,
205
+ `shell`-Action `args[]` und `env.*` (alle Modus `text`). **Keine** Templates: `command`, Labels in
206
+ `source.update`, Filterwerte, `secrets`, alle übrigen Felder.
207
+
208
+ **Modus `text`:** Werte werden unverändert eingesetzt (Zahlen/Booleans als `5`, `true`). Ein erlaubter Platzhalter
209
+ ohne Wert wird zu einem **leeren String**.
210
+
211
+ **Modus `path`** (nur `working_directory`):
212
+
213
+ - `{workspace.base_dir}` ist eine vertrauenswürdige Wurzel: nur ganz am Anfang erlaubt und muss von `/` oder dem
214
+ Ende gefolgt werden (`{workspace.base_dir}{run.id}` ist ein Fehler).
215
+ - Jeder andere Wert wird zu **genau einem** sicheren Pfadsegment: jedes Zeichen außer `[A-Za-z0-9._@+=,-]` wird
216
+ `_` (auch `/`, `\`, Leerzeichen, NUL); ein Wert nur aus Punkten wird zu Unterstrichen; leer → `_`; max. 128 Zeichen.
217
+ - Literale Teile des Templates werden **nicht** bereinigt; das Ergebnis wird normalisiert (`..` im Literal wird
218
+ aufgelöst – das ist Sache des Autors).
219
+ - Ein Platzhalter ohne Wert ist ein **Fehler** (Run endet als `start_error`). Deshalb dürfen im
220
+ `working_directory` nur Parameter stehen, die `required` sind oder einen `default` haben.
221
+ - Das Ergebnis muss absolut sein und darf nicht `workspace.base_dir` selbst sein.
222
+
223
+ **Sicherheit:** Ersetzte Werte (z. B. `{item.title}`, `{result.message}`) stammen aus externen Quellen und sind
224
+ untrusted. In `args` sind sie wegen der fehlenden Shell unkritisch; wer sie über `/bin/sh -c` interpoliert,
225
+ erzeugt eine Injection. In `source.comment.message` wird `<!--` zu `&lt;!--` neutralisiert (§6.9).
226
+
227
+ ---
228
+
229
+ ## 3. Die Dateien
230
+
231
+ ### 3.1 config.yaml
232
+
233
+ Alle Schlüssel sind optional. Fehlt die Datei, gelten alle Defaults (mit Warnung).
234
+
235
+ | Schlüssel | Typ | Default | Erlaubte Werte / Regeln | Bedeutung |
236
+ |---|---|---|---|---|
237
+ | `server.host` | String | `127.0.0.1` | nicht leer | Bind-Adresse von API/UI. Keine Authentifizierung – nur Loopback oder hinter authentifizierendem Proxy/VPN. |
238
+ | `server.port` | Integer | `8787` | 1–65535 | TCP-Port. |
239
+ | `server.allowed_hosts` | Liste String | `[]` | je `name` oder `name:port`; IPv6 in Klammern (`[::1]:8787`) | zusätzliche Namen, die im `Host`-Header akzeptiert werden (DNS-Rebinding-Schutz), z. B. der öffentliche Name des Reverse-Proxys. Vergleich ohne Groß-/Kleinschreibung; `name` = jeder Port, `name:port` = nur dieser Port. Ein `Host` **ohne** Port (Standardport des Schemas, z. B. hinter einem HTTPS-Proxy) passt auf `name`, `name:80` und `name:443`. Immer erlaubt: `localhost`, `127.0.0.1`, `[::1]` und – wenn nicht Loopback/Wildcard – `server.host` selbst. |
240
+ | `scheduler.polling_interval` | Dauer | `30s` | ≥ 1 s | Pause zwischen zwei Planungsschritten (gemessen ab Ende des vorherigen). |
241
+ | `scheduler.max_concurrent` | Integer | `5` | ≥ 1 | Globales Limit für **automatische** Starts. Gezählt werden alle aktiven Runs (inkl. manueller und `recovery_pending`). |
242
+ | `workspace.base_dir` | String | `<data-dir>/workspaces` | relativ → zum config-dir | Wurzel der Run-Workspaces; nur Verzeichnisse **strikt darunter** werden symlink-sicher angelegt und automatisch gelöscht. |
243
+ | `workspace.cleanup` | Enum | `on_success` | `always`, `on_success`, `never` | Wann der Workspace nach Run-Ende gelöscht wird (§6.12). |
244
+ | `lifecycle.action_retry.max_attempts` | Integer | `3` | ≥ 1 | Standard-Anzahl **Versuche insgesamt** pro Lifecycle-Action. |
245
+ | `lifecycle.action_retry.delay` | Dauer | `30s` | ≥ 0 | Pause zwischen Versuchen (mind. ein `Retry-After` der API). |
246
+ | `retention.completed_runs` | Dauer \| `never` | `30d` | | Aufbewahrung erfolgreicher Runs (`completed` + `success`). |
247
+ | `retention.failed_runs` | Dauer \| `never` | `90d` | | alle anderen beendeten Runs (fachlich `failure`, technisch gescheitert, `cancelled`), soweit nicht „blocked“. |
248
+ | `retention.blocked_runs` | Dauer \| `never` | `never` | | Runs, deren technischer Fehler in einen Retry-Block gezählt wurde. |
249
+ | `retention.max_log_size_per_run` | Größe | `100MB` | | Log-Limit des Haupt-Workers pro Run (stdout+stderr); Überlauf wird markiert. |
250
+ | `retention.cleanup_interval` | Dauer | `24h` | ≥ 1 s | Abstand der Retention-Durchläufe (erster Lauf 60 s nach Bereitschaft). |
251
+ | `notifications.on` | Liste Enum | alle drei | `retry_block_created`, `lifecycle_action_failed`, `recovery_failed` | global aktivierte Ereignisse. `[]` = keine Benachrichtigungen. |
252
+ | `notifications.channels` | Liste | `[]` | | Zustellkanäle, siehe unten. |
253
+
254
+ Retention löscht nie aktive Runs, Runs mit unfertiger oder gescheiterter Lifecycle-Kette und Runs eines **aktiven**
255
+ Retry-Blocks. Gelöscht werden DB-Zeilen, `runs/<id>/`, `logs/<id>/` und der Workspace (nur unter `base_dir`).
256
+
257
+ **Kanal `type: webhook`:**
258
+
259
+ | Schlüssel | Typ | Pflicht | Default | Bedeutung |
260
+ |---|---|---|---|---|
261
+ | `type` | `webhook` | ja | | einziger Kanaltyp in V1 |
262
+ | `id` | Konfigurations-ID | nein | `channel-<n>` (1-basiert nach Position) | eindeutig innerhalb der Kanäle |
263
+ | `url` | String | genau eines von `url`/`url_env` | | absolute `http(s)`-URL. Wird intern redigiert, kann aber Tokens enthalten → besser `url_env`. |
264
+ | `url_env` | Env-Name | genau eines von `url`/`url_env` | | Umgebungsvariable mit der URL (gelesen bei jeder Zustellung; fehlt sie → Zustellung scheitert endgültig). |
265
+ | `headers_env` | Map Header-Name → Env-Name | nein | `{}` | zusätzliche HTTP-Header, Werte aus Umgebungsvariablen (fehlt eine → Zustellung scheitert endgültig). |
266
+ | `on` | Liste Enum | nein | `notifications.on` | Ereignisse für diesen Kanal. Wirksam ist nur die Schnittmenge mit `notifications.on`. |
267
+
268
+ Zustellung: `POST` mit `content-type: application/json`, Body
269
+ `{"source":"adispatchd","id":…,"event":"…","createdAt":"…","text":"[adispatchd] …","payload":{…}}`
270
+ (Slack-kompatibles `text`), Timeout 10 s, keine Redirects. 2xx = Erfolg; 408/429/5xx/Netzfehler werden bis zu
271
+ 6-mal versucht (Pausen 5 s, 30 s, 2 min, 10 min, 30 min); andere Status sind endgültig. Ereignisse:
272
+ `retry_block_created` (Retry-Block entstanden), `lifecycle_action_failed` (Action endgültig gescheitert und
273
+ **ohne** `continue_on_error`), `recovery_failed` (Run nach Neustart nicht sicher zuordenbar → `recovery_pending`).
274
+
275
+ Vollständige `config.yaml` mit allen Schlüsseln (Defaults, außer wo kommentiert):
276
+
277
+ ```yaml
278
+ # example: reference
279
+ # file: config.yaml
280
+ server:
281
+ host: 127.0.0.1
282
+ port: 8787
283
+ allowed_hosts: [orchestrator.example.com] # Name des authentifizierenden Reverse-Proxys
284
+ scheduler:
285
+ polling_interval: 30s
286
+ max_concurrent: 5
287
+ workspace:
288
+ base_dir: /var/lib/adispatchd/workspaces # Default: <data-dir>/workspaces
289
+ cleanup: on_success # always | on_success | never
290
+ lifecycle:
291
+ action_retry:
292
+ max_attempts: 3
293
+ delay: 30s
294
+ retention:
295
+ completed_runs: 30d
296
+ failed_runs: 90d
297
+ blocked_runs: never
298
+ max_log_size_per_run: 100MB
299
+ cleanup_interval: 24h
300
+ notifications:
301
+ on: [retry_block_created, lifecycle_action_failed, recovery_failed]
302
+ channels:
303
+ - id: ops
304
+ type: webhook
305
+ url_env: OPS_WEBHOOK_URL
306
+ headers_env: { Authorization: OPS_WEBHOOK_AUTH }
307
+ on: [retry_block_created, recovery_failed]
308
+ ```
309
+
310
+ ### 3.2 sources.yaml
311
+
312
+ ```yaml
313
+ # example: reference
314
+ # file: sources.yaml
315
+ sources:
316
+ project_a: # Source-ID (Konfigurations-ID), Teil jedes Item-Schlüssels project_a:<nr>
317
+ type: github
318
+ owner: example # GitHub-Owner/Organisation
319
+ repository: project-a # Repository-Name
320
+ identity: maintainer # Identity für lesende Aufrufe (und Fallback für Schreiben)
321
+ project_b:
322
+ type: github
323
+ owner: example
324
+ repository: project-b
325
+ identity: maintainer
326
+ ```
327
+
328
+ | Schlüssel | Typ | Pflicht | Regeln | Bedeutung |
329
+ |---|---|---|---|---|
330
+ | `sources` | Map Source-ID → Source | nein (Default `{}`) | Schlüssel = Konfigurations-ID | alle Sources |
331
+ | `<id>.type` | `github` | ja | einziger Adapter in V1 | |
332
+ | `<id>.owner` | String | ja | nicht leer (Format nicht geprüft) | Owner/Organisation |
333
+ | `<id>.repository` | String | ja | nicht leer | Repository-Name; `{source.repository}` und Feld `repository` = `owner/repository` in der hier geschriebenen Schreibweise |
334
+ | `<id>.identity` | Identity-ID | ja | muss in `identities.yaml` existieren | liest Issues/PRs/Dependencies; Fallback für Schreibaktionen (§6.10) |
335
+
336
+ Nur `github.com` wird unterstützt (keine Einstellung für GitHub Enterprise). Gelesen werden Issues **und** Pull
337
+ Requests des Repositories über `GET /repos/{owner}/{repo}/issues`, standardmäßig **nur offene** (siehe §4.5).
338
+
339
+ ### 3.3 identities.yaml
340
+
341
+ ```yaml
342
+ # example: reference
343
+ # file: identities.yaml
344
+ identities:
345
+ maintainer:
346
+ type: github_app
347
+ app_id: 100001 # Zahl, keine Zeichenkette
348
+ installation_id: 200001
349
+ private_key_env: GITHUB_MAINTAINER_PRIVATE_KEY
350
+ coder:
351
+ type: github_app
352
+ app_id: 100002
353
+ installation_id: 200002
354
+ private_key_env: GITHUB_CODER_PRIVATE_KEY
355
+ ```
356
+
357
+ | Schlüssel | Typ | Pflicht | Regeln | Bedeutung |
358
+ |---|---|---|---|---|
359
+ | `identities` | Map Identity-ID → Identity | nein (Default `{}`) | Schlüssel = Konfigurations-ID | |
360
+ | `<id>.type` | `github_app` | ja | einziger Typ in V1 | |
361
+ | `<id>.app_id` | Integer | ja | > 0 | GitHub-App-ID (auch zur Erkennung eigener Kommentare) |
362
+ | `<id>.installation_id` | Integer | ja | > 0 | Installation der App im Ziel-Owner |
363
+ | `<id>.private_key_env` | Env-Name | ja | `^[A-Za-z_][A-Za-z0-9_]*$` | Name der Umgebungsvariable mit dem privaten Schlüssel |
364
+
365
+ **Secrets über die Umgebung:** Der Schlüssel steht nie in YAML. Die Variable muss in der Umgebung des
366
+ Orchestrator-Prozesses gesetzt sein (unter systemd: `EnvironmentFile`, Default `/etc/default/adispatchd`,
367
+ siehe [operations.md](operations.md)). Format des Werts: **RSA**-Private-Key im PEM-Format
368
+ (`-----BEGIN RSA PRIVATE KEY-----` bzw. PKCS#8 `-----BEGIN PRIVATE KEY-----`); akzeptiert werden echte
369
+ Zeilenumbrüche, wörtliche `\n`-Escapes (typisch in Env-Dateien), CRLF sowie umschließende `"…"`/`'…'`.
370
+ Gelesen wird der Schlüssel erst beim **ersten Token-Bedarf** (nicht beim Laden/Validieren); fehlt er oder ist er
371
+ ungültig, scheitert der jeweilige Zugriff mit einem nicht wiederholbaren Authentifizierungsfehler (Source-Fehler
372
+ in der Planungsdiagnose, `start_error` bei `execute.secrets`, endgültig gescheiterte Action). Aus dem Schlüssel
373
+ werden kurzlebige Installation-Tokens erzeugt und im Speicher gecacht; Schlüssel, JWT und Tokens werden in Logs
374
+ redigiert. Minimale App-Rechte: Issues lesen/schreiben, Pull Requests schreiben (Labels/Kommentare auf PRs),
375
+ Metadata lesen.
376
+
377
+ ### 3.4 workflows/*.yaml
378
+
379
+ Ein Workflow pro Datei. Vollständige Referenz mit (fast) allen Schlüsseln – ein Source-Workflow:
380
+
381
+ ```yaml
382
+ # example: reference
383
+ # file: workflows/full-reference.yaml
384
+ id: full-reference # Pflicht; Dateiname sollte <id>.yaml sein
385
+ name: Full Reference # Anzeigename, Default = id
386
+ enabled: true # Default true; false = keine automatischen Starts
387
+ priority: 10 # Default 0; höher = früher (auch negativ/dezimal erlaubt)
388
+ max_concurrent: 2 # Default 1; Limit gleichzeitiger automatischer Runs dieses Workflows
389
+ identity: coder # Default: Identity der Source; Schreib-Identity für Actions
390
+
391
+ trigger:
392
+ type: source # source | schedule | manual
393
+ source: project_a
394
+
395
+ filter: # deklarativ, nur bei trigger.type: source
396
+ all:
397
+ - { field: labels, operator: contains, value: ready }
398
+ - { field: is_pull_request, operator: equals, value: false }
399
+ - not: { field: labels, operator: contains, value: on-hold }
400
+ - any:
401
+ - { field: dependencies.blocked_by_open, operator: equals, value: 0 }
402
+ - { field: labels, operator: contains, value: ignore-dependencies }
403
+
404
+ shell_filter: # nur Source-Workflows; ein Programm ohne Argumente
405
+ command: scripts/filter.sh # bekommt ADISPATCH_CHECK_CONTEXT_FILE; Exit 0/1/≥2
406
+ timeout: 30s
407
+
408
+ item_priority:
409
+ rules: # erste zutreffende Regel (true) gewinnt
410
+ - when: { field: labels, operator: contains, value: 'priority:critical' }
411
+ value: 100
412
+ - when: { field: labels, operator: contains, value: 'priority:high' }
413
+ value: 50
414
+ default: 0
415
+
416
+ conditions:
417
+ workflow: # einmal pro Planungsschritt bzw. Cron-Slot
418
+ - type: shell
419
+ command: scripts/check-credits.sh
420
+ args: ['{workflow.id}']
421
+ timeout: 15s
422
+ cache: 60s # nur bei workflow-Conditions erlaubt
423
+ item: # pro Kandidat, nur Source-Workflows
424
+ - type: shell
425
+ command: scripts/check-item.sh
426
+ args: ['{item.key}', '{item.url}']
427
+ timeout: 30s
428
+
429
+ execute:
430
+ type: shell
431
+ command: scripts/worker.sh
432
+ args: ['--issue', '{item.number}', '--run', '{run.id}']
433
+ working_directory: '{workspace.base_dir}/{workflow.id}/{run.id}' # Default
434
+ create_directory: true
435
+ env:
436
+ NODE_ENV: production
437
+ ISSUE_URL: '{item.url}'
438
+ secrets:
439
+ GH_TOKEN: { identity: coder } # kurzlebiges Installation-Token der Identity
440
+ timeout:
441
+ max_duration: 6h
442
+ idle_timeout: 45m
443
+ cancel_grace: 30s
444
+
445
+ retry:
446
+ max_technical_failures: 3
447
+
448
+ on_start:
449
+ - type: source.update
450
+ labels: { add: [in-progress], remove: [ready] }
451
+ on_success:
452
+ - id: done-labels
453
+ type: source.update
454
+ labels: { add: [done], remove: [in-progress] }
455
+ - type: source.comment
456
+ message: 'Fertig ({result.result}): {result.message}'
457
+ identity: maintainer
458
+ continue_on_error: true
459
+ retry: { max_attempts: 5, delay: 1m }
460
+ - type: shell
461
+ command: scripts/notify.sh
462
+ args: ['{workflow.id}', '{run.id}', '{result.status}']
463
+ env: { CHANNEL: '#agents' }
464
+ timeout: 1m
465
+ on_failure:
466
+ - type: source.update
467
+ labels: { add: [needs-maintainer], remove: [in-progress] }
468
+ on_crash:
469
+ - type: source.update
470
+ labels: { add: [ready], remove: [in-progress] }
471
+ on_start_error:
472
+ - type: source.update
473
+ labels: { add: [ready], remove: [in-progress] }
474
+ on_cancel:
475
+ - type: source.update
476
+ labels: { add: [ready], remove: [in-progress] }
477
+ on_timeout:
478
+ - type: source.update
479
+ labels: { add: [ready], remove: [in-progress] }
480
+ ```
481
+
482
+ #### 3.4.1 Oberste Ebene
483
+
484
+ | Schlüssel | Typ | Pflicht | Default | Regeln | Bedeutung |
485
+ |---|---|---|---|---|---|
486
+ | `id` | Konfigurations-ID | ja | | eindeutig über alle Workflows | stabiler Schlüssel (Runs, Locks, Blocks, API) |
487
+ | `name` | String | nein | `id` | nicht leer | Anzeigename, `{workflow.name}` |
488
+ | `enabled` | Boolean | nein | `true` | | `false`: keine automatischen Starts (Source/Cron); **manuelle Starts bleiben möglich** |
489
+ | `priority` | Zahl | nein | `0` | | Workflow-Priorität, wird vor der Item-Priorität sortiert (§6.2) |
490
+ | `max_concurrent` | Integer | nein | `1` | ≥ 1 | Limit gleichzeitig aktiver Runs dieses Workflows für automatische Starts |
491
+ | `identity` | Identity-ID | nein | Source-Identity | muss existieren | Schreib-Identity für `source.update`/`source.comment` (§6.10) |
492
+ | `trigger` | Objekt | ja | | §3.4.2 | |
493
+ | `filter` | Filter-AST | nein | (alles passt) | nur `trigger.type: source` | §4 |
494
+ | `shell_filter` | Objekt | nein | | nur `source` | §3.4.4 |
495
+ | `item_priority` | Objekt | nein | Priorität 0 | nur `source` | §3.4.5 |
496
+ | `conditions` | Objekt | nein | keine | `item` nur bei `source` | §3.4.6 |
497
+ | `parameters` | Map | nein | `{}` | §3.4.7 | Parameter für manuelle Starts |
498
+ | `execute` | Objekt | ja | | §3.4.8 | Haupt-Worker |
499
+ | `retry.max_technical_failures` | Integer | nein | `3` | ≥ 1 | aufeinanderfolgende technische Fehler bis zum Retry-Block (§6.4) |
500
+ | `on_start`, `on_success`, `on_failure`, `on_crash`, `on_start_error`, `on_cancel`, `on_timeout` | Liste Actions | nein | `[]` | §3.4.10 | Lifecycle-Hooks |
501
+
502
+ #### 3.4.2 `trigger`
503
+
504
+ | `type` | weitere Schlüssel | Bedeutung |
505
+ |---|---|---|
506
+ | `source` | `source` (Pflicht, Source-ID, muss existieren) | Items der Source werden jeden Planungsschritt gepollt; ein Run ist an genau ein Item gebunden. |
507
+ | `schedule` | `cron` (Pflicht, nicht leer), `timezone` (Default `UTC`, IANA-Name wie `Europe/Berlin`) | höchstens ein Run pro Cron-Slot, ohne Item (§6.7). |
508
+ | `manual` | – | nur manuelle Starts (UI/API), ohne Item. |
509
+
510
+ Cron-Syntax: [croner](https://github.com/hexagon/croner) – 5 Felder (`min std tag monat wochentag`) oder 6 mit
511
+ führenden Sekunden; Listen, Bereiche, Schritte (`*/15`), Namen (`MON`, `JAN`) und croner-Erweiterungen (`L`, `#`)
512
+ werden akzeptiert. Ein Ausdruck, der nie feuert (`0 3 31 2 *`), ist ein Fehler. Jeder Workflow kann – unabhängig
513
+ vom Trigger – manuell gestartet werden; Source-Workflows dann mit Item-Referenz.
514
+
515
+ #### 3.4.3 `filter`
516
+
517
+ Deklarativer Filter-AST, siehe §4. Nur für Source-Workflows (sonst Fehler). Fehlt er, passt jedes Item der
518
+ Source (beachte §4.5: standardmäßig nur offene Issues/PRs).
519
+
520
+ #### 3.4.4 `shell_filter`
521
+
522
+ | Schlüssel | Typ | Pflicht | Default | Bedeutung |
523
+ |---|---|---|---|---|
524
+ | `command` | String | ja | | Programm (§2.4). **Keine `args`** – das Item kommt über `ADISPATCH_CHECK_CONTEXT_FILE`. |
525
+ | `timeout` | Dauer | nein | `30s` | > 0; danach `unknown` |
526
+
527
+ Exit `0` = passt, `1` = passt nicht, `≥ 2`/Signal/Timeout/Startfehler = `unknown` (kein Start). Nie gecacht, nie im
528
+ Dry-Run ausgeführt, nicht in Views. Umgebung und Kontextdatei: §6.11.
529
+
530
+ #### 3.4.5 `item_priority`
531
+
532
+ | Schlüssel | Typ | Default | Bedeutung |
533
+ |---|---|---|---|
534
+ | `rules` | Liste `{ when: <Filter-AST>, value: <Zahl> }` | `[]` | in Reihenfolge ausgewertet; die erste Regel mit Ergebnis `true` liefert die Priorität; `false`/`unknown` zählen nicht |
535
+ | `default` | Zahl | `0` | wenn keine Regel zutrifft |
536
+
537
+ Ohne `item_priority` ist die Item-Priorität 0. Regeln dürfen teure Felder verwenden (werden dann aufgelöst).
538
+
539
+ #### 3.4.6 `conditions`
540
+
541
+ `conditions.workflow[]` und `conditions.item[]` haben dieselbe Form:
542
+
543
+ | Schlüssel | Typ | Pflicht | Default | Regeln | Bedeutung |
544
+ |---|---|---|---|---|---|
545
+ | `type` | `shell` | ja | | | |
546
+ | `command` | String | ja | | §2.4 | Prüfprogramm |
547
+ | `args` | Liste String | nein | `[]` | Templates (§5) | |
548
+ | `timeout` | Dauer | nein | `30s` | > 0 | danach `unknown` |
549
+ | `cache` | Dauer | nein | kein Cache | **nur** `conditions.workflow` | Ergebnis `true`/`false` so lange wiederverwenden; `unknown` wird nie gecacht |
550
+
551
+ - `workflow`: werden – falls Kandidaten vorhanden – einmal pro Planungsschritt bzw. einmal pro Cron-Slot geprüft,
552
+ in Listenreihenfolge, Abbruch beim ersten Nicht-`true`. Erlaubt bei allen Triggern; bei `manual` wirkungslos
553
+ (manuelle Starts umgehen Conditions).
554
+ - `item`: pro Kandidat, nur Source-Workflows, nie gecacht.
555
+ - Exit-Semantik wie `shell_filter`. Dry-Runs führen Conditions nie aus.
556
+
557
+ #### 3.4.7 `parameters`
558
+
559
+ Map Parametername → Definition. Werte **übergeben** werden nur bei manuellen Starts (UI-Formular,
560
+ `POST /api/v1/workflows/:id/start` mit `{"parameters": {…}}`) und vor dem Start validiert; automatisch gestartete
561
+ Runs (Source, Cron) erhalten die `default`-Werte (siehe unten).
562
+
563
+ | Schlüssel | Typ | Pflicht | Default | Regeln |
564
+ |---|---|---|---|---|
565
+ | `type` | Enum | ja | | `string`, `number`, `boolean`, `enum` |
566
+ | `values` | Liste | bei `enum` ja | | nicht leer, eindeutig; Zahlen/Booleans werden zu Strings |
567
+ | `required` | Boolean | nein | `false` | |
568
+ | `default` | passend zum Typ | nein | keiner | `string` → String, `number` → endliche Zahl, `boolean` → Boolean, `enum` → einer der `values` (Skalare werden zu Strings, `values: [1, 2]` + `default: 1` ist gültig) |
569
+ | `description` | String | nein | | Anzeige im Formular |
570
+
571
+ Validierung beim Start: unbekannte Parameter → Fehler; fehlender/leerer Wert (`null`, `""`) → `default`, sonst
572
+ Fehler, wenn `required`; `number` akzeptiert auch numerische Strings (`"7"`), `boolean` auch `"true"`/`"false"`.
573
+
574
+ Regeln der Konfigurationsprüfung:
575
+
576
+ - `required: true` **ohne** `default` in einem `source`- oder `schedule`-Workflow ist ein Fehler (automatische
577
+ Runs könnten ihn nie liefern).
578
+ - Im `working_directory` nur Parameter mit `required: true` oder `default`.
579
+ - Automatisch gestartete Runs (Source, Cron) erhalten die **`default`-Werte** aller Parameter, die einen haben
580
+ (gespeichert am Run, sichtbar in API/UI und Kontextdatei). Parameter ohne Default bleiben dort leer
581
+ (`{parameters.x}` = leerer String).
582
+
583
+ #### 3.4.8 `execute`
584
+
585
+ | Schlüssel | Typ | Pflicht | Default | Regeln | Bedeutung |
586
+ |---|---|---|---|---|---|
587
+ | `type` | `shell` | ja | | einziger Executor in V1 | |
588
+ | `command` | String | ja | | §2.4 | Worker-Programm |
589
+ | `args` | Liste String | nein | `[]` | Templates, Modus `text` | Argumente |
590
+ | `working_directory` | String | nein | `{workspace.base_dir}/{workflow.id}/{run.id}` | Modus `path`; beginnt mit `/` oder `{workspace.base_dir}` | Arbeitsverzeichnis = Workspace (`ADISPATCH_WORKSPACE`) |
591
+ | `create_directory` | Boolean | nein | `true` | | Verzeichnis anlegen; bei `false` muss es existieren (sonst `start_error`) |
592
+ | `env` | Map Name → String/Zahl/Boolean | nein | `{}` | Templates; kein `ADISPATCH_`-Präfix | zusätzliche Umgebungsvariablen |
593
+ | `secrets` | Map Name → `{ identity: <id> }` | nein | `{}` | Identity muss existieren; Name nicht zugleich in `env`; kein `ADISPATCH_` | Installation-Token der Identity als Env-Variable (nur im Speicher) |
594
+ | `timeout.max_duration` | Dauer | nein | unbegrenzt | > 0 | Gesamtlaufzeit → `timed_out` |
595
+ | `timeout.idle_timeout` | Dauer | nein | unbegrenzt | > 0 | Zeit ohne Ausgabe auf stdout/stderr → `timed_out` (Warnung, wenn > `max_duration`) |
596
+ | `cancel_grace` | Dauer | nein | `30s` | | Wartezeit zwischen `SIGTERM` und `SIGKILL` an die Prozessgruppe bei Cancel/Timeout |
597
+
598
+ Workspace-Regeln: Liegt der gerenderte Pfad strikt unter `workspace.base_dir`, wird er sicher angelegt
599
+ (Symlink-geprüft) und gemäß `workspace.cleanup` gelöscht. Liegt er außerhalb, wird er wie konfiguriert angelegt,
600
+ aber **nie** automatisch gelöscht. Der Pfad darf nicht `base_dir` selbst sein. Eine Warnung erscheint, wenn ein
601
+ `env`-Schlüssel nach Secret aussieht (`…_TOKEN`, `…PASSWORD`, `API_KEY`, …) und einen nicht-leeren Literalwert hat.
602
+
603
+ #### 3.4.9 `retry`
604
+
605
+ `retry.max_technical_failures` (Integer ≥ 1, Default 3): Nach so vielen **aufeinanderfolgenden** technischen
606
+ Fehlern (`crashed`, `start_error`, `timed_out`) derselben Workflow/Item-Kombination entsteht ein Retry-Block (§6.4).
607
+
608
+ #### 3.4.10 Hooks
609
+
610
+ Sieben Hooks, jeder eine Liste von Actions, die **sequentiell** ausgeführt werden:
611
+
612
+ | Hook | läuft … |
613
+ |---|---|
614
+ | `on_start` | vor dem Start des Workers (nach Re-Read des Items). Scheitert die Kette, startet der Worker **nicht** (→ `start_error`). |
615
+ | `on_success` | nach `completed` + `success` |
616
+ | `on_failure` | nach `completed` + `failure` (fachlich, gültige Ergebnisdatei) |
617
+ | `on_crash` | nach `crashed` |
618
+ | `on_start_error` | nach `start_error` (sofern der Run `starting` erreicht hat) |
619
+ | `on_cancel` | nach `cancelled` (sofern der Run `starting` erreicht hat) |
620
+ | `on_timeout` | nach `timed_out` |
621
+
622
+ Genau ein Abschluss-Hook pro Run; Details in §6.8. `source.update`/`source.comment` nur in Source-Workflows.
623
+
624
+ #### 3.4.11 Actions
625
+
626
+ Gemeinsame Schlüssel aller Actions:
627
+
628
+ | Schlüssel | Typ | Pflicht | Default | Bedeutung |
629
+ |---|---|---|---|---|
630
+ | `type` | Enum | ja | | `source.update`, `source.comment`, `shell` |
631
+ | `id` | Konfigurations-ID | nein | `<hook>.<index>` (z. B. `on_success.1`) | stabiler Name im Journal; **workflowweit** eindeutig |
632
+ | `continue_on_error` | Boolean | nein | `false` | nach endgültigem Scheitern trotzdem mit der nächsten Action weitermachen |
633
+ | `retry.max_attempts` | Integer ≥ 1 | nein | `lifecycle.action_retry.max_attempts` | Versuche insgesamt |
634
+ | `retry.delay` | Dauer | nein | `lifecycle.action_retry.delay` | Pause zwischen Versuchen (`0s` erlaubt) |
635
+
636
+ `source.update` – Labels des Items ändern (nur Source-Workflows):
637
+
638
+ | Schlüssel | Typ | Pflicht | Regeln |
639
+ |---|---|---|---|
640
+ | `labels.add` | Liste String | nein (`[]`) | mindestens eines von `add`/`remove` nicht leer; kein Label in beiden |
641
+ | `labels.remove` | Liste String | nein (`[]`) | |
642
+
643
+ Kein Template. Vor dem Schreiben wird das Item neu gelesen; nur fehlende Labels werden hinzugefügt, nur vorhandene
644
+ entfernt (Vergleich ohne Groß-/Kleinschreibung) → idempotent. Schreib-Identity: Workflow → Source (§6.10).
645
+
646
+ `source.comment` – Kommentar am Issue/PR (nur Source-Workflows; Konversationskommentar, kein Review):
647
+
648
+ | Schlüssel | Typ | Pflicht | Bedeutung |
649
+ |---|---|---|---|
650
+ | `message` | String | ja | Template (Modus `text`); `result.*` nicht in `on_start` |
651
+ | `identity` | Identity-ID | nein | Action-Identity (vor Workflow- und Source-Identity) |
652
+
653
+ `shell` – beliebiges Programm mit Run-/Ergebnis-Kontext:
654
+
655
+ | Schlüssel | Typ | Pflicht | Default | Bedeutung |
656
+ |---|---|---|---|---|
657
+ | `command` | String | ja | | §2.4 |
658
+ | `args` | Liste String | nein | `[]` | Templates |
659
+ | `env` | Map | nein | `{}` | Templates, kein `ADISPATCH_`-Präfix |
660
+ | `timeout` | Dauer | nein | `5m` | Laufzeitlimit pro Versuch (> 0) |
661
+
662
+ Erfolg = Exit 0; alles andere (Exit ≠ 0, Signal, Timeout, Startfehler) = gescheiterter, wiederholbarer Versuch.
663
+ Keine Secrets. Arbeitsverzeichnis: der Run-Workspace, falls er existiert, sonst ein eigenes Verzeichnis pro Versuch
664
+ unter `data-dir/runs/<run>/actions/…`. SIGTERM→SIGKILL-Frist fest 5 s, Log-Limit fest 10 MiB pro Versuch.
665
+
666
+ ### 3.5 views/*.yaml
667
+
668
+ ```yaml
669
+ # example: reference
670
+ # file: views/reference.yaml
671
+ id: reference
672
+ title: Offene Arbeit
673
+ sources: [project_a, project_b] # gleicher Adaptertyp, eindeutig
674
+ filter:
675
+ all:
676
+ - { field: state, operator: in, value: [open, closed] } # sonst nur offene Items
677
+ - { field: run.last_status, operator: not_equals, value: null }
678
+ fields:
679
+ - { field: repository, label: Repository }
680
+ - { field: number, label: Nr. } # Link automatisch auf url (Felddefinition)
681
+ - { field: title, label: Titel, link: url }
682
+ - { field: labels, label: Labels }
683
+ - { field: run.last_workflow, label: Letzter Agent }
684
+ - { field: run.last_status, label: Status }
685
+ - { field: run.last_result.message, label: Ergebnis }
686
+ - { field: updated_at, label: Aktualisiert }
687
+ sort: { field: updated_at, direction: desc }
688
+ ```
689
+
690
+ | Schlüssel | Typ | Pflicht | Default | Regeln | Bedeutung |
691
+ |---|---|---|---|---|---|
692
+ | `id` | Konfigurations-ID | ja | | eindeutig über alle Views | |
693
+ | `title` | String | nein | `id` | nicht leer | |
694
+ | `sources` | Liste Source-ID | ja | | ≥ 1, existieren, eindeutig, gleicher Adaptertyp | Datenquellen |
695
+ | `filter` | Filter-AST | nein | alles | §4; inkl. `run.last_*` | nur Items mit Ergebnis `true` werden gezeigt |
696
+ | `shell_filter` | – | – | | **verboten** (Fehler) | Views führen nie Skripte aus |
697
+ | `fields` | Liste | ja | | ≥ 1 | Spalten in Reihenfolge |
698
+ | `fields[].field` | String | ja | | muss im Feldkatalog des Adaptertyps oder in §4.8 stehen | Feldname (§4.7, §4.8) |
699
+ | `fields[].label` | String | nein | Feldname | | Spaltenüberschrift |
700
+ | `fields[].link` | String (Feldname, geprüft wie `field`) | nein | `link` der Felddefinition (`number`, `title` → `url`) | | Feld, dessen Wert (nur absolute `http(s)`-URL) als Link dient |
701
+ | `sort.field` | String | ja (wenn `sort`) | | geprüft wie `fields[].field` | Sortierfeld |
702
+ | `sort.direction` | Enum | nein | `asc` | `asc`, `desc` | leere Werte immer zuletzt; Gleichstand nach Item-Schlüssel (natürlich) |
703
+
704
+ Laden: beim Öffnen/Refresh aus allen Sources (parallel; eine fehlerhafte Source erscheint als Fehlerzeile, die
705
+ anderen liefern trotzdem), Cache 30 s, max. 2 000 Zeilen. Nicht auflösbare Felder (z. B. fehlende API-Rechte) bleiben erkennbar leer; unbekannte Feldnamen sind bereits
706
+ ein Konfigurationsfehler (§4.3).
707
+
708
+ ### 3.6 Bekannte Grenzen (V1)
709
+
710
+ - `shell_filter` hat nur `command` und `timeout`: **keine `args`**, kein `env`. Das Item kommt ausschließlich über
711
+ `ADISPATCH_CHECK_CONTEXT_FILE`; Parameter für das Skript über einen Wrapper bzw. die Kontextdatei.
712
+ - Conditions (`conditions.workflow`, `conditions.item`) haben `args`, aber **kein `env`**; die Umgebung ist die
713
+ Minimalumgebung plus `ADISPATCH_CHECK_CONTEXT_FILE` (§6.11).
714
+ - `shell`-Actions: Frist zwischen `SIGTERM` und `SIGKILL` fest **5 s**, Log-Limit fest **10 MiB** pro Versuch –
715
+ nicht konfigurierbar (`execute.cancel_grace` und `retention.max_log_size_per_run` gelten nur für den Haupt-Worker).
716
+ - Prüfskripte (Conditions, `shell_filter`): Ausgabe wird bis 16 KiB je Stream erfasst, SIGTERM→SIGKILL nach 2 s.
717
+ - Nur Adapter `github` (github.com), nur Identity-Typ `github_app`, nur Kanaltyp `webhook`, nur Executor `shell`.
718
+
719
+ ---
720
+
721
+ ## 4. Filter-Sprache
722
+
723
+ Gemeinsam für `filter` (Workflows), `item_priority.rules[].when` und `filter` (Views).
724
+
725
+ ### 4.1 Grammatik
726
+
727
+ ```text
728
+ Knoten := All | Any | Not | Prädikat
729
+ All := { all: [ Knoten, … ] } # einziger Schlüssel; [] = true
730
+ Any := { any: [ Knoten, … ] } # einziger Schlüssel; [] = false
731
+ Not := { not: Knoten } # einziger Schlüssel
732
+ Prädikat := { field: <String>, operator: <Operator>, value: <Wert> } # genau diese drei Schlüssel
733
+ Operator := equals | not_equals | contains | in | greater_than | less_than
734
+ Wert := String | Zahl | Boolean | null # equals, not_equals, contains
735
+ | [ Skalar, … ] (nicht leer) # in
736
+ | Zahl | String (ISO-Datum) # greater_than, less_than
737
+ ```
738
+
739
+ `value` ist immer Pflicht (`null` explizit schreiben). Strukturfehler werden exakt verortet
740
+ (`filter.all[1].operator`). Eine Liste als `value` nimmt **nur** `in`; alle anderen Operatoren verlangen einen
741
+ Skalar (`greater_than`/`less_than`: Zahl oder Datums-String) – sonst Fehler.
742
+
743
+ ### 4.2 Operatoren – exakte Semantik
744
+
745
+ `a` = Feldwert, `v` = `value`. Ergebnis `true`/`false`/`unknown`; „unknown (Typ)“ heißt: inkompatible Typen (bei
746
+ katalogisierten Feldern verhindert die Werttyp-Prüfung beim Laden diesen Fall, Tabelle unten).
747
+
748
+ | Operator | Skalares Feld (string/number/boolean) | Datumsfeld (`date`) | Listenfeld (`string[]`) |
749
+ |---|---|---|---|
750
+ | `equals` | gleicher JS-Typ und gleicher Wert; anderer Typ → unknown (Typ). `'1'` vs `1`, `'true'` vs `true` → unknown | beide als ISO-8601 geparst, Vergleich als Zeitpunkt; nicht parsebar → unknown | nicht erlaubt (Validierungsfehler) |
751
+ | `not_equals` | Negation von `equals`; unknown bleibt unknown | dito | nicht erlaubt |
752
+ | `contains` | nur String-Feld: Teilstring (Groß-/Kleinschreibung beachtet) | nicht erlaubt | `true`, wenn ein Element `=== v`; leere Liste → `false`; kein Element vom Typ von `v` → unknown |
753
+ | `in` | `true`, wenn `equals` mit einem Listenelement `true` ist, sonst `false` (string/number) | nicht erlaubt | `true` bei nichtleerer Schnittmenge; leere Feldliste → `false`; keinerlei Typgleichheit → unknown |
754
+ | `greater_than` / `less_than` | Zahl vs Zahl numerisch (String-Felder: nicht erlaubt) | ISO-8601-Zeitpunkte | nicht erlaubt |
755
+
756
+ `null`-Regeln (ein Feldwert `null` bedeutet „bekannt leer“, z. B. kein Autor):
757
+
758
+ - `equals null` ist genau dann `true`, wenn der Feldwert `null` ist; `equals x` auf `null`-Feld → `false`;
759
+ `not_equals` entsprechend umgekehrt.
760
+ - `contains`/`in` auf `null`-Feld → `false`; `null` als Wert ist nur bei `equals`/`not_equals` erlaubt.
761
+ - `greater_than`/`less_than` mit `null` auf einer Seite → unknown.
762
+
763
+ Datumswerte: `YYYY-MM-DD` oder `YYYY-MM-DDTHH:MM[:SS[.fff]][Z|±HH:MM]`; ohne Offset = UTC. Relative Angaben
764
+ („vor 7 Tagen“) gibt es nicht. Datumssemantik gilt nur für Felder, die der Adapter als `date` deklariert.
765
+
766
+ **Erlaubte Operatoren je Feldtyp** (beim Laden geprüft, Spalte „Operatoren“ in §4.7/§4.8):
767
+
768
+ | Feldtyp | Operatoren | Erlaubte `value`-Typen |
769
+ |---|---|---|
770
+ | `string` | equals, not_equals, contains, in | String (`in`: Liste von Strings); `null` bei equals/not_equals |
771
+ | `number` | equals, not_equals, in, greater_than, less_than | Zahl (`in`: Liste von Zahlen); `null` bei equals/not_equals |
772
+ | `boolean` | equals, not_equals | Boolean (nicht `'true'`); `null` bei equals/not_equals |
773
+ | `date` | equals, not_equals, greater_than, less_than | ISO-8601-String (s. o.); `null` bei equals/not_equals |
774
+ | `string[]` | contains, in | String bzw. Liste von Strings |
775
+
776
+ **Groß-/Kleinschreibung:** `labels` (GitHub) vergleicht ohne Beachtung der Groß-/Kleinschreibung – wie GitHub
777
+ und `source.update` (`Ready` passt auf `ready`). Alle anderen String-Vergleiche beachten sie.
778
+
779
+ ### 4.3 Fehlende und unbekannte Felder
780
+
781
+ - **Beim Laden geprüft** gegen den statischen Feldkatalog des Adaptertyps der Source (Workflows: `filter`,
782
+ `item_priority.rules[].when`; Views: `filter`, `fields[].field`, `fields[].link`, `sort.field`): unbekannte
783
+ Feldnamen (mit Vorschlag „did you mean …“), nicht erlaubte Operatoren, falsche Werttypen, ungültige Datumswerte.
784
+ - `run.last_*` ist nur in Views erlaubt; in Workflows ist es ein Fehler („only available in views“).
785
+ - Kann ein bekanntes Feld zur Laufzeit nicht ermittelt werden (fehlender API-Zugriff, z. B. Cross-Repo-Blocker
786
+ ohne Rechte), ist es **unknown** – nie 0, leer oder `false` („fail closed“). `workflow dry-run` zeigt pro Regel
787
+ den Wert und den Grund.
788
+
789
+ ### 4.4 Dreiwertige Logik (Kleene)
790
+
791
+ | A | B | `all [A, B]` | `any [A, B]` | `not A` |
792
+ |---|---|---|---|---|
793
+ | true | true | true | true | false |
794
+ | true | false | false | true | false |
795
+ | true | unknown | unknown | true | false |
796
+ | false | false | false | false | true |
797
+ | false | unknown | false | unknown | true |
798
+ | unknown | unknown | unknown | unknown | unknown |
799
+
800
+ `all`: `false` dominiert, sonst unknown falls ein Kind unknown, sonst `true`. `any`: `true` dominiert, sonst
801
+ unknown falls ein Kind unknown, sonst `false`. Ergebnis für den Start: **nur `true` startet**. Bei Views wird nur
802
+ `true` angezeigt (unknown wird gezählt), in `item_priority` trifft nur `true`.
803
+
804
+ ### 4.5 Auswertungsreihenfolge, teure Felder, Pushdown
805
+
806
+ - **Teure Felder** (GitHub: `dependencies.*`) stehen nicht in der Listenabfrage und werden lazy nachgeladen – pro
807
+ Item in **einer** Anfragerunde für alle benötigten teuren Felder, gültig für einen Planungsschritt/View-Aufruf.
808
+ - Innerhalb von `all`/`any` werden zuerst Kinder ausgewertet, die nur billige Felder brauchen; sobald das Ergebnis
809
+ feststeht, wird abgebrochen (Rest = `not_evaluated` in der Diagnose). Workflow-Filter laufen zweistufig: erst nur
810
+ billige Felder (teure = unknown); ist das Ergebnis schon `true` oder `false`, ist es endgültig, nur bei unknown
811
+ werden teure Felder später (nach Lock/Block/Conditions) aufgelöst.
812
+ - **Pushdown:** Nur Prädikate der obersten Konjunktion (`all`, verschachtelte `all` werden abgeflacht; ein
813
+ einzelnes Prädikat zählt auch) mit billigem Feld und in `pushdown` deklariertem Operator gehen in die API-Abfrage.
814
+ `any`/`not` werden nie gepusht. Der vollständige Filter wird **immer** zusätzlich lokal ausgewertet.
815
+ - **GitHub-Abfrage:** `state` = `open` (Default!), `closed` oder `all`, abgeleitet aus `state equals open|closed`
816
+ bzw. `state in [...]` auf oberster Ebene; `labels contains "x"` (ohne Komma) → `labels=x` (alle müssen
817
+ vorhanden sein). **Ohne ein solches `state`-Prädikat werden nur offene Issues/PRs gelesen.**
818
+ - **Regel für `state`** (Workflow- und View-`filter`, geprüft beim Laden): `state` darf nur auf oberster Ebene
819
+ stehen (direkt oder in oberster `all`), nur mit `equals` oder `in` und nur mit den Werten `open`/`closed`.
820
+ Unter `any`/`not`, mit `not_equals`/`contains` oder mit anderen Werten wäre es stillschweigend wirkungslos
821
+ (es würden trotzdem nur offene Items gelesen) und ist daher ein Fehler. In `item_priority` ist `state` frei
822
+ verwendbar (beeinflusst die Abfrage nicht).
823
+
824
+ ### 4.6 Workflow-Filter vs. View-Filter
825
+
826
+ | | Workflow `filter` | View `filter` |
827
+ |---|---|---|
828
+ | Ergebnis unknown | kein Start (Diagnose `filter_unknown`) | Zeile ausgeblendet, gezählt in `filterUnknown` |
829
+ | Felder | Adapterfelder | Adapterfelder + `run.last_*` |
830
+ | Shell | zusätzlich `shell_filter`, Conditions | nie |
831
+ | Zeitpunkt | jeder Planungsschritt; vor `on_start` erneut der billige Teil auf dem frisch gelesenen Item | Öffnen/Refresh (Cache 30 s) |
832
+
833
+ ### 4.7 Feldkatalog des GitHub-Adapters
834
+
835
+ | Feld | Typ | Sinnvolle Operatoren | Pushdown | Teuer | Bedeutung |
836
+ |---|---|---|---|---|---|
837
+ | `number` | number | equals, not_equals, in, greater_than, less_than | | | Issue-/PR-Nummer (Link → `url`) |
838
+ | `title` | string | equals, not_equals, contains, in | | | Titel (Link → `url`) |
839
+ | `state` | string | equals, not_equals, contains, in – im `filter` nur `equals`/`in` auf oberster Ebene (§4.5) | equals, in | | `open` \| `closed` |
840
+ | `labels` | string[] | contains, in | contains | | Label-Namen; Vergleich **ohne** Groß-/Kleinschreibung |
841
+ | `assignees` | string[] | contains, in | | | Logins der Assignees |
842
+ | `author` | string | equals, not_equals, contains, in | | | Login des Autors (`null` falls unbekannt) |
843
+ | `is_pull_request` | boolean | equals, not_equals | | | `true` für Pull Requests |
844
+ | `created_at` | date | equals, not_equals, greater_than, less_than | | | Erstellungszeit |
845
+ | `updated_at` | date | equals, not_equals, greater_than, less_than | | | letzte Änderung |
846
+ | `url` | string | equals, not_equals, contains, in | | | Web-URL |
847
+ | `repository` | string | equals, not_equals, contains, in | | | `owner/repository` (wie in `sources.yaml`) |
848
+ | `dependencies.blocked_by_total` | number | equals, not_equals, in, greater_than, less_than | | ja | Anzahl blockierender Issues (native „blocked by“, inkl. Cross-Repo) |
849
+ | `dependencies.blocked_by_open` | number | equals, not_equals, in, greater_than, less_than | | ja | davon offen; unknown ohne Zugriff |
850
+ | `dependencies.blocked` | boolean | equals, not_equals | | ja | mindestens ein Blocker offen; unknown ohne Zugriff |
851
+ | `dependencies.status` | string | equals, not_equals, contains, in | | ja | `ok` = vollständig aufgelöst, sonst `unknown` (dieses Feld selbst ist dann bekannt) |
852
+
853
+ Abhängigkeiten sind unknown, wenn die API-Zusammenfassung fehlt, ein Blocker einen unerwarteten Status hat oder die
854
+ Identity weniger Blocker sieht, als GitHub meldet. „Offen ⇒ kein Start“ gilt nur, wenn der Filter das ausdrücklich
855
+ verlangt (z. B. `dependencies.blocked_by_open equals 0`).
856
+
857
+ ### 4.8 Run-Felder für Views
858
+
859
+ Pro Item der **zuletzt erzeugte** Run (über alle Workflows, globaler Item-Schlüssel). Ohne Run: alle `null`
860
+ (bekannt leer, also z. B. `run.last_status equals null` = „noch nie bearbeitet“).
861
+
862
+ Operatoren je Typ wie in §4.2 (`string`: equals, not_equals, contains, in; `date`: equals, not_equals,
863
+ greater_than, less_than – als Zeitpunkt verglichen).
864
+
865
+ | Feld | Typ | Bedeutung |
866
+ |---|---|---|
867
+ | `run.last_id` | string | Run-ID |
868
+ | `run.last_workflow` | string | Workflow-ID |
869
+ | `run.last_status` | string | technischer Status (`running`, `completed`, `crashed`, …) |
870
+ | `run.last_result` | string | fachliches Ergebnis `success`/`failure` (sonst `null`) |
871
+ | `run.last_result.message` | string | `message` der Ergebnisdatei |
872
+ | `run.last_status_message` | string | letzte Status-Meldung des Workers (`@@ADISPATCH_EVENT` `status`) |
873
+ | `run.last_started_at` | date | Start |
874
+ | `run.last_finished_at` | date | Ende |
875
+
876
+ ---
877
+
878
+ ## 5. Platzhalter-Referenz
879
+
880
+ ### 5.1 Werte
881
+
882
+ | Platzhalter | Wert | Fehlt, wenn … |
883
+ |---|---|---|
884
+ | `{workflow.id}` | Workflow-ID | nie |
885
+ | `{workflow.name}` | `name` (Default `id`) | nie |
886
+ | `{workspace.base_dir}` | absoluter, normalisierter `workspace.base_dir` | nie |
887
+ | `{timestamp}` | aktuelle UTC-Zeit beim Rendern, `YYYYMMDDTHHMMSSZ` | nie |
888
+ | `{run.id}` | Run-ID `run-YYYYMMDDTHHMMSSmmmZ-xxxxxxxx` | – |
889
+ | `{run.trigger}` | `source` \| `schedule` \| `manual` | – |
890
+ | `{run.started_at}` | ISO-8601 mit ms (UTC) | in `on_start` und in Hooks von Runs, deren Worker nie lief |
891
+ | `{workspace.path}` | absoluter Workspace-Pfad des Runs | in `on_start`; in Abschluss-Hooks, wenn der Worker nie gespawnt wurde |
892
+ | `{source.id}` | Source-ID | Run ohne Item |
893
+ | `{source.type}` | `github` | Run ohne Item |
894
+ | `{source.repository}` | `owner/repository` | Run ohne Item |
895
+ | `{item.id}` | Item-ID (GitHub: Nummer als String) | Run ohne Item |
896
+ | `{item.number}` | Nummer (nur bei numerischer Item-ID) | Run ohne Item |
897
+ | `{item.key}` | globaler Schlüssel `<source>:<id>` | Run ohne Item |
898
+ | `{item.url}` | Web-URL | Run ohne Item |
899
+ | `{item.title}` | Titel (frisch gelesen bzw. aus der Planung) | Run ohne Item |
900
+ | `{result.status}` | technischer Endstatus (`completed`, `crashed`, `start_error`, `timed_out`, `cancelled`) | – |
901
+ | `{result.result}` | `success` \| `failure` | bei technischem Fehler/Cancel |
902
+ | `{result.message}` | `message` der Ergebnisdatei | keine/ungültige Ergebnisdatei oder ohne `message` |
903
+ | `{parameters.<name>}` | Parameterwert (`true`, `5`, Text) | optionaler Parameter ohne Wert und ohne Default |
904
+
905
+ Fehlender Wert: Modus `text` → leerer String; Modus `path` → Fehler (`start_error`).
906
+
907
+ ### 5.2 Scope – wo welcher Platzhalter erlaubt ist
908
+
909
+ ✓ = erlaubt, – = Konfigurationsfehler, (s) = nur wenn `trigger.type: source`, (leer) = erlaubt, aber dort leer.
910
+
911
+ | Platzhalter | `execute.working_directory` (path) | `execute.args`, `execute.env` | `on_start` (`message`, `args`, `env`) | übrige Hooks | `conditions.workflow[].args` | `conditions.item[].args` |
912
+ |---|---|---|---|---|---|---|
913
+ | `workflow.*`, `workspace.base_dir`, `timestamp` | ✓ (`base_dir` nur am Anfang) | ✓ | ✓ | ✓ | ✓ | ✓ |
914
+ | `run.id`, `run.trigger` | ✓ | ✓ | ✓ | ✓ | – | – |
915
+ | `run.started_at` | ✓ | ✓ | ✓ (leer) | ✓ | – | – |
916
+ | `workspace.path` | – | ✓ | ✓ (leer) | ✓ | – | – |
917
+ | `source.*`, `item.*` | ✓ (s) | ✓ (s) | ✓ (s) | ✓ (s) | – | ✓ |
918
+ | `result.*` | – | – | – | ✓ | – | – |
919
+ | `parameters.<name>` | ✓ nur `required` oder mit `default` | ✓ | ✓ | ✓ | – | – |
920
+
921
+ Nicht als Template ausgewertet (Platzhalter bleiben wörtlich stehen): alle `command`, `source.update.labels`,
922
+ `shell_filter`, Filterwerte, `secrets`, alle anderen Felder.
923
+
924
+ ---
925
+
926
+ ## 6. Auswertungsablauf
927
+
928
+ ### 6.1 Planungsschritt (Source-Workflows)
929
+
930
+ Ein Planungsschritt läuft sofort beim Start, danach jeweils `polling_interval` nach Ende des vorherigen sowie direkt
931
+ nach jedem erfolgreichen Reload. Schritte überlappen nie; es gibt keine persistente Kandidaten-Warteschlange.
932
+
933
+ 0. **Gate:** Ist der Controller nicht bereit (Recovery läuft), pausiert oder im Drain/Shutdown, wird der ganze
934
+ Schritt übersprungen.
935
+ 1. **Pollen** (alle aktiven Source-Workflows parallel; deaktivierte erscheinen nur als `disabled`): Liste der Source
936
+ mit dem pushbaren Filterteil (gleiche Source + gleicher Pushdown = ein API-Aufruf pro Schritt). Fehler einer
937
+ Source betreffen nur deren Workflows (`source_error`).
938
+ 2. **Deduplizieren** pro `(Source, Item, Workflow)`.
939
+ 3. **Billiger Filter** (teure Felder = unknown): `false` → verworfen. **Item-Priorität** bestimmen (darf teure
940
+ Felder laden).
941
+ 4. **Sortieren** aller Kandidaten aller Workflows (§6.2).
942
+ 5. Pro Kandidat **sequentiell** in dieser Reihenfolge:
943
+ 1. freie Kapazität global und für den Workflow? (zuerst, damit volle Limits keine API-Aufrufe kosten)
944
+ 2. aktiver Run desselben Workflows für dieses Item (Lock) → `locked`
945
+ 3. aktiver Retry-Block → `blocked`
946
+ 4. Workflow-Conditions (einmal pro Schritt und Workflow, ggf. aus dem `cache`)
947
+ 5. teurer deklarativer Filter (nur wenn der billige Teil unknown war): `false` → `filtered`, unknown → `filter_unknown`
948
+ 6. `shell_filter`
949
+ 7. Item-Conditions
950
+ 8. synchron und atomar: Gate erneut, Workflow noch aktiv **und unverändert** (sonst `workflow_unavailable`),
951
+ Kapazität erneut, Reservierung in SQLite (scheitert bei Lock/Block)
952
+ 9. Start an den Run-Controller (§6.5)
953
+ 6. Ein nicht startfähiger Kandidat blockiert nie die folgenden.
954
+
955
+ Diagnose je Workflow (letzter Schritt, max. 500 Kandidaten) unter `GET /api/v1/workflows/:id` bzw. im Dry-Run.
956
+
957
+ ### 6.2 Priorität und Tie-Breaker
958
+
959
+ Totale Ordnung über alle Kandidaten: (1) **Workflow-Priorität** absteigend (effektiv, inkl. Override),
960
+ (2) **Item-Priorität** absteigend, (3) Erstellungszeit des Items aufsteigend (fehlend/ungültig zuletzt),
961
+ (4) Item-Schlüssel natürlich aufsteigend (`repo:9` < `repo:10`), (5) Workflow-ID aufsteigend. Ein Item mit hoher
962
+ Item-Priorität überholt also keinen Workflow mit höherer Workflow-Priorität. Kein Aging.
963
+
964
+ ### 6.3 Limits
965
+
966
+ - Global `scheduler.max_concurrent` und pro Workflow `max_concurrent` gelten nur für **automatische** Starts
967
+ (Source, Cron). Aktive Runs **aller** Trigger zählen (inkl. manueller und `recovery_pending`).
968
+ - Aktiv = `reserved`, `starting`, `running`, `cancelling`, `finalizing`, `recovery_pending` (also auch während
969
+ Abschluss-Hooks laufen).
970
+ - Overrides wirken sofort.
971
+
972
+ ### 6.4 Locks und Retry-Blocks
973
+
974
+ - **Lock:** höchstens ein aktiver Run pro `(Workflow, Item)`. Verschiedene Workflows dürfen dasselbe Item parallel
975
+ bearbeiten. Cron-Runs: Lock pro Slot; manuelle Runs ohne Item: nie Konflikt.
976
+ - **Zählung technischer Fehler** pro `(Workflow, Item)` bzw. pro Workflow bei Runs ohne Item:
977
+ `crashed`, `start_error`, `timed_out` erhöhen; `completed`+`success` setzt auf 0 (neue Zählperiode);
978
+ `completed`+`failure` und `cancelled` ändern nichts. Auch manuelle Runs zählen.
979
+ - Erreicht der Zähler `retry.max_technical_failures`, entsteht in **derselben Transaktion** wie der Übergang nach
980
+ `finalizing` ein Retry-Block – also bevor `on_crash`/`on_timeout` z. B. das Label zurücksetzen. Der Block
981
+ verhindert automatische Starts, bis er entfernt wird (UI „Remove Block“, `POST /api/v1/blocks/:id/clear`);
982
+ Entfernen startet eine neue Zählperiode. Benachrichtigung `retry_block_created`.
983
+
984
+ ### 6.5 Run-Start
985
+
986
+ 1. Source-Runs: Item frisch lesen. Nicht mehr vorhanden → `cancelled` (ohne Hooks, nicht gezählt). Bei
987
+ automatischen Runs zusätzlich billiger Filterteil auf dem frischen Item: `false` → `cancelled` ohne Hooks
988
+ (unknown startet trotzdem). Source fehlt oder Lesefehler → `start_error` **ohne** Hooks (gezählt).
989
+ 2. `starting`, `on_start`-Kette. Scheitert sie → Worker startet nicht, `start_error`, `on_start_error`.
990
+ 3. Workspace-Pfad, `args`/`env` rendern, Secrets holen, Kontextdatei schreiben; Fehler → `start_error`, `on_start_error`.
991
+ 4. Worker als eigene Prozessgruppe starten → `running`.
992
+ 5. Ergebnis klassifizieren (§6.8), Abschluss-Hook, Workspace-Cleanup, Endstatus, Lock frei.
993
+
994
+ ### 6.6 Manueller Start
995
+
996
+ UI oder `POST /api/v1/workflows/:id/start` `{"item"?: "…", "parameters"?: {…}}`:
997
+
998
+ - **Umgeht:** `enabled`/Overrides, Pause, Filter, `shell_filter`, Conditions, Retry-Blocks, Prioritäten, Limits,
999
+ den Filter-Recheck vor `on_start`.
1000
+ - **Umgeht nicht:** den Lock pro Workflow+Item (HTTP 409 `lock_conflict`), Drain/Shutdown, laufende Recovery,
1001
+ Parametervalidierung.
1002
+ - Source-Workflow: Item-Referenz Pflicht – `123`, `#123`, `owner/repo#123` oder
1003
+ `https://github.com/owner/repo/issues/123` bzw. `/pull/123` (Owner/Repo ohne Groß-/Kleinschreibung, muss zur
1004
+ Source passen); das Item muss existieren. Andere Trigger: Item verboten.
1005
+ - Manuelle Runs zählen zu den Limits automatischer Starts und zum Fehlerzähler.
1006
+
1007
+ ### 6.7 Cron
1008
+
1009
+ - Slots berechnet croner in der `timezone` des Triggers (Default `UTC`), Sekundengenauigkeit.
1010
+ - **Höchstens ein Run pro Slot**; jeder behandelte Slot bekommt einen `cron_firings`-Eintrag mit Entscheidung.
1011
+ - Pro Slot: aktiv? Gate offen? Limits frei? kein Workflow-Block? → Workflow-Conditions → nochmals prüfen →
1012
+ Eintrag + Reservierung in einer Transaktion. Sonst `skipped_disabled|paused|not_ready|draining|limit|blocked|
1013
+ condition|condition_unknown`.
1014
+ - **Verpasste Slots werden nie nachgeholt.** Toleranz 60 s: Ein Slot, der höchstens so spät behandelt wird, läuft
1015
+ normal; ältere werden `skipped_missed`. Nach Neustart: ab dem letzten persistierten Slot; nur der jüngste (falls
1016
+ in der Toleranz) kann noch laufen. Neuer Workflow/geänderter Ausdruck: ab „jetzt“.
1017
+ - **DST:** Jede Wanduhrzeit höchstens einmal (beim ersten Auftreten); tägliche Zeiten in der Frühjahrslücke laufen
1018
+ einmal, um die Lückenlänge verschoben; stündliche Slots in der Lücke entfallen. Exakte Intervalle → `UTC`.
1019
+ - Details und Tabellen: [cron-semantics.md](cron-semantics.md).
1020
+
1021
+ ### 6.8 Outcome → Status → Hook
1022
+
1023
+ | Situation | Technischer Status | Fachlich | Hook | Technischer Fehler (zählt)? |
1024
+ |---|---|---|---|---|
1025
+ | Item vor dem Start verschwunden / passt nicht mehr | `cancelled` | – | keiner | nein |
1026
+ | Cancel vor `starting` | `cancelled` | – | keiner | nein |
1027
+ | Source nicht verfügbar / Item-Re-Read scheitert | `start_error` | – | **keiner** | ja |
1028
+ | `on_start`-Kette gestoppt | `start_error` | – | `on_start_error` | ja |
1029
+ | Cancel während `on_start` oder vor Spawn | `cancelled` | – | `on_cancel` | nein |
1030
+ | Vorbereitung scheitert (Template, Workspace, Secret-Token) | `start_error` | – | `on_start_error` | ja |
1031
+ | Spawn unmöglich (ENOENT, keine Exec-Rechte) | `start_error` | – | `on_start_error` | ja |
1032
+ | kontrollierter Abbruch (UI/API/Cancel-All) | `cancelled` | – | `on_cancel` | nein |
1033
+ | `max_duration` oder `idle_timeout` überschritten | `timed_out` | – | `on_timeout` | ja |
1034
+ | durch Signal beendet (nicht Cancel/Timeout) | `crashed` | – | `on_crash` | ja |
1035
+ | Exit ≠ 0 (Ergebnisdatei wird ignoriert) | `crashed` | – | `on_crash` | ja |
1036
+ | Exit 0 + ungültige/zu große Ergebnisdatei | `crashed` | – | `on_crash` | ja |
1037
+ | Exit 0 + keine Ergebnisdatei | `completed` | `success` | `on_success` | nein (setzt Zähler zurück) |
1038
+ | Exit 0 + `"result": "success"` | `completed` | `success` | `on_success` | nein (setzt Zähler zurück) |
1039
+ | Exit 0 + `"result": "failure"` | `completed` | `failure` | `on_failure` | nein (Zähler unverändert) |
1040
+ | Controller-Neustart, Worker nachweislich weg | `crashed` (bzw. `cancelled`, wenn Cancel lief) | – | `on_crash` / `on_cancel` | `crashed`: ja |
1041
+ | Controller-Neustart, Prozess nicht sicher zuordenbar | `recovery_pending` (hält Lock) | – | – | – |
1042
+
1043
+ Eine fachliche `failure` gibt es nur über eine gültige Ergebnisdatei; ein Exit-Code allein ist nie „failure“.
1044
+ Ergebnisdatei-Format: [worker-protocol.md](worker-protocol.md).
1045
+
1046
+ ### 6.9 Action-Ketten
1047
+
1048
+ - Actions eines Hooks laufen **sequentiell** in Listenreihenfolge; jeder Zustand wird vor dem Weiterarbeiten
1049
+ persistiert (Journal `pending → running → succeeded|failed|skipped`, jeder Versuch einzeln).
1050
+ - **Retry:** bis `max_attempts` Versuche, Pause `delay` (mindestens ein `Retry-After`). Sofort endgültig (ohne
1051
+ weitere Versuche) bei nicht wiederholbaren Fehlern: Template-/Konfigurationsfehler, Authentifizierung, Item
1052
+ nicht gefunden, ungültige Anfrage. Wiederholbar: transiente API-Fehler, Rate-Limits, jeder `shell`-Fehlschlag.
1053
+ - **Stopp:** Scheitert eine Action endgültig ohne `continue_on_error`, bleibt die Kette dort stehen; folgende
1054
+ Actions bleiben `pending`, der Run zeigt „Lifecycle failed“, Benachrichtigung `lifecycle_action_failed`. Mit
1055
+ `continue_on_error: true` geht es weiter (keine Benachrichtigung).
1056
+ - Der Run-Status hängt nicht vom Hook-Ergebnis ab – Ausnahme `on_start` (→ `start_error`).
1057
+ - **Manuell:** `POST /api/v1/actions/:id/retry` (frische Versuchsrunde, danach läuft die Kette weiter; nicht für
1058
+ `on_start`, wenn der Run nicht mehr `starting` ist) und `POST /api/v1/actions/:id/skip` (failed/pending →
1059
+ `skipped`, Kette läuft weiter). Der Haupt-Worker wird nie neu gestartet; erfolgreiche Actions laufen nie erneut.
1060
+ - **Idempotenz:** `source.update` liest vor dem Schreiben neu und ändert nur, was fehlt. `source.comment` hängt als
1061
+ letzte Zeile `<!-- adispatchd:<run-id>:<journal-action-id> -->` an; vor jeder Wiederholung (und nach Neustart) wird ein
1062
+ Kommentar mit diesem Marker gesucht, der von der eigenen App stammt → kein Duplikat. `<!--` im gerenderten Text
1063
+ wird zu `&lt;!--`.
1064
+ - **Neustart:** unterbrochene `source.*`-Actions werden fortgesetzt; eine unterbrochene `shell`-Action wird
1065
+ `failed` und nie automatisch wiederholt (manueller Retry). Eine unterbrochene `on_start`-Kette wird verworfen.
1066
+
1067
+ ### 6.10 Identitäten je Zugriff
1068
+
1069
+ | Zugriff | Identity |
1070
+ |---|---|
1071
+ | Pollen, Item lesen, Dependencies auflösen, Item-Re-Read vor `on_start` | `sources.<id>.identity` |
1072
+ | `source.update` (inkl. Re-Read vor dem Schreiben) | Workflow-`identity` → Source-`identity` |
1073
+ | `source.comment` (inkl. Marker-Suche) | Action-`identity` → Workflow-`identity` → Source-`identity` |
1074
+ | `execute.secrets.<NAME>` | die angegebene Identity (Installation-Token, ca. 1 h gültig, beim Start geholt) |
1075
+ | `shell`-Actions, Conditions, `shell_filter` | keine (keine Secrets) |
1076
+
1077
+ ### 6.11 Umgebung von Worker, Actions und Prüfskripten
1078
+
1079
+ | | Haupt-Worker | `shell`-Action | Conditions / `shell_filter` |
1080
+ |---|---|---|---|
1081
+ | Basis | `PATH`, `HOME`, `LANG`, `TZ`, `USER`, `LOGNAME`, `TMPDIR` aus der Controller-Umgebung (PATH-Fallback `/usr/local/bin:/usr/bin:/bin`) | wie Worker | dieselben Variablen (ohne PATH-Fallback) |
1082
+ | Konfiguriert | `execute.env`, dann `execute.secrets` | Action-`env` | – |
1083
+ | Orchestrator | `ADISPATCH_RUN_ID`, `ADISPATCH_WORKFLOW_ID`, `ADISPATCH_CONTEXT_FILE`, `ADISPATCH_RESULT_FILE`, `ADISPATCH_WORKSPACE`; bei Item zusätzlich `ADISPATCH_SOURCE_TYPE`, `ADISPATCH_SOURCE_ID`, `ADISPATCH_SOURCE_ITEM_ID`, `ADISPATCH_SOURCE_URL` | dieselben (`ADISPATCH_WORKSPACE` = Arbeitsverzeichnis der Action; Kontext `kind: lifecycle_action` mit Hook, Action, `result`) | `ADISPATCH_CHECK_CONTEXT_FILE` (JSON: `kind`, `workflow.id`, bei Item `source{id,type,item_id,item_key,url,item_type,created_at,attributes}`) |
1084
+ | Arbeitsverzeichnis | Workspace | Workspace, falls vorhanden, sonst Versuchsverzeichnis | temporäres Verzeichnis |
1085
+
1086
+ Vorrang (spätere gewinnen): Basis < `env` < `secrets` < `ADISPATCH_*`. Die übrige Controller-Umgebung wird **nie**
1087
+ vererbt. Details zu Kontextdatei, Ergebnisdatei, Events und Cancel: [worker-protocol.md](worker-protocol.md).
1088
+
1089
+ ### 6.12 Workspace-Cleanup
1090
+
1091
+ Nach dem Abschluss-Hook: `always` = immer, `on_success` = nur `completed`+`success`, `never` = nie. Gelöscht wird nur
1092
+ ein Workspace strikt unter `workspace.base_dir` (auch nach Auflösen von Symlinks; ein Workspace, der selbst ein
1093
+ Symlink ist, wird nie gelöscht). Später entfernt die Retention auch verbliebene Workspaces gelöschter Runs.
1094
+
1095
+ ### 6.13 Dry-Run
1096
+
1097
+ `adispatch workflow dry-run <id>` (gegen die laufende Instanz, sonst mit `--config-dir` lokal) bzw.
1098
+ `POST /api/v1/workflows/:id/dry-run`: Source-Abfrage, Filter-Trace pro Regel (inkl. Dependencies), Prioritäten,
1099
+ Locks/Blocks, Kapazität (in Prioritätsreihenfolge simuliert). Reserviert nichts, startet keine Worker oder Actions
1100
+ und führt **nie** `shell_filter`/Conditions aus (`not_evaluated`, Entscheidung `pending_checks`). Cron-Workflows:
1101
+ nächste 5 Slots und Prognose; Manual-Workflows: nur Hinweise.
1102
+
1103
+ ---
1104
+
1105
+ ## 7. Vollständige Beispiele
1106
+
1107
+ ### 7.1 Minimal-Setup (ohne GitHub)
1108
+
1109
+ Ein manueller Workflow, keine Sources/Identities nötig.
1110
+
1111
+ ```yaml
1112
+ # example: minimal
1113
+ # file: config.yaml
1114
+ # Alles optional – nur zur Verdeutlichung zwei Werte.
1115
+ server:
1116
+ port: 8787
1117
+ scheduler:
1118
+ max_concurrent: 2
1119
+ ```
1120
+
1121
+ ```yaml
1122
+ # example: minimal
1123
+ # file: workflows/hello.yaml
1124
+ id: hello
1125
+ name: Hello World
1126
+ trigger: { type: manual }
1127
+ parameters:
1128
+ name: { type: string, default: world, description: Wen grüßen? }
1129
+ execute:
1130
+ type: shell
1131
+ command: scripts/hello.sh # relativ zum config-dir
1132
+ args: ['{parameters.name}']
1133
+ timeout: { max_duration: 10m }
1134
+ ```
1135
+
1136
+ ### 7.2 Coder-/Reviewer-Paar über Labels
1137
+
1138
+ Label-Fluss auf dem Issue: `ready-for-coding` → `coding` → `ready-for-review` → `reviewing` → `ready-to-merge`
1139
+ (bzw. zurück zu `ready-for-coding` oder `needs-maintainer`). Der Reviewer hat die höhere Workflow-Priorität, damit
1140
+ fertige Arbeit zuerst geprüft wird.
1141
+
1142
+ ```yaml
1143
+ # example: github
1144
+ # file: config.yaml
1145
+ scheduler:
1146
+ polling_interval: 60s
1147
+ max_concurrent: 4
1148
+ workspace:
1149
+ cleanup: on_success
1150
+ ```
1151
+
1152
+ ```yaml
1153
+ # example: github
1154
+ # file: identities.yaml
1155
+ identities:
1156
+ maintainer: { type: github_app, app_id: 100001, installation_id: 200001, private_key_env: GITHUB_MAINTAINER_PRIVATE_KEY }
1157
+ coder: { type: github_app, app_id: 100002, installation_id: 200002, private_key_env: GITHUB_CODER_PRIVATE_KEY }
1158
+ reviewer: { type: github_app, app_id: 100003, installation_id: 200003, private_key_env: GITHUB_REVIEWER_PRIVATE_KEY }
1159
+ ```
1160
+
1161
+ ```yaml
1162
+ # example: github
1163
+ # file: sources.yaml
1164
+ sources:
1165
+ project_a: { type: github, owner: example, repository: project-a, identity: maintainer }
1166
+ project_b: { type: github, owner: example, repository: project-b, identity: maintainer }
1167
+ ```
1168
+
1169
+ ```yaml
1170
+ # example: github
1171
+ # file: workflows/coder.yaml
1172
+ id: coder
1173
+ name: Coder – Project A
1174
+ priority: 50
1175
+ max_concurrent: 2
1176
+ identity: coder
1177
+ trigger: { type: source, source: project_a }
1178
+ filter:
1179
+ all:
1180
+ - { field: labels, operator: contains, value: ready-for-coding } # Pushdown: labels=ready-for-coding
1181
+ - { field: is_pull_request, operator: equals, value: false }
1182
+ - { field: dependencies.blocked_by_open, operator: equals, value: 0 } # teuer, lazy
1183
+ item_priority:
1184
+ rules:
1185
+ - when: { field: labels, operator: contains, value: 'priority:high' }
1186
+ value: 50
1187
+ conditions:
1188
+ workflow:
1189
+ - { type: shell, command: scripts/check-credits.sh, timeout: 15s, cache: 60s }
1190
+ execute:
1191
+ type: shell
1192
+ command: scripts/coder.sh
1193
+ args: ['{item.url}']
1194
+ secrets:
1195
+ GH_TOKEN: { identity: coder }
1196
+ timeout: { max_duration: 6h, idle_timeout: 45m }
1197
+ retry: { max_technical_failures: 3 }
1198
+ on_start:
1199
+ - type: source.update
1200
+ labels: { add: [coding], remove: [ready-for-coding] }
1201
+ on_success:
1202
+ - type: source.update
1203
+ labels: { add: [ready-for-review], remove: [coding] }
1204
+ - type: source.comment
1205
+ message: 'Coder fertig (Run {run.id}): {result.message}'
1206
+ continue_on_error: true
1207
+ on_failure:
1208
+ - type: source.update
1209
+ labels: { add: [needs-maintainer], remove: [coding] }
1210
+ - type: source.comment
1211
+ message: 'Needs maintainer: {result.message}'
1212
+ on_crash: [ { type: source.update, labels: { add: [ready-for-coding], remove: [coding] } } ]
1213
+ on_start_error: [ { type: source.update, labels: { add: [ready-for-coding], remove: [coding] } } ]
1214
+ on_timeout: [ { type: source.update, labels: { add: [ready-for-coding], remove: [coding] } } ]
1215
+ on_cancel: [ { type: source.update, labels: { add: [ready-for-coding], remove: [coding] } } ]
1216
+ ```
1217
+
1218
+ ```yaml
1219
+ # example: github
1220
+ # file: workflows/reviewer.yaml
1221
+ id: reviewer
1222
+ name: Reviewer – Project A
1223
+ priority: 60
1224
+ identity: reviewer
1225
+ trigger: { type: source, source: project_a }
1226
+ filter:
1227
+ all:
1228
+ - { field: labels, operator: contains, value: ready-for-review }
1229
+ - { field: is_pull_request, operator: equals, value: false }
1230
+ execute:
1231
+ type: shell
1232
+ command: scripts/reviewer.sh
1233
+ args: ['{item.url}']
1234
+ secrets:
1235
+ GH_TOKEN: { identity: reviewer }
1236
+ timeout: { max_duration: 2h }
1237
+ on_start:
1238
+ - type: source.update
1239
+ labels: { add: [reviewing], remove: [ready-for-review] }
1240
+ on_success: # Review bestanden
1241
+ - type: source.update
1242
+ labels: { add: [ready-to-merge], remove: [reviewing] }
1243
+ on_failure: # Änderungen nötig → zurück an den Coder
1244
+ - type: source.update
1245
+ labels: { add: [ready-for-coding], remove: [reviewing] }
1246
+ - type: source.comment
1247
+ message: 'Review: {result.message}'
1248
+ on_crash: [ { type: source.update, labels: { add: [ready-for-review], remove: [reviewing] } } ]
1249
+ on_start_error: [ { type: source.update, labels: { add: [ready-for-review], remove: [reviewing] } } ]
1250
+ on_timeout: [ { type: source.update, labels: { add: [ready-for-review], remove: [reviewing] } } ]
1251
+ on_cancel: [ { type: source.update, labels: { add: [ready-for-review], remove: [reviewing] } } ]
1252
+ ```
1253
+
1254
+ ### 7.3 View über zwei Repositories
1255
+
1256
+ ```yaml
1257
+ # example: github
1258
+ # file: views/needs-maintainer.yaml
1259
+ id: needs-maintainer
1260
+ title: Needs Maintainer
1261
+ sources: [project_a, project_b]
1262
+ filter:
1263
+ all:
1264
+ - { field: labels, operator: contains, value: needs-maintainer }
1265
+ fields:
1266
+ - { field: repository, label: Repository }
1267
+ - { field: number, label: Issue }
1268
+ - { field: title, label: Titel, link: url }
1269
+ - { field: run.last_workflow, label: Letzter Agent }
1270
+ - { field: run.last_result.message, label: Ergebnis }
1271
+ - { field: updated_at, label: Aktualisiert }
1272
+ sort: { field: updated_at, direction: desc }
1273
+ ```
1274
+
1275
+ ### 7.4 Cron-Workflow
1276
+
1277
+ ```yaml
1278
+ # example: cron
1279
+ # extends: minimal
1280
+ # file: workflows/nightly-maintenance.yaml
1281
+ id: nightly-maintenance
1282
+ name: Nightly Maintenance
1283
+ priority: 20
1284
+ trigger:
1285
+ type: schedule
1286
+ cron: '0 3 * * *' # täglich 03:00 Ortszeit
1287
+ timezone: Europe/Berlin
1288
+ conditions:
1289
+ workflow:
1290
+ - type: shell
1291
+ command: scripts/maintenance-window.sh
1292
+ args: ['{timestamp}']
1293
+ execute:
1294
+ type: shell
1295
+ command: scripts/maintenance.sh
1296
+ args: ['--run', '{run.id}', '--trigger', '{run.trigger}']
1297
+ timeout: { max_duration: 1h }
1298
+ retry: { max_technical_failures: 2 } # Block im Workflow-Scope (kein Item)
1299
+ on_failure:
1300
+ - type: shell
1301
+ command: scripts/notify.sh
1302
+ args: ['{workflow.name}: {result.message}']
1303
+ on_crash:
1304
+ - type: shell
1305
+ command: scripts/notify.sh
1306
+ args: ['{workflow.name} abgestürzt ({result.status}), Run {run.id}']
1307
+ ```
1308
+
1309
+ ### 7.5 Manual-Workflow mit Parametern
1310
+
1311
+ ```yaml
1312
+ # example: manual-params
1313
+ # extends: minimal
1314
+ # file: workflows/deploy.yaml
1315
+ id: deploy
1316
+ name: Deploy
1317
+ trigger: { type: manual }
1318
+ parameters:
1319
+ environment:
1320
+ type: enum
1321
+ values: [staging, production]
1322
+ required: true
1323
+ description: Zielumgebung
1324
+ version:
1325
+ type: string
1326
+ required: true
1327
+ dry_run:
1328
+ type: boolean
1329
+ default: false
1330
+ replicas:
1331
+ type: number
1332
+ default: 2
1333
+ execute:
1334
+ type: shell
1335
+ command: scripts/deploy.sh
1336
+ # environment ist Pflicht → im Pfad erlaubt (wird zu einem sicheren Segment)
1337
+ working_directory: '{workspace.base_dir}/deploy/{parameters.environment}/{run.id}'
1338
+ args: ['--version={parameters.version}', '--dry-run={parameters.dry_run}', '--replicas={parameters.replicas}']
1339
+ env:
1340
+ DEPLOY_ENV: '{parameters.environment}'
1341
+ timeout: { max_duration: 30m }
1342
+ on_success:
1343
+ - type: shell
1344
+ command: scripts/notify.sh
1345
+ args: ['Deploy {parameters.version} nach {parameters.environment}: {result.message}']
1346
+ ```
1347
+
1348
+ Start: `curl -X POST -H 'X-Adispatch-Request: 1' -H 'Content-Type: application/json'
1349
+ -d '{"parameters":{"environment":"staging","version":"1.4.2"}}' http://127.0.0.1:8787/api/v1/workflows/deploy/start`.
1350
+
1351
+ ### 7.6 Notifications per Webhook
1352
+
1353
+ ```yaml
1354
+ # example: notifications
1355
+ # extends: minimal
1356
+ # file: config.yaml
1357
+ notifications:
1358
+ on: [retry_block_created, lifecycle_action_failed, recovery_failed]
1359
+ channels:
1360
+ - id: slack # Slack-Incoming-Webhook; URL enthält ein Token → aus der Umgebung
1361
+ type: webhook
1362
+ url_env: SLACK_WEBHOOK_URL
1363
+ - id: ops
1364
+ type: webhook
1365
+ url: https://ops.example.com/hooks/orchestrator
1366
+ headers_env:
1367
+ Authorization: OPS_WEBHOOK_AUTH # z. B. "Bearer …" in der Env-Datei
1368
+ on: [recovery_failed] # nur dieses Ereignis
1369
+ ```
1370
+
1371
+ Umgebung (z. B. `/etc/default/adispatchd`): `SLACK_WEBHOOK_URL=https://hooks.slack.com/services/…`,
1372
+ `OPS_WEBHOOK_AUTH=Bearer …`. Danach **Neustart** (Env-Änderung) bzw. `config reload` (nur YAML-Änderung).
1373
+
1374
+ ---
1375
+
1376
+ ## 8. Häufige Validierungsfehler
1377
+
1378
+ Format der Ausgabe von `config validate`: `datei:zeile: pfad: meldung`. Alle folgenden Blöcke sind bewusst
1379
+ ungültig und werden vom Test gegen die angegebene Meldung geprüft.
1380
+
1381
+ **Tippfehler im Schlüssel** – alle Objekte sind strikt.
1382
+
1383
+ ```yaml
1384
+ # example: err-unknown-key
1385
+ # extends: minimal
1386
+ # invalid
1387
+ # expect: config.yaml: scheduler: Unknown key(s): "max_concurent"
1388
+ # file: config.yaml
1389
+ scheduler:
1390
+ max_concurent: 2
1391
+ ```
1392
+
1393
+ Lösung: Schreibweise laut §3 korrigieren.
1394
+
1395
+ **Falsches Dauerformat** – keine zusammengesetzten Dauern, keine Leerzeichen.
1396
+
1397
+ ```yaml
1398
+ # example: err-duration
1399
+ # extends: minimal
1400
+ # invalid
1401
+ # expect: workflows/hello.yaml: execute.timeout.max_duration: Invalid duration "1h30m" (expected e.g. 100ms, 30s, 45m, 6h, 30d)
1402
+ # file: workflows/hello.yaml
1403
+ id: hello
1404
+ trigger: { type: manual }
1405
+ execute:
1406
+ type: shell
1407
+ command: scripts/hello.sh
1408
+ timeout: { max_duration: 1h30m }
1409
+ ```
1410
+
1411
+ Lösung: `90m`. Eine unquotierte Zahl (`max_duration: 30`) meldet `Expected a duration such as 30s, 45m, 6h`.
1412
+
1413
+ **Unbekannte Referenz** (Source, Identity) – auch Folgefehler einer kaputten `identities.yaml`.
1414
+
1415
+ ```yaml
1416
+ # example: err-unknown-source
1417
+ # extends: github
1418
+ # invalid
1419
+ # expect: workflows/coder.yaml: trigger.source: Unknown source "project_x"
1420
+ # expect: workflows/coder.yaml: identity: Unknown identity "codr"
1421
+ # file: workflows/coder.yaml
1422
+ id: coder
1423
+ identity: codr
1424
+ trigger: { type: source, source: project_x }
1425
+ execute: { type: shell, command: scripts/coder.sh }
1426
+ ```
1427
+
1428
+ **Unbekannter oder unpassender Platzhalter** – `{result.*}` gibt es in `on_start` noch nicht.
1429
+
1430
+ ```yaml
1431
+ # example: err-placeholder
1432
+ # extends: github
1433
+ # invalid
1434
+ # expect: workflows/coder.yaml: on_start[0].message: Placeholder "{result.message}" is not available in this context
1435
+ # expect: workflows/coder.yaml: on_start[0].message: Unknown placeholder "{item.body}"
1436
+ # file: workflows/coder.yaml
1437
+ id: coder
1438
+ trigger: { type: source, source: project_a }
1439
+ execute: { type: shell, command: scripts/coder.sh }
1440
+ on_start:
1441
+ - type: source.comment
1442
+ message: 'Start: {result.message} – {item.body}'
1443
+ ```
1444
+
1445
+ Lösung: nur Platzhalter aus §5 im erlaubten Scope; literale Klammern als `{{`/`}}`.
1446
+
1447
+ **Source-Elemente in Workflows ohne Source-Trigger**
1448
+
1449
+ ```yaml
1450
+ # example: err-source-only
1451
+ # extends: minimal
1452
+ # invalid
1453
+ # expect: workflows/nightly.yaml: filter: "filter" is only allowed for workflows with a source trigger (trigger is "schedule")
1454
+ # expect: workflows/nightly.yaml: on_success[0].type: "source.comment" needs a source item; the workflow has no source trigger
1455
+ # file: workflows/nightly.yaml
1456
+ id: nightly
1457
+ trigger: { type: schedule, cron: '0 3 * * *' }
1458
+ filter: { field: state, operator: equals, value: open }
1459
+ execute: { type: shell, command: scripts/nightly.sh }
1460
+ on_success:
1461
+ - type: source.comment
1462
+ message: done
1463
+ ```
1464
+
1465
+ Lösung: `filter`, `shell_filter`, `item_priority`, `conditions.item`, `source.update`, `source.comment` nur mit
1466
+ `trigger.type: source`; für Cron/Manual `shell`-Actions verwenden.
1467
+
1468
+ **Filter-Struktur** – `in` braucht eine Liste, `value` ist Pflicht.
1469
+
1470
+ ```yaml
1471
+ # example: err-filter
1472
+ # extends: github
1473
+ # invalid
1474
+ # expect: workflows/coder.yaml: filter.all[0].value: Operator "in" requires a non-empty list of strings, numbers or booleans
1475
+ # expect: workflows/coder.yaml: filter.all[1].operator: Predicate requires "operator", one of: equals, not_equals, contains, in, greater_than, less_than
1476
+ # expect: workflows/coder.yaml: filter.all[2].value: Predicate requires a "value" (use null explicitly if intended)
1477
+ # expect: workflows/coder.yaml: filter.all[3].value: Operator "contains" requires a scalar value (string, number, boolean or null)
1478
+ # expect: workflows/coder.yaml: filter.all[4].value: Operator "equals" requires a scalar value (string, number, boolean or null)
1479
+ # file: workflows/coder.yaml
1480
+ id: coder
1481
+ trigger: { type: source, source: project_a }
1482
+ filter:
1483
+ all:
1484
+ - { field: state, operator: in, value: open }
1485
+ - { field: labels, operator: has, value: ready }
1486
+ - { field: author, operator: equals }
1487
+ - { field: labels, operator: contains, value: [a, b] } # Liste nur mit "in"
1488
+ - { field: author, operator: equals, value: [alice, bob] }
1489
+ execute: { type: shell, command: scripts/coder.sh }
1490
+ ```
1491
+
1492
+ **Relatives `working_directory` / Wurzel nicht am Anfang**
1493
+
1494
+ ```yaml
1495
+ # example: err-working-directory
1496
+ # extends: minimal
1497
+ # invalid
1498
+ # expect: workflows/hello.yaml: execute.working_directory: Must be absolute: start with "/" or "{workspace.base_dir}"
1499
+ # file: workflows/hello.yaml
1500
+ id: hello
1501
+ trigger: { type: manual }
1502
+ execute:
1503
+ type: shell
1504
+ command: scripts/hello.sh
1505
+ working_directory: 'work/{run.id}'
1506
+ ```
1507
+
1508
+ Weitere Meldungen: `Placeholder "{workspace.base_dir}" must be followed by "/" or the end of the path template`
1509
+ bzw. `… is a directory root and must start the path template`.
1510
+
1511
+ **Parameter im Pfad bzw. in automatischen Workflows**
1512
+
1513
+ ```yaml
1514
+ # example: err-parameters
1515
+ # extends: minimal
1516
+ # invalid
1517
+ # expect: workflows/hello.yaml: execute.working_directory: Parameter "name" is optional without default and cannot be used in a path
1518
+ # expect: workflows/hello.yaml: execute.args[0]: Parameter "nme" is not declared
1519
+ # file: workflows/hello.yaml
1520
+ id: hello
1521
+ trigger: { type: manual }
1522
+ parameters:
1523
+ name: { type: string }
1524
+ execute:
1525
+ type: shell
1526
+ command: scripts/hello.sh
1527
+ working_directory: '{workspace.base_dir}/{parameters.name}'
1528
+ args: ['{parameters.nme}']
1529
+ ```
1530
+
1531
+ ```yaml
1532
+ # example: err-required-parameter
1533
+ # extends: minimal
1534
+ # invalid
1535
+ # expect: workflows/nightly.yaml: parameters.target.required: Required parameter without default in an automatically triggered workflow: automatic runs could never supply it
1536
+ # file: workflows/nightly.yaml
1537
+ id: nightly
1538
+ trigger: { type: schedule, cron: '0 3 * * *' }
1539
+ parameters:
1540
+ target: { type: string, required: true }
1541
+ execute: { type: shell, command: scripts/nightly.sh }
1542
+ ```
1543
+
1544
+ **Reservierte oder doppelte Env-Namen**
1545
+
1546
+ ```yaml
1547
+ # example: err-env
1548
+ # extends: github
1549
+ # invalid
1550
+ # expect: workflows/coder.yaml: execute.env.ADISPATCH_MODE: Environment variables with prefix "ADISPATCH_" are reserved
1551
+ # expect: workflows/coder.yaml: execute.secrets.GH_TOKEN: "GH_TOKEN" is defined both in "env" and "secrets"
1552
+ # file: workflows/coder.yaml
1553
+ id: coder
1554
+ trigger: { type: source, source: project_a }
1555
+ execute:
1556
+ type: shell
1557
+ command: scripts/coder.sh
1558
+ env: { ADISPATCH_MODE: fast, GH_TOKEN: '' }
1559
+ secrets: { GH_TOKEN: { identity: coder } }
1560
+ ```
1561
+
1562
+ **Cron und Zeitzone**
1563
+
1564
+ ```yaml
1565
+ # example: err-cron
1566
+ # extends: minimal
1567
+ # invalid
1568
+ # expect: workflows/nightly.yaml: trigger.cron: Invalid cron expression "0 25 * * *"
1569
+ # expect: workflows/nightly.yaml: trigger.timezone: Invalid time zone "Europe/Berln"
1570
+ # file: workflows/nightly.yaml
1571
+ id: nightly
1572
+ trigger: { type: schedule, cron: '0 25 * * *', timezone: Europe/Berln }
1573
+ execute: { type: shell, command: scripts/nightly.sh }
1574
+ ```
1575
+
1576
+ Ein Ausdruck, der nie feuert, meldet `Cron expression "…" never fires`.
1577
+
1578
+ **Webhook-Kanal**
1579
+
1580
+ ```yaml
1581
+ # example: err-webhook
1582
+ # extends: minimal
1583
+ # invalid
1584
+ # expect: config.yaml: notifications.channels[0]: Webhook channel needs exactly one of "url" or "url_env"
1585
+ # expect: config.yaml: notifications.channels[1].url: Webhook "url" must be an absolute http(s) URL
1586
+ # file: config.yaml
1587
+ notifications:
1588
+ channels:
1589
+ - { type: webhook, url: 'https://hooks.example.com/x', url_env: HOOK_URL }
1590
+ - { type: webhook, url: 'hooks.example.com/x' }
1591
+ ```
1592
+
1593
+ **Labels und Action-IDs**
1594
+
1595
+ ```yaml
1596
+ # example: err-actions
1597
+ # extends: github
1598
+ # invalid
1599
+ # expect: workflows/coder.yaml: on_start[0].labels: Label "coding" is both added and removed
1600
+ # expect: workflows/coder.yaml: on_success[0].labels: "source.update" must add or remove at least one label
1601
+ # expect: workflows/coder.yaml: on_success[1].id: Duplicate action ID "notify" (also used at on_start[1])
1602
+ # file: workflows/coder.yaml
1603
+ id: coder
1604
+ trigger: { type: source, source: project_a }
1605
+ execute: { type: shell, command: scripts/coder.sh }
1606
+ on_start:
1607
+ - type: source.update
1608
+ labels: { add: [coding], remove: [coding] }
1609
+ - { id: notify, type: shell, command: scripts/notify.sh }
1610
+ on_success:
1611
+ - type: source.update
1612
+ labels: { add: [] }
1613
+ - { id: notify, type: shell, command: scripts/notify.sh }
1614
+ ```
1615
+
1616
+ **Views ohne Shell, gleiche Quellen eindeutig**
1617
+
1618
+ ```yaml
1619
+ # example: err-view
1620
+ # extends: github
1621
+ # invalid
1622
+ # expect: views/needs-maintainer.yaml: shell_filter: Views do not support "shell_filter"; views never run shell scripts (Spec §17)
1623
+ # expect: views/needs-maintainer.yaml: sources: Sources of a view must be unique
1624
+ # file: views/needs-maintainer.yaml
1625
+ id: needs-maintainer
1626
+ sources: [project_a, project_a]
1627
+ shell_filter: { command: scripts/filter.sh }
1628
+ fields: [ { field: title } ]
1629
+ ```
1630
+
1631
+ **Ungültige ID und YAML-Typen**
1632
+
1633
+ ```yaml
1634
+ # example: err-types
1635
+ # extends: minimal
1636
+ # invalid
1637
+ # expect: workflows/hello.yaml: id: Invalid ID (allowed: letters, digits, "_" and "-", must start with a letter or digit, max. 64)
1638
+ # expect: workflows/hello.yaml: enabled: Invalid input: expected boolean, received string
1639
+ # expect: workflows/hello.yaml: trigger.type: Invalid discriminator value. Expected 'source' | 'schedule' | 'manual'
1640
+ # file: workflows/hello.yaml
1641
+ id: hello world
1642
+ enabled: yes # YAML 1.2: String, kein Boolean
1643
+ trigger: { type: cron }
1644
+ execute: { type: shell, command: scripts/hello.sh }
1645
+ ```
1646
+
1647
+ **Unbekannte Felder, unpassende Operatoren und Werttypen** – Prüfung gegen den Feldkatalog (§4.7, §4.8).
1648
+
1649
+ ```yaml
1650
+ # example: err-fields
1651
+ # extends: github
1652
+ # invalid
1653
+ # expect: workflows/coder.yaml: filter.all[0].field: Unknown field "labls" for source type github (did you mean "labels"?)
1654
+ # expect: workflows/coder.yaml: filter.all[1].operator: Operator "greater_than" is not supported for field "labels" (type string[]); allowed: contains, in
1655
+ # expect: workflows/coder.yaml: filter.all[2].value: Value for field "is_pull_request" (type boolean) must be a boolean, got string "false"
1656
+ # expect: workflows/coder.yaml: filter.all[3].value: Value "last week" for date field "updated_at" is not an ISO-8601 date
1657
+ # expect: workflows/coder.yaml: filter.all[4].field: Field "run.last_status" is only available in views, not in workflows
1658
+ # file: workflows/coder.yaml
1659
+ id: coder
1660
+ trigger: { type: source, source: project_a }
1661
+ filter:
1662
+ all:
1663
+ - { field: labls, operator: contains, value: ready-for-coding }
1664
+ - { field: labels, operator: greater_than, value: 2 }
1665
+ - { field: is_pull_request, operator: equals, value: 'false' }
1666
+ - { field: updated_at, operator: greater_than, value: last week }
1667
+ - { field: run.last_status, operator: equals, value: completed }
1668
+ execute: { type: shell, command: scripts/coder.sh }
1669
+ ```
1670
+
1671
+ Lösung: Feldnamen aus §4.7 (Views zusätzlich §4.8), Operatoren laut §4.2, Booleans unquotiert, Datumswerte ISO.
1672
+ In Views werden auch `fields[].field`, `fields[].link` und `sort.field` so geprüft.
1673
+
1674
+ **`state` an wirkungsloser Stelle** – die Source liest ohne `state`-Selektor auf oberster Ebene nur offene Items.
1675
+
1676
+ ```yaml
1677
+ # example: err-state
1678
+ # extends: github
1679
+ # invalid
1680
+ # expect: workflows/coder.yaml: filter.any[0].field: "state" only works at the top level of the filter (directly or inside top-level "all"): the source lists only open issues/PRs unless the top-level filter selects others (state equals/in open|closed), so under "any"/"not" it would silently never see them
1681
+ # expect: views/needs-maintainer.yaml: filter.all[0].operator: "state" must use "equals" or "in": only these select which items are listed (otherwise only open issues/PRs are seen)
1682
+ # file: workflows/coder.yaml
1683
+ id: coder
1684
+ trigger: { type: source, source: project_a }
1685
+ filter:
1686
+ any:
1687
+ - { field: state, operator: equals, value: closed }
1688
+ - { field: labels, operator: contains, value: reopen }
1689
+ execute: { type: shell, command: scripts/coder.sh }
1690
+ ```
1691
+
1692
+ ```yaml
1693
+ # example: err-state
1694
+ # file: views/needs-maintainer.yaml
1695
+ id: needs-maintainer
1696
+ sources: [project_a, project_b]
1697
+ filter:
1698
+ all:
1699
+ - { field: state, operator: not_equals, value: open }
1700
+ fields: [ { field: title } ]
1701
+ ```
1702
+
1703
+ Lösung: `state` als oberstes Prädikat, z. B. `all: [{ field: state, operator: in, value: [open, closed] }, { any: [...] }]`.
1704
+
1705
+ **Null-Intervalle und -Timeouts**
1706
+
1707
+ ```yaml
1708
+ # example: err-zero-durations
1709
+ # extends: minimal
1710
+ # invalid
1711
+ # expect: config.yaml: scheduler.polling_interval: Duration must be at least 1s
1712
+ # expect: config.yaml: retention.cleanup_interval: Duration must be at least 1s
1713
+ # file: config.yaml
1714
+ scheduler:
1715
+ polling_interval: 0s
1716
+ retention:
1717
+ cleanup_interval: 500ms
1718
+ lifecycle:
1719
+ action_retry: { delay: 0s } # erlaubt: sofort wiederholen
1720
+ ```
1721
+
1722
+ `timeout: 0s` bei Conditions, `shell_filter` oder `shell`-Actions meldet `Duration must be greater than 0`.
1723
+
1724
+ **Warnungen** (blockieren nicht):
1725
+
1726
+ ```yaml
1727
+ # example: warn-common
1728
+ # extends: minimal
1729
+ # expect-warning: workflows/hello.yaml: id: Workflow ID "greeter" differs from file name "hello.yaml"
1730
+ # expect-warning: workflows/hello.yaml: execute.env.API_TOKEN: Looks like a secret stored literally in YAML
1731
+ # expect-warning: workflows/hello.yaml: execute.timeout.idle_timeout: "idle_timeout" is longer than "max_duration"
1732
+ # file: workflows/hello.yaml
1733
+ id: greeter
1734
+ trigger: { type: manual }
1735
+ execute:
1736
+ type: shell
1737
+ command: scripts/hello.sh
1738
+ env: { API_TOKEN: abc123 }
1739
+ timeout: { max_duration: 1m, idle_timeout: 5m }
1740
+ ```
1741
+
1742
+ Weitere häufige Meldungen (ohne eigenes Beispiel):
1743
+
1744
+ | Meldung | Ursache / Lösung |
1745
+ |---|---|
1746
+ | `YAML syntax error: Map keys must be unique` | Schlüssel doppelt im selben Mapping |
1747
+ | `YAML syntax error: …` | Einrückung, nicht geschlossene `{`/`[`, Tabs |
1748
+ | `Top level must be a mapping` | Datei beginnt mit einer Liste (`- …`) |
1749
+ | `Invalid input: expected object, received undefined` (Pfad `execute`/`trigger`) | Pflichtblock fehlt |
1750
+ | `Invalid discriminator value. Expected 'github'` | falscher `type` in Source/Identity/Kanal |
1751
+ | `Duplicate workflow ID "x" (already defined in workflows/a.yaml)` | zwei Dateien mit gleicher `id` |
1752
+ | `Default does not match parameter type: expected …` | `default` passt nicht zu `type`/`values` |
1753
+ | `"cache" is only supported for workflow conditions` | `cache` unter `conditions.item` |
1754
+ | `Duration must be greater than 0` | `max_duration`/`idle_timeout: 0s` |
1755
+ | `Invalid host name (expected "name" or "name:port")` | Schema/Pfad in `allowed_hosts` (`https://…`) |
1756
+ | `Invalid environment variable name` | z. B. `private_key_env: my-key` |
1757
+ | `Expected a size such as 100MB` | Größe als Zahl statt String (`max_log_size_per_run: 100`) |
1758
+ | `Invalid size "100" (expected e.g. 512KB, 100MB, 1GB)` | String ohne oder mit unbekannter Einheit (`'100'`, `'100MiBs'`) |
1759
+ | `config.yaml not found, using defaults` (Warnung) | `config.yaml` fehlt |
1760
+
1761
+ Laufzeitfehler, die `config validate` **nicht** findet: fehlende Umgebungsvariablen (Keys, Webhooks),
1762
+ nicht existierende/nicht ausführbare Skripte, fehlende App-Rechte (→ Felder unknown bzw. Actions scheitern). Dafür: `workflow dry-run` und die Run-Details.
1763
+
1764
+ ---
1765
+
1766
+ ## 9. Checkliste „neuen Workflow anlegen“
1767
+
1768
+ 1. **Trigger wählen:** `source` (Items), `schedule` (Cron) oder `manual`.
1769
+ 2. **Voraussetzungen:** Source in `sources.yaml`, Identities in `identities.yaml`, Private Keys als
1770
+ Umgebungsvariablen des Dienstes (Env-Datei, danach Neustart), App-Rechte prüfen.
1771
+ 3. **Datei** `workflows/<id>.yaml` anlegen (Dateiname = `id`), am besten mit `enabled: false` beginnen.
1772
+ 4. **Filter** nur mit Feldern aus §4.7; Labels exakt schreiben; für geschlossene Items ein `state`-Prädikat auf
1773
+ oberster Ebene; teure `dependencies.*` nur wenn nötig.
1774
+ 5. **execute:** Skript ausführbar machen (`chmod +x`), Pfad relativ zum config-dir oder absolut; Argumente als
1775
+ Liste; Timeouts setzen; Secrets nur über `secrets`.
1776
+ 6. **Hooks:** Label-Übergänge für jeden Ausgang (`on_start`, `on_success`, `on_failure`, `on_crash`,
1777
+ `on_start_error`, `on_timeout`, `on_cancel`) so wählen, dass das Item nicht hängen bleibt und nicht sofort wieder
1778
+ matcht (Retry-Blocks fangen Endlosschleifen technischer Fehler ab).
1779
+ 7. **Validieren:** `adispatch config validate --config-dir <dir>` → Exit 0, Warnungen lesen.
1780
+ 8. **Trockenlauf:** `adispatch workflow dry-run <id> --config-dir <dir>` – Kandidaten, Filter-Trace
1781
+ (kein `unknown` durch Tippfehler?), Prioritäten, Locks/Blocks, Kapazität.
1782
+ 9. **Aktivieren:** `enabled: true` setzen und `adispatch config reload --config-dir <dir>` (bei Fehlern
1783
+ bleibt die alte Konfiguration aktiv). Alternativ zuerst ein manueller Start über die UI.
1784
+ 10. **Beobachten:** erster Run in der UI (Status, Logs, Lifecycle-Journal), ggf. `max_concurrent` per Override
1785
+ anpassen und danach in YAML übernehmen (Overrides gehen beim Neustart verloren).