@starci/hfs 4.7.31 → 4.7.33

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 (473) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +13 -6
  3. package/emit/contracts.mjs +4 -3
  4. package/emit/form-bounds-reader.ts.partial +58 -0
  5. package/emit/form-bounds.mjs +155 -0
  6. package/emit/graphql-input-bounds.mjs +97 -0
  7. package/emit/input-bounds.mjs +44 -0
  8. package/emit/operation-schemas.mjs +137 -0
  9. package/emit/operations-worker.mjs +2 -2
  10. package/emit/operations.mjs +52 -16
  11. package/emit/schema-worker.mjs +17 -4
  12. package/emit/type-schema.mjs +17 -5
  13. package/package.json +9 -2
  14. package/release/gate.mjs +18 -23
  15. package/release/index.mjs +1 -1
  16. package/release/push.mjs +4 -5
  17. package/runtime/engine/admission.mjs +331 -0
  18. package/runtime/engine/canonical-json.mjs +14 -0
  19. package/runtime/engine/db/blob.mjs +380 -0
  20. package/runtime/engine/db/event-compact.mjs +73 -0
  21. package/runtime/engine/db/ledger-open.mjs +38 -0
  22. package/runtime/engine/db/ledger-paths.mjs +87 -0
  23. package/runtime/engine/db/ledger.mjs +1068 -0
  24. package/runtime/engine/db/machine-connection.mjs +150 -0
  25. package/runtime/engine/db/machine-open.mjs +60 -0
  26. package/runtime/engine/db/machine-schema.mjs +105 -0
  27. package/runtime/engine/db/machine.mjs +1317 -0
  28. package/runtime/engine/db/provider-reservations.mjs +127 -0
  29. package/runtime/engine/db/provision-authority.mjs +76 -0
  30. package/runtime/engine/db/provision-lock.mjs +29 -0
  31. package/runtime/engine/db/provision-operations.mjs +146 -0
  32. package/runtime/engine/db/ref-value.mjs +67 -0
  33. package/runtime/engine/db/schema/machine.sql +920 -0
  34. package/runtime/engine/db/schema/runtime.sql +1113 -0
  35. package/runtime/engine/refuse.mjs +11 -0
  36. package/runtime/knowledge/hfs/canon-pins.yaml +65 -9
  37. package/runtime/knowledge/hfs/design-truths.yaml +149 -0
  38. package/runtime/knowledge/hfs/fe-stack.yaml +23 -5
  39. package/runtime/knowledge/hfs/rules.yaml +269 -48
  40. package/runtime/knowledge/hfs/slots.yaml +113 -53
  41. package/runtime/knowledge/patterns/be/api.yaml +46 -20
  42. package/runtime/knowledge/patterns/be/event-bus.yaml +19 -12
  43. package/runtime/knowledge/patterns/be/idempotency.yaml +265 -0
  44. package/runtime/knowledge/patterns/be/jobs.yaml +17 -12
  45. package/runtime/knowledge/patterns/be/projections.yaml +2 -2
  46. package/runtime/knowledge/patterns/be/queues.yaml +57 -22
  47. package/runtime/knowledge/patterns/be/reactors.yaml +1 -1
  48. package/runtime/knowledge/patterns/be/realtime.yaml +326 -27
  49. package/runtime/knowledge/patterns/be/saga.yaml +2 -2
  50. package/runtime/knowledge/sonar-gate.yaml +1 -1
  51. package/runtime/modules/kernel/command-policy.yaml +249 -0
  52. package/runtime/modules/kernel/failure-codes.yaml +211 -52
  53. package/runtime/modules/kernel/hook-input.yaml +6 -0
  54. package/runtime/modules/kernel/protected-zone.yaml +81 -0
  55. package/runtime/modules/kernel/removed-vocabulary.yaml +62 -0
  56. package/runtime/modules/kernel/roles.yaml +405 -0
  57. package/runtime/modules/models/registry.yaml +3 -1
  58. package/runtime/modules/models/runtimes.yaml +2 -0
  59. package/runtime/modules/schemas/application-stacks.schema.yaml +10 -2
  60. package/runtime/packages/cli/src/catalog.generated.mjs +46 -59
  61. package/runtime/packages/grammar/scripts/build-stamp.mjs +98 -0
  62. package/runtime/scripts/agent/codex-app-server.mjs +79 -0
  63. package/runtime/scripts/agent/codex-mirror-source.mjs +34 -0
  64. package/runtime/scripts/agent/host-agents.mjs +80 -0
  65. package/runtime/scripts/agent/launch-trust-policy.mjs +53 -0
  66. package/runtime/scripts/agent/trust-launch.mjs +191 -0
  67. package/runtime/scripts/agent/trust.mjs +523 -0
  68. package/runtime/scripts/api/fs/rename-over.mjs +21 -0
  69. package/runtime/scripts/api/fs/temp-path.mjs +8 -0
  70. package/runtime/scripts/api/fs/write-json-file.mjs +10 -0
  71. package/runtime/scripts/api/git/diff-names.mjs +8 -0
  72. package/runtime/scripts/api/git/head-time.mjs +10 -0
  73. package/runtime/scripts/api/git/index-lock.mjs +78 -0
  74. package/runtime/scripts/api/git/is-ancestor.mjs +5 -0
  75. package/runtime/scripts/api/git/log.mjs +5 -0
  76. package/runtime/scripts/api/git/merge-base-query.mjs +5 -0
  77. package/runtime/scripts/api/git/merge-tree.mjs +5 -0
  78. package/runtime/scripts/api/git/rev-list.mjs +5 -0
  79. package/runtime/scripts/api/git/rev-parse.mjs +13 -0
  80. package/runtime/scripts/api/git/status-query.mjs +5 -0
  81. package/runtime/scripts/api/git/worktree-list-query.mjs +5 -0
  82. package/runtime/scripts/api/node/spawn-node.mjs +6 -0
  83. package/runtime/scripts/api/npm/dist-tag.mjs +8 -0
  84. package/runtime/scripts/api/npm/lib.mjs +32 -0
  85. package/runtime/scripts/api/npm/pack-dry-run.mjs +9 -0
  86. package/runtime/scripts/api/npm/run-npm.mjs +6 -0
  87. package/runtime/scripts/api/npm/view.mjs +30 -0
  88. package/runtime/scripts/api/npm/whoami.mjs +8 -0
  89. package/runtime/scripts/api/process/hide-child-windows.mjs +31 -0
  90. package/runtime/scripts/api/process/kill-tree.mjs +22 -0
  91. package/runtime/scripts/api/process/lib.mjs +125 -0
  92. package/runtime/scripts/api/process/probe-guard-command.mjs +74 -0
  93. package/runtime/scripts/api/process/process-list.mjs +28 -0
  94. package/runtime/scripts/api/process/system-tool.mjs +37 -0
  95. package/runtime/scripts/gates/canon-digest.mjs +81 -0
  96. package/runtime/scripts/gates/canon-pins.mjs +16 -0
  97. package/runtime/scripts/gates/commit-proof.mjs +53 -0
  98. package/runtime/scripts/gates/gate-input.mjs +98 -0
  99. package/runtime/scripts/gates/gate.mjs +669 -0
  100. package/runtime/scripts/gates/grammar-dist.mjs +206 -0
  101. package/runtime/scripts/gates/runtime-package-selection.mjs +27 -0
  102. package/runtime/scripts/gates/test-world-run.mjs +254 -0
  103. package/runtime/scripts/gates/type-impact.mjs +277 -0
  104. package/runtime/scripts/gates/type-program.mjs +113 -0
  105. package/runtime/scripts/guards/assigned-command.mjs +12 -0
  106. package/runtime/scripts/guards/call-rights.mjs +47 -0
  107. package/runtime/scripts/guards/command-guard.mjs +468 -0
  108. package/runtime/scripts/guards/command-policy.mjs +342 -0
  109. package/runtime/scripts/guards/critic-reach.mjs +82 -0
  110. package/runtime/scripts/guards/deps-guard.mjs +150 -0
  111. package/runtime/scripts/guards/env-dump-verdict.mjs +89 -0
  112. package/runtime/scripts/guards/git-pathspec.mjs +225 -0
  113. package/runtime/scripts/guards/git-policy.mjs +355 -0
  114. package/runtime/scripts/guards/guards-root.mjs +27 -0
  115. package/runtime/scripts/guards/hook-install.mjs +343 -0
  116. package/runtime/scripts/guards/hook-io.mjs +76 -0
  117. package/runtime/scripts/guards/hook-verdict.mjs +12 -0
  118. package/runtime/scripts/guards/install-verdict.mjs +41 -0
  119. package/runtime/scripts/guards/kernel-seat.mjs +63 -0
  120. package/runtime/scripts/guards/launch-verdict.mjs +74 -0
  121. package/runtime/scripts/guards/op-starci.mjs +16 -0
  122. package/runtime/scripts/guards/op-write-scope.mjs +27 -0
  123. package/runtime/scripts/guards/orca-self-lifecycle.mjs +39 -0
  124. package/runtime/scripts/guards/process-kill-verdict.mjs +106 -0
  125. package/runtime/scripts/guards/protected-zone.mjs +56 -0
  126. package/runtime/scripts/guards/refusals.mjs +27 -0
  127. package/runtime/scripts/guards/release-definition.mjs +185 -0
  128. package/runtime/scripts/guards/release-push-gate.mjs +72 -0
  129. package/runtime/scripts/guards/release-record.mjs +58 -0
  130. package/runtime/scripts/guards/release-suite-mode.mjs +15 -0
  131. package/runtime/scripts/guards/rights.mjs +268 -0
  132. package/runtime/scripts/guards/seat-tools.mjs +47 -0
  133. package/runtime/scripts/guards/shell-commands.mjs +328 -0
  134. package/runtime/scripts/guards/supervisor-seat.mjs +80 -0
  135. package/runtime/scripts/guards/verdict-process.mjs +81 -0
  136. package/runtime/scripts/hfs/architecture/dead-exports.mjs +10 -10
  137. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +1 -1
  138. package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +53 -0
  139. package/runtime/scripts/hfs/architecture/reachability.mjs +1 -1
  140. package/runtime/scripts/hfs/architecture/sql-owner.mjs +17 -6
  141. package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +105 -0
  142. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +38 -52
  143. package/runtime/scripts/hfs/architecture/supabase-keys.mjs +261 -0
  144. package/runtime/scripts/hfs/architecture/transport-owner.mjs +49 -3
  145. package/runtime/scripts/hfs/architecture/typescript.mjs +1 -1
  146. package/runtime/scripts/hfs/check.mjs +6 -4
  147. package/runtime/scripts/hfs/coverage-scope.mjs +28 -1
  148. package/runtime/scripts/hfs/db-restamp-verb.mjs +54 -14
  149. package/runtime/scripts/hfs/declaration-shape.mjs +4 -4
  150. package/runtime/scripts/hfs/declaration-slots.mjs +14 -3
  151. package/runtime/scripts/hfs/manifest-shape.mjs +10 -6
  152. package/runtime/scripts/hfs/rule-params-shape.mjs +18 -6
  153. package/runtime/scripts/hfs/rules/a15-findings.mjs +4 -1
  154. package/runtime/scripts/hfs/rules/bypass-markers.mjs +22 -7
  155. package/runtime/scripts/hfs/rules/database-migrations.mjs +69 -14
  156. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +9 -7
  157. package/runtime/scripts/hfs/rules/database-queues.mjs +68 -18
  158. package/runtime/scripts/hfs/rules/edition.mjs +56 -12
  159. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +1 -10
  160. package/runtime/scripts/hfs/rules/fe-stack.mjs +12 -7
  161. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +1 -1
  162. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +75 -9
  163. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +1 -1
  164. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +15 -4
  165. package/runtime/scripts/hfs/rules/text-integrity.mjs +73 -0
  166. package/runtime/scripts/hfs/runtime-rules/hook-shape.mjs +74 -0
  167. package/runtime/scripts/hfs/runtime-rules/release-notes.mjs +44 -0
  168. package/runtime/scripts/hfs/sealed-members.mjs +3 -3
  169. package/runtime/scripts/hfs/slot-side-problems.mjs +3 -3
  170. package/runtime/scripts/hfs/slots.mjs +2 -1
  171. package/runtime/scripts/hfs/stack-deploy-config.mjs +22 -5
  172. package/runtime/scripts/hfs/stacks-gate.mjs +28 -7
  173. package/runtime/scripts/hfs/view.mjs +2 -1
  174. package/runtime/scripts/lib/app-data.mjs +11 -0
  175. package/runtime/scripts/lib/bypass-markers.mjs +272 -75
  176. package/runtime/scripts/lib/check-format.mjs +7 -0
  177. package/runtime/scripts/lib/cli-arg.mjs +120 -0
  178. package/runtime/scripts/lib/css-rules.mjs +220 -0
  179. package/runtime/scripts/lib/dom-tree.mjs +4 -0
  180. package/runtime/scripts/lib/escape.mjs +16 -0
  181. package/runtime/scripts/lib/event-items.mjs +22 -0
  182. package/runtime/scripts/lib/failure-code-template-errors.mjs +161 -0
  183. package/runtime/scripts/lib/git-dir.mjs +120 -0
  184. package/runtime/scripts/lib/grammar-vendor-parity.mjs +377 -0
  185. package/runtime/scripts/lib/guard-command.mjs +24 -0
  186. package/runtime/scripts/lib/hook-limits.mjs +5 -0
  187. package/runtime/scripts/lib/interrupt-cleanup.mjs +38 -0
  188. package/runtime/scripts/lib/js-lex.mjs +200 -0
  189. package/runtime/scripts/lib/json-formats.mjs +0 -1
  190. package/runtime/scripts/lib/language.mjs +94 -1
  191. package/runtime/scripts/lib/loosening-budgets.mjs +44 -0
  192. package/runtime/scripts/lib/loosening-config.mjs +361 -0
  193. package/runtime/scripts/lib/loosening-state.mjs +223 -0
  194. package/runtime/scripts/lib/menu-parts.mjs +11 -0
  195. package/runtime/scripts/lib/normalize.mjs +25 -0
  196. package/runtime/scripts/lib/op-gate.mjs +15 -0
  197. package/runtime/scripts/lib/redact.mjs +208 -0
  198. package/runtime/scripts/lib/runtime-location.mjs +21 -0
  199. package/runtime/scripts/lib/runtime-package-inventory.mjs +21 -0
  200. package/runtime/scripts/lib/runtime-package-manifests.mjs +84 -0
  201. package/runtime/scripts/lib/runtime-publication-tags.mjs +99 -0
  202. package/runtime/scripts/lib/seat-env.mjs +43 -0
  203. package/runtime/scripts/lib/secret-patterns.mjs +58 -9
  204. package/runtime/scripts/lib/shell-source.mjs +4 -0
  205. package/runtime/scripts/lib/source-phrases.mjs +34 -0
  206. package/runtime/scripts/lib/sqlite.mjs +21 -0
  207. package/runtime/scripts/lib/swarm-stack.mjs +2 -1
  208. package/runtime/scripts/lib/tar-files.mjs +39 -0
  209. package/runtime/scripts/lib/ts-analysis.mjs +10 -0
  210. package/runtime/scripts/lib/worktree-exclude.mjs +35 -0
  211. package/runtime/scripts/lib/worktree-record.mjs +14 -0
  212. package/runtime/scripts/machine/artifact-hold.mjs +52 -0
  213. package/runtime/scripts/machine/ask-receipts.mjs +113 -0
  214. package/runtime/scripts/machine/evidence-store.mjs +223 -0
  215. package/runtime/scripts/machine/home.mjs +216 -0
  216. package/runtime/scripts/machine/host-lock.mjs +184 -0
  217. package/runtime/scripts/machine/lock-recovery.mjs +55 -0
  218. package/runtime/scripts/machine/roles-contract.mjs +90 -0
  219. package/runtime/scripts/machine/runtime-change.mjs +20 -0
  220. package/runtime/scripts/machine/workflow-tree.mjs +122 -0
  221. package/runtime/scripts/reconciler/checklist-items.mjs +7 -0
  222. package/runtime/scripts/reconciler/guard-row.mjs +25 -0
  223. package/runtime/scripts/work/asset-slot.mjs +284 -0
  224. package/runtime/scripts/work/brand/brand-colour.mjs +125 -0
  225. package/runtime/scripts/work/brand/brand-tokens.mjs +167 -0
  226. package/runtime/scripts/work/brand/brand-value-source.mjs +106 -0
  227. package/runtime/scripts/work/brand/brand.mjs +892 -0
  228. package/runtime/scripts/work/draw/draw-dna-anatomy.mjs +60 -0
  229. package/runtime/scripts/work/draw/draw-dna-findings.mjs +256 -0
  230. package/runtime/scripts/work/draw/draw-dna-selector.mjs +38 -0
  231. package/runtime/scripts/work/draw/draw-dna.mjs +483 -0
  232. package/runtime/scripts/work/draw/draw-layer.mjs +305 -0
  233. package/runtime/scripts/work/draw/draw-rationale-page.mjs +237 -0
  234. package/runtime/scripts/work/draw/draw-rationale-ref.mjs +14 -0
  235. package/runtime/scripts/work/draw/draw-rationale.mjs +353 -0
  236. package/runtime/scripts/work/draw/draw-source.mjs +412 -0
  237. package/runtime/scripts/work/draw/draw-taste.mjs +278 -0
  238. package/runtime/scripts/work/draw/draw-typecheck.mjs +72 -0
  239. package/runtime/scripts/work/draw/rationale-findings.mjs +205 -0
  240. package/runtime/scripts/work/draw-grammar.mjs +175 -0
  241. package/runtime/scripts/work/draw-render-page.mjs +188 -0
  242. package/runtime/scripts/work/draw-render.mjs +626 -0
  243. package/runtime/scripts/work/grammar-proposal.mjs +279 -0
  244. package/runtime/scripts/work/png.mjs +237 -0
  245. package/runtime/scripts/work/render-tools.mjs +64 -0
  246. package/runtime/scripts/work/work-io.mjs +119 -0
  247. package/scaffold/add-inbox-lite.mjs +2 -2
  248. package/scaffold/add-plan.mjs +40 -13
  249. package/scaffold/add-queue-lite.mjs +372 -0
  250. package/scaffold/add.mjs +5 -2
  251. package/scaffold/app.mjs +1 -2
  252. package/scaffold/edition-gate.mjs +11 -7
  253. package/scaffold/lite-exports.mjs +10 -0
  254. package/scaffold/pattern-file-slots.mjs +33 -0
  255. package/scaffold/test-world.mjs +1 -1
  256. package/scaffold/yaml-block-edit.mjs +117 -0
  257. package/src/main.mjs +5 -4
  258. package/stack/deploy.mjs +7 -3
  259. package/stack/post-deploy.mjs +48 -0
  260. package/sync/index.mjs +10 -0
  261. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +1 -1
  262. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +1 -1
  263. package/templates/app/codegen-graphql.mjs.partial +59 -12
  264. package/templates/app/hooks/husky/pre-commit +2 -2
  265. package/templates/app/hooks/husky/pre-push +10 -3
  266. package/templates/app/package-scripts/redis-dev.json +3 -0
  267. package/templates/app/package-scripts-lite/package.json +1 -0
  268. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +36 -8
  269. package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
  270. package/templates/app/skeleton/.starcistacks/production/README.md +10 -9
  271. package/templates/app/skeleton/.starcistacks/production/infra/swarm/postgres-init.sh +21 -0
  272. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +1 -1
  273. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +1 -0
  274. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +47 -5
  275. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +5 -3
  276. package/templates/app/skeleton/README.md +2 -0
  277. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +5 -0
  278. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +2 -2
  279. package/templates/app/skeleton-lite/.starcistacks/production/README.md +1 -1
  280. package/templates/app/skeleton-lite/README.md +1 -1
  281. package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +18 -0
  282. package/templates/be/inbox/platform/event-bus/errors/event-bus.error.ts.tpl +16 -0
  283. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +10 -0
  284. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +1 -1
  285. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +15 -3
  286. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +11 -3
  287. package/templates/be/inbox/platform/event-bus/index.ts.tpl +2 -0
  288. package/templates/be/inbox/platform/event-bus/messages/event-bus.messages.ts.tpl +7 -0
  289. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +2 -2
  290. package/templates/be/inbox/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
  291. package/templates/be/inbox/platform/idempotency/index.ts.tpl +2 -0
  292. package/templates/be/inbox/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
  293. package/templates/be/inbox/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
  294. package/templates/be/inbox/platform/inbox/client-operation.policy.ts.tpl +17 -0
  295. package/templates/be/inbox/platform/inbox/errors/inbox.error.ts.tpl +22 -0
  296. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +21 -0
  297. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +334 -11
  298. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +55 -4
  299. package/templates/be/inbox/platform/inbox/index.ts.tpl +2 -0
  300. package/templates/be/inbox/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
  301. package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
  302. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +26 -0
  303. package/templates/be/inbox/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
  304. package/templates/be/inbox/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
  305. package/templates/be/inbox/platform/migration.sql.tpl +1 -0
  306. package/templates/be/inbox/service.spec.ts.tpl +4 -4
  307. package/templates/be/inbox/service.ts.tpl +3 -1
  308. package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -2
  309. package/templates/be/inbox/webhook-door.ts.tpl +3 -2
  310. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +3 -0
  311. package/templates/be/patterns/event-bus/platform/event-bus.contracts.ts.tpl +10 -0
  312. package/templates/be/patterns/event-bus/platform/event-bus.port.ts.tpl +2 -2
  313. package/templates/be/patterns/event-bus/platform/event-bus.service.spec.ts.tpl +16 -3
  314. package/templates/be/patterns/event-bus/platform/event-bus.service.ts.tpl +11 -3
  315. package/templates/be/patterns/event-bus/platform/event-envelope.policy.spec.ts.tpl +139 -50
  316. package/templates/be/patterns/event-bus/platform/event-envelope.policy.ts.tpl +25 -2
  317. package/templates/be/patterns/event-bus/platform/event-relay.service.spec.ts.tpl +139 -78
  318. package/templates/be/patterns/event-bus/platform/event-relay.service.ts.tpl +10 -6
  319. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +262 -159
  320. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +32 -5
  321. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +2 -0
  322. package/templates/be/patterns/event-bus/platform/persistence/entities/event-outbox.entity.ts.tpl +5 -1
  323. package/templates/be/patterns/event-bus/platform/persistence/event-bus.rows.ts.tpl +2 -0
  324. package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -4
  325. package/templates/be/patterns/event-bus/platform/persistence/migrations/1789800009000-create-event-outbox.ts.tpl +1 -0
  326. package/templates/be/patterns/idempotency/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
  327. package/templates/be/patterns/idempotency/platform/idempotency/index.ts.tpl +2 -0
  328. package/templates/be/patterns/idempotency/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
  329. package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
  330. package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.ts.tpl +17 -0
  331. package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.spec.ts.tpl +66 -0
  332. package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.ts.tpl +24 -0
  333. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.spec.ts.tpl +60 -0
  334. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.ts.tpl +12 -0
  335. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.spec.ts.tpl +49 -0
  336. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.ts.tpl +12 -0
  337. package/templates/be/patterns/idempotency/platform/inbox/effectful.decorators.ts.tpl +7 -0
  338. package/templates/be/patterns/idempotency/platform/inbox/errors/inbox.error.ts.tpl +22 -0
  339. package/templates/be/patterns/idempotency/platform/inbox/inbox.decorators.ts.tpl +17 -0
  340. package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +7 -0
  341. package/templates/be/patterns/idempotency/platform/inbox/inbox.module.ts.tpl +28 -0
  342. package/templates/be/patterns/idempotency/platform/inbox/inbox.options.ts.tpl +7 -0
  343. package/templates/be/patterns/idempotency/platform/inbox/inbox.port.ts.tpl +44 -0
  344. package/templates/be/patterns/idempotency/platform/inbox/inbox.service.spec.ts.tpl +388 -0
  345. package/templates/be/patterns/idempotency/platform/inbox/inbox.service.ts.tpl +77 -0
  346. package/templates/be/patterns/idempotency/platform/inbox/index.ts.tpl +10 -0
  347. package/templates/be/patterns/idempotency/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
  348. package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +9 -0
  349. package/templates/be/patterns/idempotency/platform/inbox/persistence/entities/inbox-claim.entity.ts.tpl +25 -0
  350. package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
  351. package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +39 -0
  352. package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1789800005000-create-inbox-claims.ts.tpl +21 -0
  353. package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1792800000000-client-operation-outcomes.ts.tpl +21 -0
  354. package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
  355. package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
  356. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +1 -0
  357. package/templates/be/patterns/jobs/platform/job-claim.service.spec.ts.tpl +4 -3
  358. package/templates/be/patterns/jobs/platform/job-claim.service.ts.tpl +2 -0
  359. package/templates/be/patterns/jobs/platform/job-runner.service.spec.ts.tpl +20 -2
  360. package/templates/be/patterns/jobs/platform/job-runner.service.ts.tpl +1 -0
  361. package/templates/be/patterns/jobs/platform/jobs.contracts.ts.tpl +4 -0
  362. package/templates/be/patterns/jobs/platform-lite/jobs.index.ts.tpl +10 -0
  363. package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +44 -0
  364. package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +33 -0
  365. package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +203 -18
  366. package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.ts.tpl +78 -22
  367. package/templates/be/patterns/queues/platform/errors/queue.error.ts.tpl +19 -0
  368. package/templates/be/patterns/queues/platform/messages/queue.messages.ts.tpl +13 -0
  369. package/templates/be/patterns/queues/platform/persistence.migration.ts.tpl +1 -0
  370. package/templates/be/patterns/queues/platform/persistence.queue-outbox.entity.ts.tpl +4 -0
  371. package/templates/be/patterns/queues/platform/persistence.queue.rows.ts.tpl +2 -0
  372. package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -4
  373. package/templates/be/patterns/queues/platform/queue-outbox.service.spec.ts.tpl +14 -2
  374. package/templates/be/patterns/queues/platform/queue-outbox.service.ts.tpl +5 -2
  375. package/templates/be/patterns/queues/platform/queue-relay.service.spec.ts.tpl +72 -74
  376. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +1 -1
  377. package/templates/be/patterns/queues/platform/queue-transport.port.ts.tpl +23 -8
  378. package/templates/be/patterns/queues/platform/queue-worker.service.spec.ts.tpl +29 -8
  379. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +7 -3
  380. package/templates/be/patterns/queues/platform/queue.contracts.ts.tpl +5 -0
  381. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +2 -0
  382. package/templates/be/patterns/queues/platform/queue.log-events.ts.tpl +4 -0
  383. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +2 -1
  384. package/templates/be/patterns/queues/platform/queue.port.ts.tpl +1 -1
  385. package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.spec.ts.tpl +417 -0
  386. package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.ts.tpl +172 -0
  387. package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +12 -0
  388. package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +32 -0
  389. package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +148 -0
  390. package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +51 -0
  391. package/templates/be/patterns/queues/platform-lite/queue-transport.port.ts.tpl +163 -0
  392. package/templates/be/patterns/queues/platform-lite/queue.config.ts.tpl +17 -0
  393. package/templates/be/patterns/queues/platform-lite/queue.error.ts.tpl +25 -0
  394. package/templates/be/patterns/queues/platform-lite/queue.index.ts.tpl +8 -0
  395. package/templates/be/patterns/queues/platform-lite/queue.messages.ts.tpl +21 -0
  396. package/templates/be/patterns/queues/platform-lite/queue.module.ts.tpl +74 -0
  397. package/templates/be/patterns/queues/platform-lite/queue.options.ts.tpl +38 -0
  398. package/templates/be/patterns/queues/queue.queue.ts.tpl +2 -2
  399. package/templates/be/patterns/reactors/application.contracts.ts.tpl +2 -0
  400. package/templates/be/patterns/reactors/consumer.ts.tpl +5 -1
  401. package/templates/be/platform/graphql/graphql.contracts.ts +2 -1
  402. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +1 -1
  403. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +6 -1
  404. package/templates/be/skeleton/apps/__app__/src/main.ts +7 -4
  405. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +4 -0
  406. package/templates/be/skeleton/apps/cli/src/main.ts +7 -4
  407. package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -1
  408. package/templates/be/skeleton/src/modules/platform/http/errors/http.error.ts +22 -0
  409. package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.spec.ts +235 -0
  410. package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.ts +85 -0
  411. package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.spec.ts +92 -0
  412. package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.ts +52 -0
  413. package/templates/be/skeleton/src/modules/platform/http/http.decorators.ts +9 -0
  414. package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +7 -0
  415. package/templates/be/skeleton/src/modules/platform/http/http.module.ts +19 -0
  416. package/templates/be/skeleton/src/modules/platform/http/http.options.ts +2 -0
  417. package/templates/be/skeleton/src/modules/platform/http/http.port.ts +40 -0
  418. package/templates/be/skeleton/src/modules/platform/http/index.ts +7 -0
  419. package/templates/be/skeleton/src/modules/platform/http/messages/http.messages.ts +15 -0
  420. package/templates/be/skeleton/src/modules/platform/ids/ids.decorators.ts +9 -0
  421. package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +7 -0
  422. package/templates/be/skeleton/src/modules/platform/ids/ids.module.ts +19 -0
  423. package/templates/be/skeleton/src/modules/platform/ids/ids.options.ts +2 -0
  424. package/templates/be/skeleton/src/modules/platform/ids/ids.port.ts +5 -0
  425. package/templates/be/skeleton/src/modules/platform/ids/index.ts +4 -0
  426. package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.spec.ts +15 -0
  427. package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.ts +10 -0
  428. package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.spec.ts +59 -0
  429. package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.ts +21 -0
  430. package/templates/be/skeleton/src/modules/platform/logging/correlation.contracts.ts +16 -0
  431. package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.spec.ts +150 -0
  432. package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.ts +42 -0
  433. package/templates/be/skeleton/src/modules/platform/logging/correlation.port.ts +15 -0
  434. package/templates/be/skeleton/src/modules/platform/logging/correlation.service.spec.ts +23 -0
  435. package/templates/be/skeleton/src/modules/platform/logging/correlation.service.ts +28 -0
  436. package/templates/be/skeleton/src/modules/platform/logging/index.ts +14 -1
  437. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +39 -6
  438. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +15 -3
  439. package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +17 -2
  440. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +13 -1
  441. package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +13 -3
  442. package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.spec.ts +69 -0
  443. package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.ts +48 -0
  444. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  445. package/templates/be/skeleton/src/modules/platform/primitives/record.policy.spec.ts +12 -0
  446. package/templates/be/skeleton/src/modules/platform/primitives/record.policy.ts +3 -0
  447. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +6 -1
  448. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +4 -0
  449. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
  450. package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +14 -1
  451. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +12 -0
  452. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  453. package/templates/be/test-world/world/test-apps.options.ts.tpl +2 -0
  454. package/templates/be/test-world/world/test-world.config.ts.tpl +1 -0
  455. package/upgrade/check.mjs +12 -135
  456. package/upgrade/file-state.mjs +1 -16
  457. package/upgrade/index.mjs +34 -361
  458. package/runtime/scripts/lib/check-code-loosening.mjs +0 -128
  459. package/runtime/scripts/lib/gate-loosening.mjs +0 -292
  460. package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +0 -89
  461. package/templates/app/upgrade-full/.starcistacks/dev/README.md +0 -16
  462. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/api.yaml +0 -13
  463. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/compose.yaml +0 -3
  464. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/postgres.yaml +0 -12
  465. package/templates/app/upgrade-full/.starcistacks/dev/infra/metadata.json +0 -7
  466. package/templates/be/skeleton-lite/apps/api/src/main.ts +0 -26
  467. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +0 -12
  468. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +0 -7
  469. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +0 -5
  470. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +0 -9
  471. package/templates/be/upgrade-full/apps/cli/src/main.ts +0 -29
  472. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +0 -7
  473. package/templates/be/upgrade-full/src/features/cli/migrate/subs/run.cli.ts +0 -23
@@ -0,0 +1,223 @@
1
+ // evidence-store.mjs — agent output into runtime.sqlite + the blob store (docs/ledger-db.md).
2
+ //
3
+ // Bytes live in the content-addressed store (engine/db/blob.mjs, <runtime root>/.runtime/artifacts/<sha[0:2]>/<sha>);
4
+ // the ledger holds only the index rows that point at them:
5
+ // blobs one row per sha this ledger references (media type, redaction, file uri)
6
+ // job_artifacts every file output, keyed (attempt_id, name) — or (workflow_id, name) for the Kernel's
7
+ // own outputs — immutable: a second put with other bytes is refused (H10)
8
+ // check_runs every op / settler / parity / integrate check: the RAW exit the runner saw apart from
9
+ // the exit the op DECLARED (H8); 'unavailable' is its own status and never red (H7)
10
+ // attempt_transcript_snapshots the redacted scrollback of a live op terminal, every 60 s
11
+ // op_attempts.transcript_sha the full redacted scrollback when the attempt ends
12
+ // Text bytes pass scripts/lib/redact.mjs before every put (blobs.redaction='v1'); media is stored as is ('binary').
13
+ // The rows go through the runtime writer (engine/db/ledger.mjs recordBlob, recordArtifact, recordCheckRun,
14
+ // recordTranscriptSnapshot, setAttemptTranscript); this module is the evidence policy on top of it: redaction,
15
+ // naming, kinds and roles, the H7/H8 status rules. Every write runs inside the caller's ledger.transaction; the blob
16
+ // put happens BEFORE the transaction (stageBlob), so a rolled-back transaction leaves only an unreferenced blob for GC.
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { putBlob, blobPath } from '../../engine/db/blob.mjs';
20
+ import { redactBytes } from '../lib/redact.mjs';
21
+ import { recordBlob, recordArtifact, recordCheckRun as writeCheckRun, recordTranscriptSnapshot, attachToReport, setAttemptTranscript,
22
+ JOB_ARTIFACT_KINDS, JOB_ARTIFACT_SUBKINDS, JOB_ARTIFACT_ROLES } from '../../engine/db/ledger.mjs';
23
+ import { newSpanId } from '../../engine/db/machine.mjs';
24
+ import { refuse } from '../../engine/refuse.mjs';
25
+
26
+ const ARTIFACT_ROLES = JOB_ARTIFACT_ROLES;
27
+ export const ARTIFACT_KINDS = JOB_ARTIFACT_KINDS;
28
+ export const ARTIFACT_SUBKINDS = JOB_ARTIFACT_SUBKINDS;
29
+ const ARTIFACT_ORIGINS = Object.freeze(['op', 'settler', 'checker', 'kernel']);
30
+ const CHECK_PHASES = Object.freeze(['before', 'after', 'verify', 'parity', 'integrate']);
31
+ const CHECK_RUNNERS = Object.freeze(['op', 'settler', 'kernel', 'parity', 'integrate']);
32
+ export const CHECK_STATUSES = Object.freeze(['pass', 'fail', 'unavailable', 'error', 'skipped']);
33
+ /** A check whose status counts as red. 'unavailable' (the checker could not run: infra) and 'skipped' never do (H7). */
34
+ const RED_CHECK_STATUSES = Object.freeze(['fail', 'error']);
35
+ export const TRANSCRIPT_SNAPSHOT_MS = 60_000;
36
+
37
+
38
+ const slash = (p) => String(p).replaceAll('\\', '/');
39
+
40
+ const IMAGE = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp', '.gif': 'image/gif', '.svg': 'image/svg+xml' };
41
+ const VIDEO = { '.webm': 'video/webm', '.mp4': 'video/mp4', '.mov': 'video/quicktime' };
42
+ const OTHER = { '.json': 'application/json', '.md': 'text/markdown', '.yaml': 'text/yaml', '.yml': 'text/yaml', '.txt': 'text/plain',
43
+ '.log': 'text/plain', '.jsonl': 'application/x-ndjson', '.out': 'text/plain', '.patch': 'text/x-diff', '.diff': 'text/x-diff', '.html': 'text/html',
44
+ '.htm': 'text/html', '.mhtml': 'multipart/related', '.zip': 'application/zip', '.trace': 'application/octet-stream', '.csv': 'text/csv', '.xml': 'application/xml' };
45
+ /** The media type of a file, from its extension. */
46
+ export const mediaTypeOf = (file) => { const ext = path.extname(String(file)).toLowerCase(); return IMAGE[ext] ?? VIDEO[ext] ?? OTHER[ext] ?? 'application/octet-stream'; };
47
+
48
+ /** The job_artifacts.kind of a file, from its name. */
49
+ export function kindOf(file) {
50
+ const base = path.basename(String(file)).toLowerCase(), ext = path.extname(base);
51
+ if (ext === '.patch') return 'patch';
52
+ if (ext === '.diff') return 'diff';
53
+ if (IMAGE[ext]) return 'image';
54
+ if (VIDEO[ext]) return 'video';
55
+ if (ext === '.trace' || (ext === '.zip' && base.includes('trace'))) return 'trace';
56
+ if (/^(report|result)([.-].*)?\.(json|md|ya?ml)$/.test(base)) return 'report';
57
+ if (['.log', '.txt', '.out', '.jsonl'].includes(ext)) return 'log';
58
+ return 'file';
59
+ }
60
+
61
+ /** The artifact role of an attached file, from its name. */
62
+ export function roleOf(file) {
63
+ const kind = kindOf(file), ext = path.extname(String(file)).toLowerCase();
64
+ if (kind === 'image') return 'screenshot';
65
+ if (kind === 'video') return 'video';
66
+ if (kind === 'trace') return 'trace';
67
+ if (kind === 'patch') return 'patch';
68
+ if (kind === 'diff') return 'diff';
69
+ if (kind === 'log') return 'log';
70
+ if (['.html', '.htm', '.mhtml'].includes(ext)) return 'dom';
71
+ return 'report-attachment';
72
+ }
73
+
74
+ // --------------------------------------------------------------------------------------------- blobs
75
+ /**
76
+ * Put bytes (a Buffer, a string, or a file path) in the blob store, redacted first when they are text. Runs OUTSIDE
77
+ * the ledger transaction. Returns {sha, bytes, mediaType, redaction, fileUri} for registerBlob / putArtifact.
78
+ * @param {string|Uint8Array|ArrayBuffer|ArrayLike<number>} source
79
+ * @param {{mediaType?: string|null, file?: boolean|null, repoRoots?: string[]}} [options]
80
+ */
81
+ export function stageBlob(source, { mediaType = null, file = null, repoRoots = [] } = {}) {
82
+ let raw;
83
+ if (Buffer.isBuffer(source)) raw = source;
84
+ else if (typeof source === 'string' && file === false) raw = Buffer.from(source, 'utf8');
85
+ else if (typeof source === 'string') raw = fs.readFileSync(source);
86
+ else raw = Buffer.from(source);
87
+ const type = mediaType ?? (typeof source === 'string' && file !== false ? mediaTypeOf(source) : 'application/octet-stream');
88
+ const { bytes, redaction } = redactBytes(raw, type, { repoRoots });
89
+ const put = putBlob(bytes, { mediaType: type });
90
+ return { sha: put.sha, bytes: put.size, mediaType: type, redaction, fileUri: slash(blobPath(put.sha)) };
91
+ }
92
+
93
+ /** Text as a staged blob (redacted), or null for empty text. */
94
+ export const stageText = (text, { mediaType = 'text/plain', repoRoots = [] } = {}) =>
95
+ (typeof text === 'string' && text.length ? stageBlob(text, { mediaType, file: false, repoRoots }) : null);
96
+
97
+ /** The blobs row of one staged blob; a sha already known keeps its first row. Returns the sha. */
98
+ export function registerBlob(db, staged, { now = Date.now() } = {}) {
99
+ if (!staged?.sha) return null;
100
+ recordBlob(db, { sha256: staged.sha, bytes: staged.bytes, mediaType: staged.mediaType, fileUri: staged.fileUri, redaction: staged.redaction ?? null, createdAt: now });
101
+ return staged.sha;
102
+ }
103
+
104
+ // ----------------------------------------------------------------------------------------- artifacts
105
+ /**
106
+ * One job_artifacts row for a staged blob. `name` is the logical name, unique per attempt (per workflow for a
107
+ * Kernel artifact, attemptId null). Filing the same name with the same bytes again returns the existing row;
108
+ * other bytes under a filed name are refused `artifact-immutable` (H10). Returns {artifactId, created}.
109
+ */
110
+ export function putArtifact(db, { workflowId, attemptId = null, jobId = null, opId = null, cut = null, role, kind = null, subkind = null,
111
+ name, blob, label = null, scopeRef = null, round = null, runId = null, origin, baseSha = null, headSha = null, integratedSha = null, now = Date.now() }) {
112
+ if (!ARTIFACT_ROLES.includes(role)) throw refuse(`artifact role must be ${ARTIFACT_ROLES.join('|')}, got '${role}'`, 'artifact-role-unknown');
113
+ if (!ARTIFACT_ORIGINS.includes(origin)) throw refuse(`artifact origin must be ${ARTIFACT_ORIGINS.join('|')}, got '${origin}'`, 'artifact-origin-unknown');
114
+ const logical = slash(name ?? '').replace(/^\/+/, '');
115
+ if (!logical || logical.split('/').includes('..')) throw refuse(`unsafe artifact name: ${name}`, 'artifact-name-invalid');
116
+ const k = kind ?? kindOf(logical);
117
+ if (!ARTIFACT_KINDS.includes(k)) throw refuse(`artifact kind must be ${ARTIFACT_KINDS.join('|')}, got '${k}'`, 'artifact-kind-unknown');
118
+ if (subkind != null && !ARTIFACT_SUBKINDS.includes(subkind)) throw refuse(`artifact subkind must be ${ARTIFACT_SUBKINDS.join('|')}, got '${subkind}'`, 'artifact-subkind-unknown');
119
+ registerBlob(db, blob, { now });
120
+ const before = attemptId != null ? db.prepare('SELECT artifact_id FROM job_artifacts WHERE attempt_id=? AND name=?').get(attemptId, logical)
121
+ : db.prepare('SELECT artifact_id FROM job_artifacts WHERE workflow_id=? AND attempt_id IS NULL AND name=?').get(workflowId, logical);
122
+ let row;
123
+ try {
124
+ row = recordArtifact(db, { workflowId, attemptId, name: logical, sha256: blob.sha, role, kind: k, origin, subkind, label, scopeRef, round, runId, cut,
125
+ baseSha, headSha, integratedSha, createdAt: now, ...(attemptId == null ? { jobId, opId } : {}) });
126
+ } catch (error) {
127
+ if (error?.code === 'STARCI_ARTIFACT_IMMUTABLE') throw refuse(`artifact '${logical}' of attempt ${attemptId ?? '(kernel)'} is already filed with other bytes; artifacts are immutable, file a new name`, 'artifact-immutable', { name: logical });
128
+ throw error;
129
+ }
130
+ return { artifactId: row.artifact_id, created: !before };
131
+ }
132
+
133
+ /** Link report → artifact (report_attachments). */
134
+ export const linkReportAttachment = (db, { reportId, artifactId }) => attachToReport(db, { reportId, artifactId });
135
+
136
+ // ------------------------------------------------------------------------------------------- attempts
137
+ /** The op_attempts row of a dispatch (workflow + dispatch id), else of the job's latest dispatch, else null. */
138
+ export function attemptOf(db, { workflowId = null, dispatchId = null, jobId = null, attemptId = null } = {}) {
139
+ if (attemptId != null) return db.prepare('SELECT * FROM op_attempts WHERE attempt_id=?').get(attemptId) ?? null;
140
+ if (workflowId && dispatchId) { const row = db.prepare('SELECT * FROM op_attempts WHERE workflow_id=? AND dispatch_id=?').get(workflowId, dispatchId); if (row) return row; }
141
+ if (jobId) return db.prepare('SELECT * FROM op_attempts WHERE job_id=? ORDER BY dispatch_seq DESC LIMIT 1').get(jobId) ?? null;
142
+ return null;
143
+ }
144
+
145
+ // ------------------------------------------------------------------------------------------- checks
146
+ /**
147
+ * The status of one check run. `unavailable` (the checker could not run: missing tool, host down, spawn error)
148
+ * is infra, never red (H7); a runner-observed non-zero exit is never 'pass' (H8).
149
+ * @param {{exitCode?: number|null, declaredExitCode?: number|null, unavailable?: boolean, error?: boolean,
150
+ * skipped?: boolean, status?: string|null}} [options]
151
+ */
152
+ function checkStatusOf({ exitCode = null, declaredExitCode = null, unavailable = false, error = false, skipped = false, status = null } = {}) {
153
+ if (status && !CHECK_STATUSES.includes(status)) throw refuse(`check status must be ${CHECK_STATUSES.join('|')}, got '${status}'`, 'check-status-unknown');
154
+ if (skipped || status === 'skipped') return 'skipped';
155
+ if (unavailable || status === 'unavailable') return 'unavailable';
156
+ if (error || status === 'error') return 'error';
157
+ // The raw exit wins over the declared one; an explicit 'fail' (a green exit with red findings) stays fail.
158
+ let exit = null;
159
+ if (Number.isInteger(exitCode)) exit = exitCode;
160
+ else if (Number.isInteger(declaredExitCode)) exit = declaredExitCode;
161
+ if (exit == null) return status === 'pass' || status === 'fail' ? status : 'error';
162
+ return exit === 0 && status !== 'fail' ? 'pass' : 'fail';
163
+ }
164
+
165
+ const integerOrNull = (value) => (Number.isInteger(value) ? value : null);
166
+
167
+ // The raw exit the runtime observed and the exit an op declared: an op's own exit is only its claim.
168
+ function exitCodesOf(runner, exitCode, declaredExitCode) {
169
+ if (runner !== 'op') return { raw: integerOrNull(exitCode), declared: integerOrNull(declaredExitCode) };
170
+ return { raw: null, declared: Number.isInteger(exitCode) ? exitCode : integerOrNull(declaredExitCode) };
171
+ }
172
+
173
+ /**
174
+ * One check_runs row (engine/db/ledger.mjs recordCheckRun). `runner` 'op' is always authority 'declared' (the op's own claim: exitCode is stored as
175
+ * declared_exit_code, exit_code stays NULL — the runtime did not observe it); the runtime's own runners store the
176
+ * raw exit they observed in exit_code. stdout/stderr/output are staged blobs (stageBlob/stageText) or null.
177
+ * run_seq numbers re-runs of the same (attempt, runner, phase, name). Returns {checkId, runSeq, status, red}.
178
+ */
179
+ export function recordCheck(db, { attemptId, name, phase, runner, authority = null, command = null, cwd = null,
180
+ inputDigest = null, exitCode = null, declaredExitCode = null, attribution = null, status = null, unavailable = false, error = false, skipped = false,
181
+ startedAt = null, finishedAt = null, wallMs = null, stdout = null, stderr = null, output = null, summary = null, note = null,
182
+ spanId = newSpanId(), now = Date.now() }) {
183
+ if (!CHECK_RUNNERS.includes(runner)) throw refuse(`check runner must be ${CHECK_RUNNERS.join('|')}, got '${runner}'`, 'check-runner-unknown');
184
+ if (!CHECK_PHASES.includes(phase)) throw refuse(`check phase must be ${CHECK_PHASES.join('|')}, got '${phase}'`, 'check-phase-unknown');
185
+ if (attemptId == null) throw refuse(`check '${name}' has no attempt`, 'check-attempt-missing');
186
+ const auth = runner === 'op' ? 'declared' : authority ?? 'runtime';
187
+ const { raw, declared } = exitCodesOf(runner, exitCode, declaredExitCode);
188
+ const st = checkStatusOf({ exitCode: raw, declaredExitCode: declared, unavailable, error, skipped, status });
189
+ for (const b of [stdout, stderr, output]) if (b) registerBlob(db, b, { now });
190
+ const wall = wallMs ?? (Number.isFinite(startedAt) && Number.isFinite(finishedAt) ? finishedAt - startedAt : null);
191
+ const row = writeCheckRun(db, { attemptId, name, phase, runner, authority: auth, status: st, spanId, createdAt: now, command, cwd, inputDigest,
192
+ exitCode: raw, declaredExitCode: declared, attributionJson: attribution == null ? null : JSON.stringify(attribution), startedAt, finishedAt, wallMs: wall,
193
+ stdoutSha: stdout?.sha ?? null, stderrSha: stderr?.sha ?? null, outputSha: output?.sha ?? null,
194
+ summaryJson: summary == null ? null : JSON.stringify(summary), note });
195
+ return { checkId: row.check_id, runSeq: row.run_seq, status: st, red: RED_CHECK_STATUSES.includes(st) };
196
+ }
197
+
198
+ /** The latest run of every check of an attempt (optionally one runner): [row] ordered by check_id. */
199
+ export const latestCheckRuns = (db, attemptId, { runner = null } = {}) => db.prepare(`SELECT c.* FROM check_runs c
200
+ WHERE c.attempt_id=? ${runner ? 'AND c.runner=?' : ''} AND c.run_seq=(SELECT max(run_seq) FROM check_runs x
201
+ WHERE x.attempt_id=c.attempt_id AND x.runner=c.runner AND x.phase=c.phase AND x.name=c.name) ORDER BY c.check_id`)
202
+ .all(attemptId, ...(runner ? [runner] : []));
203
+
204
+ // ---------------------------------------------------------------------------------------- transcripts
205
+ const linesOf = (text) => (text ? text.split('\n').length : 0);
206
+
207
+ /** A periodic scrollback snapshot of a live attempt; an unchanged scrollback adds no row. Returns {written, sha}. */
208
+ export function recordAttemptSnapshot(db, { attemptId, text, blob = null, at = Date.now() }) {
209
+ const staged = blob ?? stageText(text);
210
+ if (!staged) return { written: false, sha: null };
211
+ registerBlob(db, staged, { now: at });
212
+ const written = recordTranscriptSnapshot(db, { attemptId, sha256: staged.sha, lines: linesOf(text ?? ''), bytes: staged.bytes, at });
213
+ return { written, sha: staged.sha };
214
+ }
215
+
216
+ /** The attempt's full redacted scrollback at its end: op_attempts.transcript_sha. Returns the sha or null. */
217
+ export function recordFinalTranscript(db, { attemptId, text = null, blob = null, at = Date.now() }) {
218
+ const staged = blob ?? stageText(text);
219
+ if (!staged || attemptId == null) return null;
220
+ registerBlob(db, staged, { now: at });
221
+ setAttemptTranscript(db, { attemptId, transcriptSha: staged.sha, at });
222
+ return staged.sha;
223
+ }
@@ -0,0 +1,216 @@
1
+ // home.mjs — the Supervisor's durable state, in machine.sqlite (engine/db/machine.mjs, DBTREE.sql B1; decision Q3).
2
+ //
3
+ // There is no supervisor ledger and no supervisor home directory: every part is a machine.sqlite row.
4
+ // Where each part lives:
5
+ // seat seats row 'supervisor' (role supervisor): state booting while a launcher holds the startup reservation
6
+ // (detail_json.expiresAt), live once spawned; terminal_handle/agent/model/pid; detail_json {token, value}
7
+ // enabled sup_signals scope 'supervisor-enabled' key 'main' {enabled, by, at}
8
+ // jobs sup_jobs (runtime.fix [Worker] jobs), sup_leases (file leases), sup_attempts, sup_reports
9
+ // audit sup_events (the digest-chained audit trail; kinds as before: supervisor-*, worker-*, owed-*, ...)
10
+ // learning sup_learning; owed items and their acks sup_owed; owner rulings sup_owner_rulings; messages sup_messages
11
+ // logs machine_logs actor 'supervisor' (supervisorLog; the Supervisor writes no text logs)
12
+ // Product ledgers are only ever read from here.
13
+ import os from 'node:os';
14
+ import path from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { DEFAULT_OWNER_LANGUAGE, loadConfig } from '../../engine/config.mjs';
17
+ import { LOCAL_ROOT_ENV, machineLog, readMachine, starciLocalRoot, withMachine } from '../../engine/db/machine.mjs';
18
+ import { headTime } from '../api/git/head-time.mjs';
19
+ import { readEnv } from '../lib/env.mjs';
20
+
21
+ export const SKILL_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
22
+
23
+ /**
24
+ * The rev of the runtime this process runs: '<HEAD committer time, ms, 13 digits>:<short sha>' of the .claude checkout
25
+ * (STARCI_RUNTIME_REV overrides), 'unknown' outside a checkout. The machine db never spawns git: a writer that stamps a
26
+ * store row passes this in. Computed once per process.
27
+ */
28
+ let cachedRev = null;
29
+ export function runtimeRevOf() {
30
+ if (cachedRev) return cachedRev;
31
+ if (readEnv('STARCI_RUNTIME_REV')) {
32
+ cachedRev = String(readEnv('STARCI_RUNTIME_REV'));
33
+ return cachedRev;
34
+ }
35
+ const head = headTime(SKILL_ROOT);
36
+ cachedRev = head ? `${String(head.time * 1000).padStart(13, '0')}:${head.sha}` : 'unknown';
37
+ return cachedRev;
38
+ }
39
+ export const SUPERVISOR_ID = 'main';
40
+ /** The seats row of the one Supervisor seat. */
41
+ export const SEAT_ID = 'supervisor';
42
+ const ENABLED_SCOPE = 'supervisor-enabled';
43
+ export const SUPERVISOR_TITLE = `[Supervisor] ${SUPERVISOR_ID}`;
44
+ export const SUPERVISOR_SEAT = Object.freeze({ id: SUPERVISOR_ID, seatId: SEAT_ID, enabledScope: ENABLED_SCOPE,
45
+ eventPrefix: 'supervisor', title: SUPERVISOR_TITLE });
46
+ export const WORKER_TITLE_PREFIX = '[Worker]';
47
+ export const FIX_KIND = 'runtime.fix';
48
+ export const STARTUP_RESERVATION_MS = 180_000;
49
+
50
+ export const DEFAULTS = Object.freeze({
51
+ workers: Object.freeze({ base: 0, max: 0, hardMax: 10 }),
52
+ landGate: Object.freeze({ mode: 'shared' }),
53
+ frozenMinutes: 10,
54
+ pollIntervalMs: 600_000,
55
+ });
56
+
57
+ // The land scratch trees live under the one lanes root (lanesRoot below), never on C: unless the owner puts it there. A [Worker]
58
+ // staging checkout is an Orca worktree (workers.mjs createStaging): Orca places it, the job records where.
59
+
60
+ /** The host-state sub-root `archive` defaults to: <starciLocalRoot>/archive, i.e. <runtime root>/.runtime/archive. */
61
+ const stateRootChild = (name, env) => path.join(starciLocalRoot(env), name);
62
+ /**
63
+ * Lane worktrees are git worktrees, so their default stays OUT of the runtime's own checkout (and its .runtime): the per-user
64
+ * profile directory (<LOCALAPPDATA>/StarCi/lanes, ~/.local/state/StarCi/lanes), or <STARCI_LOCAL_ROOT>/lanes when that seam is set.
65
+ * The owner moves it with roots.lanes or STARCI_LANES_ROOT.
66
+ */
67
+ const lanesDefault = (env) => (readEnv(LOCAL_ROOT_ENV, env) ? stateRootChild('lanes', env)
68
+ : path.join(readEnv('LOCALAPPDATA', env) || path.join(os.homedir(), '.local', 'state'), 'StarCi', 'lanes'));
69
+ /** The owner's relocation of a host root (config.yaml `roots.<key>`, validated by engine/config.mjs), or null. `config` is the owner config (default: loadConfig()). */
70
+ const ownerRoot = (key, config) => {
71
+ try { return (config === undefined ? loadConfig() : config)?.roots?.[key] ?? null; } catch { return null; }
72
+ };
73
+
74
+ /**
75
+ * The one lane-worktree root: env STARCI_LANES_ROOT (specs, a one-off run), then the owner config `roots.lanes`
76
+ * (config.yaml, gitignored), else lanesDefault (outside the checkout). No host location is written in a tracked file.
77
+ */
78
+ export function lanesRoot({ env = process.env, config = undefined } = {}) {
79
+ return path.resolve(String(env?.STARCI_LANES_ROOT || ownerRoot('lanes', config) || lanesDefault(env)));
80
+ }
81
+
82
+ /** The one archive root (session files, blob retention, ledger backups): env STARCI_ARCHIVE_ROOT, then the owner config `roots.archive`, else <starciLocalRoot>/archive. */
83
+ export function archiveRoot({ env = process.env, config = undefined } = {}) {
84
+ return path.resolve(String(env?.STARCI_ARCHIVE_ROOT || ownerRoot('archive', config) || stateRootChild('archive', env)));
85
+ }
86
+
87
+ export const landRoot = (env = process.env) => path.join(lanesRoot({ env }), 'land');
88
+
89
+ /** fn(machine handle) over a writer, closed afterwards. */
90
+ export const withSupervisor = (fn, { env = process.env } = {}) => withMachine(fn, { env });
91
+ /** fn(machine handle) over a read-only handle; `fallback` when machine.sqlite does not exist or cannot be read. */
92
+ export const readSupervisor = (fn, fallback = null, { env = process.env } = {}) => readMachine(fn, fallback, { env });
93
+
94
+ /**
95
+ * The `{env}`-bound reader of a supervisor-store projection `read`: `fresh` yields the fallback when the
96
+ * store is absent (a thunk, so the value is never shared between calls).
97
+ */
98
+ export const supervisorRead = (read, fresh) => ({ env = process.env } = {}) => readSupervisor(read, fresh(), { env });
99
+
100
+ /**
101
+ * The seat: {token, value, at, expiresAt, pid, expired, starting} or null (no seat, or an empty one). `value` is what
102
+ * the launcher wrote ({terminal, agent, model, ...} or {state:'starting', attempt}).
103
+ */
104
+ export function seatOf(m, now = Date.now(), profile = SUPERVISOR_SEAT) {
105
+ const row = m.seatOf(profile.seatId);
106
+ if (!row || row.state === 'empty') return null;
107
+ const detail = row.detail ?? {};
108
+ const expiresAt = detail.expiresAt ?? null;
109
+ const expired = expiresAt != null && expiresAt <= now;
110
+ const value = detail.value ?? {};
111
+ return { token: detail.token ?? null, value, at: row.booted_at ?? row.last_seen_at ?? null, expiresAt, pid: row.pid ?? null, expired, starting: value.state === 'starting' && !expired, state: row.state };
112
+ }
113
+ /** Write the seat (replacing it): a {state:'starting'} value is the startup reservation (state booting, with expiry). A new seat starts with no refused input: the run of failures belongs to the seat it replaced. */
114
+ export function writeSeat(m, { token, value, expiresAt = null, now = Date.now(), profile = SUPERVISOR_SEAT }) {
115
+ const starting = value?.state === 'starting';
116
+ return m.upsertSeat({ seatId: profile.seatId, role: 'supervisor', state: starting ? 'booting' : 'live', parkedReason: null,
117
+ terminalHandle: value?.terminal ?? null, agent: value?.agent ?? null, model: value?.model ?? null, pid: process.pid,
118
+ bootedAt: now, lastSeenAt: now, inputFailuresConsecutive: 0, lastInputFailureAt: null, detailJson: { token, value, expiresAt } });
119
+ }
120
+ /** Empty the seat (only the holder of `token` when given). True when it was cleared. */
121
+ export function clearSeat(m, { token = null, profile = SUPERVISOR_SEAT } = {}) {
122
+ const cur = seatOf(m, Date.now(), profile);
123
+ if (!cur || (token && cur.token !== token)) return false;
124
+ m.upsertSeat({ seatId: profile.seatId, role: 'supervisor', state: 'empty', terminalHandle: null, pid: null, detailJson: null });
125
+ return true;
126
+ }
127
+
128
+ /** Whether the owner enabled the seat: true / false, or null when never set. */
129
+ export function enabledOf(m, profile = SUPERVISOR_SEAT) {
130
+ const row = m.supSignal(profile.enabledScope, profile.id);
131
+ return row ? row.value?.enabled === true : null;
132
+ }
133
+ export function setEnabled(m, enabled, { by = 'cli', now = Date.now(), profile = SUPERVISOR_SEAT } = {}) {
134
+ m.transaction(() => {
135
+ m.setSupSignal({ scope: profile.enabledScope, key: profile.id, value: { enabled, by, at: new Date(now).toISOString() } });
136
+ supervisorEvent(m, { entityId: profile.id, kind: `${profile.eventPrefix}-${enabled ? 'enabled' : 'disabled'}`, payload: { by }, now });
137
+ });
138
+ }
139
+
140
+ /** Native supervised seats retain their terminals until their owning launcher proves closure. */
141
+ export function supervisedSeatHandles(m) {
142
+ return new Set(m.seats().filter(row => row.role === 'supervisor' && row.state !== 'empty')
143
+ .map(row => row.terminal_handle ?? row.detail?.value?.terminal).filter(Boolean));
144
+ }
145
+
146
+ /** Append one audit event (sup_events). */
147
+ export function supervisorEvent(m, { entityType = 'supervisor', entityId = SUPERVISOR_ID, kind, payload = null, now = Date.now() }) {
148
+ return m.supEvent({ entityType, entityId, kind, payload, at: now });
149
+ }
150
+ /** The newest event of `kind` as {at, ...payload}, or null. */
151
+ export function newestEvent(m, kind) {
152
+ const row = m.newestSupEvent(kind);
153
+ return row ? { at: row.created_at, ...row.payload } : null;
154
+ }
155
+
156
+ /**
157
+ * Where the Supervisor role runs (config.yaml supervisor.mode, default 'chat'). Owner, 2026-09-25: "move the
158
+ * supervisor back into chat so it stays persistent" - the role lives in the owner's desktop chat session again: it owns channel 'main'
159
+ * (registers and drains it with no Orca terminal), runs the 10-minute tick itself and lands its Opus lanes through
160
+ * land.mjs --lane. 'kernel' is the optional [Supervisor] Orca kernel (start-supervisor.mjs + supervisor-watchdog.mjs); in chat
161
+ * mode nothing (resume-all, restart-all, /start, the watchdog) starts one. STARCI_SUPERVISOR_MODE overrides the
162
+ * config (specs, a one-off CLI run).
163
+ */
164
+ const SUPERVISOR_MODES = Object.freeze(['chat', 'kernel']);
165
+ export const DEFAULT_SUPERVISOR_MODE = 'chat';
166
+ /** engine/config.mjs owns the owner-language default; home re-exports it so base (scripts/lib/i18n.mjs) never imports machine. */
167
+ export { DEFAULT_OWNER_LANGUAGE };
168
+ export const DEFAULT_AGENT = 'claude';
169
+ export function supervisorMode({ env = process.env, config = undefined } = {}) {
170
+ const fromEnv = String(env?.STARCI_SUPERVISOR_MODE ?? '').trim();
171
+ if (SUPERVISOR_MODES.includes(fromEnv)) return fromEnv;
172
+ let cfg = config;
173
+ if (cfg === undefined) { try { cfg = loadConfig(); } catch { cfg = null; } }
174
+ const mode = cfg?.supervisor?.mode;
175
+ return SUPERVISOR_MODES.includes(mode) ? mode : DEFAULT_SUPERVISOR_MODE;
176
+ }
177
+
178
+ /**
179
+ * config.yaml supervisor block with defaults: {mode, agent, model, effort, repos, pollIntervalMs, workers, landGate, frozenMinutes}.
180
+ * `agent` and `model` are the owner's pin of the seat (an `only` bias over the supervisor tier chain); both are null when unpinned.
181
+ */
182
+ export function supervisorSettings({ config = undefined } = {}) {
183
+ let cfg = config;
184
+ if (cfg === undefined) cfg = loadConfig();
185
+ const sup = cfg?.supervisor ?? {};
186
+ const seat = sup.kernel ?? {};
187
+ const pick = (...values) => values.find((v) => typeof v === 'string' && v.trim())?.trim() ?? null;
188
+ return {
189
+ mode: SUPERVISOR_MODES.includes(sup.mode) ? sup.mode : DEFAULT_SUPERVISOR_MODE,
190
+ agent: pick(seat.agent),
191
+ model: pick(seat.model),
192
+ effort: pick(seat.effort, cfg?.effort),
193
+ repos: Array.isArray(sup.repos) ? sup.repos : [],
194
+ pollIntervalMs: Number.isInteger(sup.pollIntervalMs) ? sup.pollIntervalMs : DEFAULTS.pollIntervalMs,
195
+ workers: { base: Number.isInteger(sup.workers?.base) ? sup.workers.base : DEFAULTS.workers.base,
196
+ max: Math.min(DEFAULTS.workers.hardMax, Number.isInteger(sup.workers?.max) ? sup.workers.max : DEFAULTS.workers.max) },
197
+ landGate: { mode: sup.landGate?.mode === 'exclusive' ? 'exclusive' : DEFAULTS.landGate.mode },
198
+ // supervisor-watchdog.mjs: a busy seat frame with no turn progress for this long is frozen, not busy.
199
+ frozenMinutes: Number.isInteger(sup.frozenMinutes) && sup.frozenMinutes > 0 ? sup.frozenMinutes : DEFAULTS.frozenMinutes,
200
+ language: typeof cfg?.language === 'string' ? cfg.language : DEFAULT_OWNER_LANGUAGE,
201
+ };
202
+ }
203
+
204
+ /** The product repositories the supervisor watches and pushes: config supervisor.repos resolved against the source root. */
205
+ export const productRepos = (settings = supervisorSettings(), { sourceRoot = path.dirname(SKILL_ROOT) } = {}) =>
206
+ settings.repos.map((repo) => path.resolve(sourceRoot, repo));
207
+
208
+ /** A db-shaped adapter that answers wakeKernel's one signal read with `terminal` (scripts/kernel/wake-delivery.mjs wakeKernel). */
209
+ export const terminalSignalDb = (terminal) => ({
210
+ prepare: () => ({ get: () => ({ value_json: JSON.stringify({ terminal }) }) }),
211
+ });
212
+
213
+ /** One Supervisor log line in machine_logs (actor supervisor, kind supervisor.<name>). Never throws. */
214
+ export function supervisorLog(name, line, { env = process.env, now = new Date(), level = 'info', data = null } = {}) {
215
+ return machineLog({ actor: 'supervisor', kind: `supervisor.${name}`, msg: String(line), level, data, at: now.getTime() }, { env });
216
+ }
@@ -0,0 +1,184 @@
1
+ // host-lock.mjs — the ONE host lock of the heavy runtime work (a land's gate run, a release cut): one holder at a time on this host.
2
+ //
3
+ // The primitive is the atomic mkdir of a single lock directory. Its owner file is JSON (schema starci/host-lock@1) naming
4
+ // who holds it: token, pid, role, purpose, the Orca terminal handle, the host and the start time. That makes three things possible:
5
+ // - a dead holder is detected (its pid is gone) and the lock is taken over atomically instead of blocking everyone;
6
+ // - a release is refused for a stranger (the token must match), so one actor never drops another's lock;
7
+ // - the rights guard (scripts/guards/rights.mjs) reads the owner (hostLockOwner) and decides who may do what meanwhile.
8
+ // A lock directory without a readable owner file (no pid to test) is held, never stale, until it is older than
9
+ // ownerlessStaleMs. The release cut (scripts/supervisor/release-cut.mjs, GOVERNANCE lane) runs under
10
+ // withHostLock({role: 'release', purpose: 'release-cut'}); the land gate under withHostLock({role: 'coordinator', purpose: 'land'}).
11
+ //
12
+ // Every function is synchronous and pure fs; the seams {fs, now, isAlive, pid, host, env, dir, newToken, remove} are injectable.
13
+ import nodeFs from 'node:fs';
14
+ import os from 'node:os';
15
+ import path from 'node:path';
16
+ import { randomBytes } from 'node:crypto';
17
+ import { pidAlive, starciLocalRoot } from '../../engine/db/machine.mjs';
18
+ import { readEnv } from '../lib/env.mjs';
19
+ import { safeRemove } from '../api/fs/safe-remove.mjs';
20
+ import { onInterrupt } from '../lib/interrupt-cleanup.mjs';
21
+
22
+ const HOST_LOCK_SCHEMA = 'starci/host-lock@1';
23
+ export const ROLES = Object.freeze(['release', 'coordinator', 'lead', 'worker']);
24
+ const OWNERLESS_STALE_MS = 6 * 60 * 60 * 1000;
25
+ const OWNER_FILE = 'owner';
26
+ const OWNERLESS_TEXT_CLIP = 500;
27
+
28
+ /** The lock directory: STARCI_HOST_LOCK_DIR, else <starci local root>/host-lock. */
29
+ export function hostLockDir({ env = process.env } = {}) {
30
+ const set = readEnv('STARCI_HOST_LOCK_DIR', env);
31
+ return set ? path.resolve(set) : path.join(starciLocalRoot(env), 'host-lock');
32
+ }
33
+
34
+ // The lock directory as found on disk: {kind: 'free'} | {kind: 'json', owner} | {kind: 'ownerless', text, mtimeMs}.
35
+ // A directory without a readable owner file (a holder between its mkdir and its write, or a crash there) is 'ownerless' with
36
+ // the directory's own age, so it too expires through ownerlessStaleMs.
37
+ function readRaw(dir, fs) {
38
+ let st;
39
+ try { st = fs.statSync(dir); } catch (error) {
40
+ if (error?.code === 'ENOENT') return { kind: 'free' };
41
+ throw error;
42
+ }
43
+ let text = '';
44
+ try { text = String(fs.readFileSync(path.join(dir, OWNER_FILE), 'utf8')); } catch { /* no owner file yet */ }
45
+ try {
46
+ const owner = JSON.parse(text);
47
+ if (owner && typeof owner === 'object' && owner.schema === HOST_LOCK_SCHEMA && typeof owner.token === 'string') return { kind: 'json', owner };
48
+ } catch { /* not an owner document */ }
49
+ return { kind: 'ownerless', text, mtimeMs: st.mtimeMs };
50
+ }
51
+
52
+ // The owner object a raw lock stands for, with `stale` set: a JSON owner is stale when its ttl ran out, or when it is on this
53
+ // host and its pid is gone; an ownerless lock is stale only past ownerlessStaleMs.
54
+ function viewOf(raw, { now, isAlive, host, ownerlessStaleMs }) {
55
+ if (raw.kind === 'free') return null;
56
+ const at = now();
57
+ if (raw.kind === 'ownerless') {
58
+ return { schema: 'ownerless', ownerless: true, text: raw.text.slice(0, OWNERLESS_TEXT_CLIP), token: null, pid: null, role: null, purpose: null, handle: null, host: null,
59
+ since: new Date(raw.mtimeMs).toISOString(), ttlMs: null, stale: at - raw.mtimeMs >= ownerlessStaleMs };
60
+ }
61
+ const owner = raw.owner;
62
+ const since = Date.parse(owner.since);
63
+ const expired = Number.isFinite(owner.ttlMs) && owner.ttlMs > 0 && Number.isFinite(since) && at - since >= owner.ttlMs;
64
+ const dead = owner.host === host && !isAlive(owner.pid);
65
+ return { ...owner, stale: expired || dead };
66
+ }
67
+
68
+ const seamsOf = ({ fs = nodeFs, now = Date.now, isAlive = pidAlive, host = os.hostname(), ownerlessStaleMs = OWNERLESS_STALE_MS } = {}) => ({ fs, now, isAlive, host, ownerlessStaleMs });
69
+ const sameLock = (raw, view) => (raw.kind === 'json' ? view.token === raw.owner.token : raw.kind === 'ownerless' && view.ownerless === true && view.text === raw.text.slice(0, OWNERLESS_TEXT_CLIP));
70
+ const removeTree = (target, remove) => { try { const r = remove(target); return r !== false && r?.ok !== false; } catch { return false; } };
71
+ const defaultRemove = (target) => safeRemove(target, { hold: () => null });
72
+
73
+ // Move the lock directory aside (an atomic rename, so exactly one contender wins), then check that what moved is the lock
74
+ // the caller judged (`expected`, a view). When it is not - another actor re-took the lock between the judgement and the
75
+ // rename - the directory is put back if its place is still free. Returns {moved: true, aside} | {moved: false, gone} where
76
+ // `gone` says the lock was already absent.
77
+ function moveAside(dir, expected, { fs, newToken }) {
78
+ const aside = `${dir}.aside-${newToken().slice(0, 12)}`;
79
+ try { fs.renameSync(dir, aside); } catch (error) {
80
+ if (error?.code === 'ENOENT') return { moved: false, gone: true };
81
+ throw error;
82
+ }
83
+ const raw = readRaw(aside, fs);
84
+ if (!sameLock(raw, expected)) {
85
+ try { fs.renameSync(aside, dir); } catch { /* the place was re-taken: the moved lock stays aside, never deleted */ }
86
+ return { moved: false, gone: false };
87
+ }
88
+ return { moved: true, aside };
89
+ }
90
+
91
+ /**
92
+ * The owner of the lock: null when it is free, else {schema, token, pid, role, purpose, handle, host, since, ttlMs, stale}.
93
+ * `stale` is true for a holder whose process is gone (or whose ttl ran out): callers report it, acquireHostLock takes it over.
94
+ * A lock directory without a readable owner reads {ownerless: true, text, since: <file time>, stale} with null token/pid/role.
95
+ */
96
+ export function hostLockOwner({ dir, env = process.env, ...seams } = {}) {
97
+ const s = seamsOf(seams);
98
+ return viewOf(readRaw(dir ?? hostLockDir({ env }), s.fs), s);
99
+ }
100
+
101
+ // One attempt to create the lock directory: `{ created }`; a lock a live holder keeps is `{ held }`; a vanished lock or a stale one
102
+ // moved aside is `{ retry, seen, tookOver }` (tookOver: the stale owner whose lock was replaced).
103
+ function createLockDir(lockDir, s, { newToken, remove }) {
104
+ try {
105
+ s.fs.mkdirSync(lockDir);
106
+ return { created: true };
107
+ } catch (error) {
108
+ if (error?.code !== 'EEXIST') throw error;
109
+ const seen = viewOf(readRaw(lockDir, s.fs), s);
110
+ if (!seen) return { retry: true, seen: null };
111
+ if (!seen.stale) return { held: seen };
112
+ const moved = moveAside(lockDir, seen, { fs: s.fs, newToken });
113
+ if (moved.moved) removeTree(moved.aside, remove);
114
+ return { retry: true, seen, tookOver: moved.moved ? seen : null };
115
+ }
116
+ }
117
+
118
+ function writeOwnerFile(lockDir, owner, s, remove) {
119
+ try { s.fs.writeFileSync(path.join(lockDir, OWNER_FILE), `${JSON.stringify(owner, null, 2)}\n`, { flag: 'wx' }); } catch (error) {
120
+ try { remove(lockDir); } catch { /* a directory without an owner expires through ownerlessStaleMs */ }
121
+ throw error;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Take the lock. {ok: true, token, owner} (plus tookOverFrom: <the stale owner> when a dead holder's lock was replaced) or
127
+ * {ok: false, reason: 'held', owner} or {ok: false, reason: 'bad-role', role}. A role outside ROLES is refused.
128
+ */
129
+ export function acquireHostLock({ role, purpose = null, handle, pid = process.pid, ttlMs = null, env = process.env, dir, newToken = () => randomBytes(16).toString('hex'), remove = defaultRemove, ...seams } = {}) {
130
+ if (!ROLES.includes(role)) return { ok: false, reason: 'bad-role', role: role ?? null, roles: ROLES };
131
+ const s = seamsOf(seams);
132
+ const lockDir = dir ?? hostLockDir({ env });
133
+ const token = newToken();
134
+ const owner = { schema: HOST_LOCK_SCHEMA, token, pid, role, purpose: purpose ?? null, handle: handle ?? readEnv('ORCA_TERMINAL_HANDLE', env) ?? null,
135
+ host: s.host, since: new Date(s.now()).toISOString(), ttlMs: ttlMs ?? null };
136
+ s.fs.mkdirSync(path.dirname(lockDir), { recursive: true });
137
+ let tookOverFrom = null;
138
+ let seen = null;
139
+ for (let attempt = 0; attempt < 4; attempt += 1) {
140
+ const step = createLockDir(lockDir, s, { newToken, remove });
141
+ if (step.held) return { ok: false, reason: 'held', owner: step.held };
142
+ if (step.retry) { seen = step.seen; tookOverFrom = step.tookOver ?? tookOverFrom; continue; }
143
+ writeOwnerFile(lockDir, owner, s, remove);
144
+ return { ok: true, token, owner, ...(tookOverFrom ? { tookOverFrom } : {}) };
145
+ }
146
+ return { ok: false, reason: 'held', owner: seen };
147
+ }
148
+
149
+ /**
150
+ * Release the lock `token` holds: {ok: true, released: true}; {ok: true, released: false} when there is no lock;
151
+ * {ok: false, reason: 'not-owner', owner} when someone else (or an ownerless owner) holds it - a stranger's release never removes it.
152
+ */
153
+ export function releaseHostLock({ token, env = process.env, dir, newToken = () => randomBytes(16).toString('hex'), remove = defaultRemove, ...seams } = {}) {
154
+ const s = seamsOf(seams);
155
+ const lockDir = dir ?? hostLockDir({ env });
156
+ const raw = readRaw(lockDir, s.fs);
157
+ if (raw.kind === 'free') return { ok: true, released: false };
158
+ const view = viewOf(raw, s);
159
+ if (!token || raw.kind !== 'json' || raw.owner.token !== token) return { ok: false, reason: 'not-owner', owner: view };
160
+ const moved = moveAside(lockDir, view, { fs: s.fs, newToken });
161
+ if (!moved.moved) {
162
+ if (moved.gone) return { ok: true, released: false };
163
+ return { ok: false, reason: 'not-owner', owner: hostLockOwner({ dir: lockDir, ...seams }) };
164
+ }
165
+ const left = !removeTree(moved.aside, remove);
166
+ return { ok: true, released: true, ...(left ? { leftover: moved.aside } : {}) };
167
+ }
168
+
169
+ /**
170
+ * Run `fn({token, owner})` under the lock and always release it. Returns fn's result, or the refusal of acquireHostLock
171
+ * ({ok: false, reason: 'held', owner}) without running fn (`options.onInterrupt` is the registration seam). An async fn holds the lock until its promise settles; a signal or an exit meanwhile releases it too (scripts/lib/interrupt-cleanup.mjs).
172
+ */
173
+ export function withHostLock(options, fn) {
174
+ const got = acquireHostLock(options);
175
+ if (!got.ok) return got;
176
+ const release = () => releaseHostLock({ token: got.token, env: options?.env, dir: options?.dir, fs: options?.fs, now: options?.now, isAlive: options?.isAlive, host: options?.host, remove: options?.remove });
177
+ const dispose = (options?.onInterrupt ?? onInterrupt)(release); // an interrupted holder drops its lock before the process ends
178
+ const done = () => { dispose(); release(); };
179
+ let out;
180
+ try { out = fn(got); } catch (error) { done(); throw error; }
181
+ if (typeof out?.then === 'function') return out.finally(done);
182
+ done();
183
+ return out;
184
+ }