@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,27 @@
1
+ // op-write-scope.mjs — where an Op may write: its attempt's owned paths and the runtime temp directory, nothing else. The owned paths
2
+ // are the absolute paths its job guard records at launch (scripts/guards/hook-install.mjs writeJobGuard); a file the Op needs outside
3
+ // them is reported as owed, never written. The PreToolUse guard applies it to Edit, Write and the shell write targets alike
4
+ // (rights.mjs fileWriteVerdict).
5
+ import os from 'node:os';
6
+ import { tempRoot } from '../../engine/temp-root.mjs';
7
+ import { pathKey, sameOrUnder } from '../lib/path-key.mjs';
8
+
9
+ const inside = (file, dirs) => dirs.some((dir) => sameOrUnder(pathKey(file), pathKey(dir)));
10
+
11
+ /** Directories every Op may write: the runtime temp root and the OS temp directory. */
12
+ const scratchDirs = (env) => [tempRoot({ env }), os.tmpdir(), env?.TEMP, env?.TMP, env?.TMPDIR].filter(Boolean);
13
+
14
+ /**
15
+ * The refusal fields {code, reason, remedy} for an Op write to `file`, or null when it may write there. An Op with no workflow
16
+ * worktree in its guard (a guard without scope information) is not confined.
17
+ */
18
+ export function opWriteScopeRefusal({ file, guard, env = process.env }) {
19
+ if (!guard?.workflowWorktree) return null;
20
+ if (inside(file, guard.owned ?? [])) return null;
21
+ if (!inside(file, [guard.workflowWorktree]) && inside(file, scratchDirs(env))) return null;
22
+ return {
23
+ code: 'RIGHTS_OP_OUTSIDE_OWNED',
24
+ reason: 'an Op writes only inside the paths its attempt owns and the runtime temp directory; this file is outside them',
25
+ remedy: 'put scratch files in the runtime temp directory; a change the work needs outside your owned paths is reported as owed in your report (starci kernel report) for your Kernel to cut',
26
+ };
27
+ }
@@ -0,0 +1,39 @@
1
+ // Own-seat lifecycle admission for R223; the policy table owns the verbs, roles, types and options.
2
+ const optionsOf = (args, entry) => {
3
+ const values = new Set(entry.values ?? []), flags = new Set(entry.flags ?? []);
4
+ const options = new Map();
5
+ for (let i = 0; i < args.length; i += 1) {
6
+ const word = args[i], at = word.indexOf('=');
7
+ const name = at < 0 ? word : word.slice(0, at);
8
+ if (options.has(name)) return null; // Ambiguous identities/types never qualify, even if repeated identically.
9
+ if (flags.has(name) && at < 0) { options.set(name, true); continue; }
10
+ if (!values.has(name)) return null; // No positionals, terminators or unknown identity-changing options.
11
+ const value = at < 0 ? args[++i] : word.slice(at + 1);
12
+ if (value == null || (at < 0 && value.startsWith('--'))) return null;
13
+ options.set(name, value);
14
+ }
15
+ return options;
16
+ };
17
+
18
+ /** True only for a policy-listed lifecycle call whose explicit identity equals the trusted caller handle. */
19
+ export function orcaSelfLifecycleAllowed({ role, args, handle, policy }) {
20
+ const lifecycle = policy.orca?.['self-lifecycle'];
21
+ if (!handle || args[0] !== lifecycle?.group || !lifecycle.roles?.includes(role)) return false;
22
+ const entry = lifecycle.verbs?.[args[1]];
23
+ if (!entry || (entry.roles && !entry.roles.includes(role))) return false;
24
+ const options = optionsOf(args.slice(2), entry);
25
+ if (!options || options.get(entry.identity) !== handle) return false;
26
+ const types = entry.typesByRole?.[role] ?? entry.types;
27
+ return !types || types.includes(options.get('--type'));
28
+ }
29
+
30
+ /**
31
+ * The option a role may not use on a policy-listed lifecycle call ('refuse-for' in the policy entry), or null: an Op that names
32
+ * a recipient (--to) or another Run (--run) addresses someone other than its own Kernel.
33
+ */
34
+ export function orcaAddressedOption({ role, args, policy }) {
35
+ const lifecycle = policy.orca?.['self-lifecycle'];
36
+ if (args[0] !== lifecycle?.group) return null;
37
+ const refused = lifecycle.verbs?.[args[1]]?.['refuse-for']?.[role] ?? [];
38
+ return args.slice(2).map((word) => String(word).split('=', 1)[0]).find((name) => refused.includes(name)) ?? null;
39
+ }
@@ -0,0 +1,106 @@
1
+ // process-kill-verdict.mjs - the command guard's process rules: a kill that selects processes by image name or pattern
2
+ // (PROCESS_KILL_BY_NAME), and a kill whose targets come from a process QUERY - Get-CimInstance Win32_Process, Get-Process,
3
+ // ps | grep, pgrep - which ends other lanes' processes by their command line, parent or path (a lane killed other lanes'
4
+ // node test runs that way, 2026-10-02). An agent ends only a PID it started and recorded: `taskkill /PID <pid>`,
5
+ // `kill <pid>`, `Stop-Process -Id <pid>`, or a variable assigned from its own Start-Process or `cmd & pid=$!`.
6
+ // Reads (Get-CimInstance listing, `ps aux | grep node`) and text that only mentions these commands pass: the query rule
7
+ // judges parsed commands, so an echo, a commit message or a heredoc never reaches it.
8
+ // A kill whose variable was set in an EARLIER shell call (`$x = ...` in one call, `Stop-Process -Id $x` or `kill $pid` in the
9
+ // next) is not judged, by design. The guard sees one call's text and keeps no state: a Bash or PowerShell tool call is a
10
+ // fresh shell, so a variable never survives into the next call - what carries a PID across calls is a file, an environment
11
+ // value or the model's own memory of a literal, none of which the guard can trace to its source. Refusing every non-literal
12
+ // target would refuse `Stop-Process -Id (Get-Content run.pid)` and `kill $(cat run.pid)`, the PID file the remedy itself
13
+ // tells an agent to keep; a query stage is the one evidence of a foreign target, and it is judged only where it is visible,
14
+ // in the same call. Tracking provenance across calls would need a ledger of the PIDs an agent started, which no hook can
15
+ // fill soundly; the by-name rule above and the same-call query rule are the sound part.
16
+ import { assignedCommand } from './assigned-command.mjs';
17
+ const TASKKILL_SELECTOR = /^(?:\/\/?|-)(?:im|fi)$/i;
18
+ const NAME_PARAMETER = /^-(?:n|na|nam|name|processname)(?::.*)?$/i;
19
+ const POWERSHELL_STOP = new Set(['stop-process', 'spps']);
20
+ const REMEDY = 'end only a PID you started yourself, the one you recorded when you started it (taskkill /PID <pid>, kill <pid>, Stop-Process -Id <pid>), never one found by a query; a process you did not start is reported, never killed';
21
+
22
+ /** The refusal for one command that kills by name or pattern, or null. */
23
+ export const nameKillVerdict = (program, args) => {
24
+ let how = null;
25
+ if (program === 'taskkill' && args.some((a) => TASKKILL_SELECTOR.test(a))) how = 'taskkill by image name or filter (/IM, /FI)';
26
+ else if (program === 'pkill' || program === 'killall') how = `${program}, which selects processes by name or pattern`;
27
+ // PowerShell's kill alias takes the full -Name/-ProcessName; a POSIX `kill -n <signal>` is not a name.
28
+ else if ((POWERSHELL_STOP.has(program) && args.some((a) => NAME_PARAMETER.test(a))) || (program === 'kill' && args.some((a) => /^-(?:name|processname)(?::.*)?$/i.test(a)))) how = 'Stop-Process -Name';
29
+ else if (program === 'wmic' && args.some((a) => /^process$/i.test(a)) && args.some((a) => /\bname\s*=|^name$/i.test(a))) how = 'wmic process selected by name';
30
+ // wmic process where <filter> delete | call terminate, unless the filter is one literal ProcessId.
31
+ else if (program === 'wmic' && args.some((a) => /^process$/i.test(a)) && args.some((a) => /\b(?:delete|terminate)\b/i.test(a)) && !args.some((a) => /(?<![a-z])(?:process)?id\s*=\s*\d+\s*$/i.test(a))) how = 'wmic process ... delete|terminate selected by a filter';
32
+ if (!how) return null;
33
+ return { code: 'PROCESS_KILL_BY_NAME', command: [program, ...args].join(' ').slice(0, 200),
34
+ reason: `${how} ends every process it matches on the machine, including Orca and other agents' workers (a lane's taskkill of node.exe restarted Orca, 2026-10-01)`,
35
+ remedy: REMEDY };
36
+ };
37
+
38
+ const QUERY_PROGRAMS = new Set(['get-process', 'gps', 'ps', 'pgrep', 'pidof', 'tasklist']);
39
+ const CIM_PROGRAMS = new Set(['get-ciminstance', 'gcim', 'get-wmiobject', 'gwmi']);
40
+ const isQuery = (c) => QUERY_PROGRAMS.has(c.program) || (CIM_PROGRAMS.has(c.program) && c.args.some((a) => /win32_process/i.test(a))) || (c.program === 'wmic' && c.args.some((a) => /^process$/i.test(a)));
41
+ const LITERAL = /^\d+(?:,\d+)*$/;
42
+ const OPTION_VALUE = /^-(?:ea|erroraction|wa|warningaction|ev|errorvariable|ov|outvariable|s|n|signal)$/i;
43
+ const TARGET_OPTION = /^-(?:id|inputobject)$/i;
44
+
45
+ // The targets one kill command names ([] when it names none: its targets come down a pipe or an xargs).
46
+ function targetsOf(args) {
47
+ const out = [];
48
+ for (let i = 0; i < args.length; i += 1) {
49
+ const a = args[i];
50
+ const inline = /^-(?:id|inputobject):(.*)$/i.exec(a);
51
+ if (inline) out.push(inline[1]);
52
+ else if (TARGET_OPTION.test(a)) { if (i + 1 < args.length) { out.push(args[i + 1]); } i += 1; }
53
+ else if (OPTION_VALUE.test(a)) i += 1;
54
+ else if (!a.startsWith('-')) out.push(a);
55
+ }
56
+ return out;
57
+ }
58
+
59
+ // 'kill' or 'Stop-Process' when its named targets are not all literal PIDs, else null.
60
+ const killTargetHow = (program, targets) => {
61
+ if (targets.length && targets.every((t) => LITERAL.test(t))) return null;
62
+ return program === 'kill' ? 'kill' : 'Stop-Process';
63
+ };
64
+
65
+ // A kill sink whose target is not one literal PID: how it is spelled, or null.
66
+ function sinkOf(c) {
67
+ const { program, args } = c;
68
+ if (POWERSHELL_STOP.has(program) || program === 'kill') {
69
+ if (args.some((a) => /^-l$/.test(a))) return null;
70
+ return killTargetHow(program, targetsOf(args));
71
+ }
72
+ if (program === 'taskkill') {
73
+ const at = args.findIndex((a) => /^(?:\/\/?|-)pid$/i.test(a));
74
+ return at >= 0 && !LITERAL.test(args[at + 1] ?? '') ? 'taskkill /PID' : null;
75
+ }
76
+ if (program === 'invoke-cimmethod' && args.some((a) => /^terminate$/i.test(a))) return 'Invoke-CimMethod Terminate';
77
+ if (/\.(?:terminate|kill)$/.test(program)) return `.${program.split('.').pop()}()`;
78
+ return null;
79
+ }
80
+
81
+ // The variables a kill names in the raw text, once per kill: `Stop-Process -Id $p.Id`, `$p | Stop-Process`, `$p.Kill()`,
82
+ // `kill $pid`, `kill $!`. A kill target that is no plain variable (a $(...), a pipe from a query) names none.
83
+ const ROOTS = [
84
+ new RegExp([
85
+ String.raw`\b(?:stop-process|spps|kill|taskkill)\b`,
86
+ String.raw`(?:\s+(?:-(?:id|inputobject)(?::|(?=\s))|(?:\/\/?|-)pid(?=\s)|-\w+(?=\s+\S*\$)))*`,
87
+ String.raw`\s*"?\$(\w+|!)`,
88
+ ].join(''), 'gi'),
89
+ /\$(\w+)\s*\|\s*(?:stop-process|spps)\b/gi,
90
+ /\$(\w+)\.(?:terminate|kill)\s*\(/gi,
91
+ ];
92
+ const OWN_START = (root) => new RegExp(root === '!' ? '$^' : String.raw`(?:\$${root}\s*=\s*\(?\s*(?:start-process|start-job|\[[\w.]+\]::start)\b|(?<![\w$])${root}=\$!)`, 'i');
93
+
94
+ /** The refusal for a kill whose targets come from a process query, or null. `commands` are the parsed commands, `text` the raw call. */
95
+ export function queryKillVerdict(parsed, text) {
96
+ const commands = parsed.map(assignedCommand);
97
+ if (!commands.some(isQuery)) return null;
98
+ const sinks = commands.map((c) => [c, sinkOf(c)]).filter(([, how]) => how);
99
+ if (!sinks.length) return null;
100
+ const roots = ROOTS.flatMap((re) => [...String(text).matchAll(re)].map((m) => m[1]));
101
+ if (roots.length >= sinks.length && roots.every((r) => r === '!' || OWN_START(r).test(text))) return null;
102
+ const [c, how] = sinks[0];
103
+ return { code: 'PROCESS_KILL_BY_NAME', command: [c.program, ...c.args].join(' ').slice(0, 200),
104
+ reason: `${how} on targets found by a process query (Get-CimInstance Win32_Process, Get-Process, ps, pgrep) ends other lanes' and agents' processes selected by command line, name, path or parent - not a process you started`,
105
+ remedy: REMEDY };
106
+ }
@@ -0,0 +1,56 @@
1
+ // protected-zone.mjs - the reader of modules/kernel/protected-zone.yaml, the ONE declaration of the protected zone
2
+ // (rule R224 RIGHTS_PROTECTED_ZONE), and the question the command guard asks of it: is this path inside a runtime checkout,
3
+ // and does it fall in a zone or on a protected catalog entry?
4
+ //
5
+ // A runtime checkout is found by rights.mjs runtimeRootOf (any directory tree holding knowledge/hfs/runtime-slots.yaml: the live
6
+ // .claude, a lane worktree of it, a staging checkout), so every worktree of the runtime is judged by the same declaration
7
+ // without asking git. Pure fs and one YAML read, loaded only when a role that can be
8
+ // refused writes a file (the guard's common path never loads it).
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { parseYaml } from '../../engine/yaml.mjs';
13
+ import { globExpression } from '../lib/glob.mjs';
14
+ import { pathKey, posixPath } from '../lib/path-key.mjs';
15
+ import { runtimeRootOf } from './rights.mjs';
16
+
17
+ const SKILL_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
18
+ const PROTECTED_ZONE_FILE = 'modules/kernel/protected-zone.yaml';
19
+
20
+ const cache = new Map();
21
+
22
+ /** {zones: [{id, why, matchers}], catalog: [{file, ids, codes}]} parsed from the declaration of `root`, or an empty zone when unreadable. */
23
+ function loadProtectedZone({ root = SKILL_ROOT, read = (file) => fs.readFileSync(file, 'utf8') } = {}) {
24
+ const key = pathKey(root);
25
+ if (cache.has(key)) return cache.get(key);
26
+ let parsed = null;
27
+ try { parsed = parseYaml(read(path.join(root, PROTECTED_ZONE_FILE))); } catch { parsed = null; }
28
+ const zone = {
29
+ zones: (parsed?.zones ?? []).map((z) => ({ id: String(z.id), why: String(z.why ?? ''), matchers: (z.paths ?? []).map((p) => globExpression(String(p))) })),
30
+ catalog: (parsed?.catalogEntries ?? []).map((c) => ({ file: posixPath(String(c.file)), ids: (c.ids ?? []).map(String), codes: (c.codes ?? []).map(String) })),
31
+ };
32
+ cache.set(key, zone);
33
+ return zone;
34
+ }
35
+
36
+ /**
37
+ * Where `file` stands against the zone: {runtimeRoot, rel, zone: {id, why} | null, catalog: entry | null}. runtimeRoot is
38
+ * null when the path is outside every runtime checkout (then nothing here applies).
39
+ */
40
+ export function zoneOfPath(file, { declaration = loadProtectedZone(), exists = fs.existsSync } = {}) {
41
+ const runtimeRoot = runtimeRootOf(file, { exists });
42
+ if (!runtimeRoot) return { runtimeRoot: null, rel: null, zone: null, catalog: null };
43
+ const rel = posixPath(path.relative(runtimeRoot, path.resolve(file)));
44
+ const zone = declaration.zones.find((z) => z.matchers.some((m) => m.test(rel))) ?? null;
45
+ const catalog = declaration.catalog.find((c) => c.file === rel) ?? null;
46
+ return { runtimeRoot, rel, zone: zone ? { id: zone.id, why: zone.why } : null, catalog };
47
+ }
48
+
49
+ /** The ids or codes of a catalog entry that `text` names (a rule id as a whole token, a code as a substring), or []. */
50
+ export function catalogNames(entry, text) {
51
+ const body = String(text ?? '');
52
+ const hits = [];
53
+ for (const id of entry.ids) if (new RegExp(`(?:^|[^A-Za-z0-9])${id}(?![0-9])`).test(body)) hits.push(id);
54
+ for (const code of entry.codes) if (body.includes(code)) hits.push(code);
55
+ return hits;
56
+ }
@@ -0,0 +1,27 @@
1
+ // refusals.mjs — how an op guard refuses: the lines the agent reads and the <guards root>/refusals.jsonl record.
2
+ // Shared by the agent command guard (command-guard.mjs, a PreToolUse hook) and the history hook's commit check
3
+ // (verify-commit.mjs).
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { guardsRoot } from './guards-root.mjs';
8
+
9
+ const skillRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
10
+
11
+ /** The refusal as the agent reads it: what was refused and why, the remedy, and the shared-checkout rule. */
12
+ export function refusalLines(tool, verdict) {
13
+ return [
14
+ `starci guard: refused \`${tool} ${verdict.command ?? ''}\` [${verdict.code}] — ${verdict.reason}.`,
15
+ ...(verdict.remedy ? [`starci guard: instead: ${verdict.remedy}.`] : []),
16
+ 'starci guard: this checkout is shared with other workflows (modules/ops/_common.yaml "Evidence, completion and commits"). Report a real need in your report; never work around this guard.',
17
+ ];
18
+ }
19
+
20
+ /** Append one refusal to <guards root>/refusals.jsonl; the refusal stands without its log line. */
21
+ export function logRefusal(entry, { root = skillRoot } = {}) {
22
+ try {
23
+ const dir = guardsRoot(root);
24
+ fs.mkdirSync(dir, { recursive: true });
25
+ fs.appendFileSync(path.join(dir, 'refusals.jsonl'), `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`);
26
+ } catch { /* the refusal stands without its log line */ }
27
+ }
@@ -0,0 +1,185 @@
1
+ // release-definition.mjs - what makes a commit of the runtime repository a RELEASE commit, the one definition shared by the
2
+ // pre-push hook (scripts/guards/release-push-gate.mjs) and the release cut (scripts/supervisor/release-cut.mjs). The remote
3
+ // main of the runtime moves exactly when a release is cut, so a push of main or of a v* tag is judged against this list:
4
+ // RELEASE_VERSION_UNCHANGED package.json `version` at the pushed commit differs from the version at the remote main's head
5
+ // RELEASE_TAG_VERSION the release tag is v<version> of the pushed commit
6
+ // RELEASE_CHANGELOG_SECTION CHANGELOG.md holds `## [<version>] - <date>` (a dated heading) with no unfinished marker (R222)
7
+ // RELEASE_TAG_MISSING an ANNOTATED tag v<version> points at the pushed commit
8
+ // RELEASE_RECEIPT_MISSING the L4 record of exactly this commit (scripts/guards/release-record.mjs, written by the cut) names this tag
9
+ // RELEASE_RECEIPT_INCOMPLETE that record holds a green row for each of RECEIPT_STEPS (the package clean-test, the scaffold e2e and the checks) (RAN on this commit or reused from another with the identical input digest, the record's `reusedFrom` names it; owner ruling speed-flow, 2026-10-25)
10
+ // RELEASE_SUITE_MODE the record says `suite: ci` (the Linux parity delegated to CI) while the checked-out config says `local`, or it lists no delegated linux-parity: a record never claims the mode by
11
+ // itself (scripts/guards/release-suite-mode.mjs); both modes ask for the same root rows, the runtime has no spec suite to delegate (owner ruling no-runtime-specs)
12
+ // Each finding names what is missing and the command that produces it. Pure over the git reads; no bypass.
13
+ import { catFile } from '../api/git/cat-file.mjs';
14
+ import { revParseQuery } from '../api/git/rev-parse-query.mjs';
15
+ import { show } from '../api/git/show.mjs';
16
+ import { tag as gitTag } from '../api/git/tag.mjs';
17
+ import { mergeBaseQuery } from '../api/git/merge-base-query.mjs';
18
+ import { pathKey, realPath } from '../lib/path-key.mjs';
19
+ import { changelogSection, releaseNotesFindings } from '../hfs/runtime-rules/release-notes.mjs';
20
+ import { readL4Record } from './release-record.mjs';
21
+ import { suiteModeOf } from './release-suite-mode.mjs';
22
+
23
+ /** The rows the L4 record must hold green, run on the commit or reused from an identical-digest row of another, in either suite mode: the package clean-test, the scaffold e2e and the checks (the runtime's real tests). */
24
+ export const RECEIPT_STEPS = Object.freeze(['npm run test:packages', 'npm run test:e2e', 'npm run check']);
25
+ const CUT_COMMAND = (tag) => `starci release cut --tag ${tag}`;
26
+ const REGEX_SPECIALS = /[.*+?^$()|[\]{}\\]/g;
27
+ const HEADING_HEAD = String.raw`^## \[`;
28
+ const DATED_HEADING_TAIL = String.raw`\] — \d{4}-\d{2}-\d{2}\s*$`;
29
+ const DATED_HEADING = (version) => new RegExp([HEADING_HEAD, version.replaceAll(REGEX_SPECIALS, String.raw`\$&`), DATED_HEADING_TAIL].join(''), 'mu');
30
+
31
+ const finding = (code, missing, fix) => ({ code, missing, fix });
32
+ const gitText = (verb, args, cwd) => {
33
+ const r = verb(args, { cwd, timeout: 60_000 });
34
+ return r.status === 0 ? String(r.stdout ?? '') : null;
35
+ };
36
+
37
+ /** The package.json version of `commit`, or null when the commit or its manifest cannot be read. */
38
+ export function versionAt(cwd, commit) {
39
+ const text = gitText(show, [`${commit}:package.json`], cwd);
40
+ try { return text === null ? null : JSON.parse(text).version ?? null; } catch { return null; }
41
+ }
42
+
43
+ const releaseUnknown = (why) => ({ ok: false, status: 'unknown', why });
44
+ const ineligibleRelease = (explicit, why) => explicit ? releaseUnknown(why) : { ok: true, candidate: null };
45
+
46
+ /** A runtime Git root's nearest annotated, manifest-bound release; enclosing application history is inapplicable. */
47
+ export function releaseTagOf({ repo, tag = null, excludeHead = false }) {
48
+ const history = runtimeGitHistory(repo);
49
+ if (!history.ok) return history;
50
+ const listed = tag === null ? gitText(gitTag, ['--merged', 'HEAD', '--list', 'v*'], repo) : tag;
51
+ if (listed === null) return releaseUnknown('release tags are unreadable');
52
+ const candidates = [];
53
+ for (const named of listed.split(/\r?\n/).filter(Boolean)) {
54
+ const result = releaseCandidate({ repo, named, current: history.head, explicit: tag !== null, excludeHead });
55
+ if (!result.ok) return result;
56
+ if (result.candidate) candidates.push(result.candidate);
57
+ }
58
+ return nearestRelease(repo, candidates);
59
+ }
60
+
61
+ /** Runtime history exists only at its own Git top-level with a readable HEAD and full ancestry. */
62
+ function runtimeGitHistory(repo) {
63
+ const top = revParseQuery(['--show-toplevel'], { cwd: repo });
64
+ if (top.status !== 0) {
65
+ if (/not a git repository/i.test(String(top.stderr ?? ''))) return { ok: false, status: 'no-repository', why: 'release history requires a Git repository' };
66
+ return releaseUnknown('release repository identity is unreadable');
67
+ }
68
+ const gitRoot = String(top.stdout ?? '').trim();
69
+ if (!gitRoot) return releaseUnknown('release repository root is unreadable');
70
+ if (pathKey(realPath(gitRoot)) !== pathKey(realPath(repo))) return { ok: false, status: 'no-runtime-repository', why: 'the runtime copy is inside an enclosing Git repository; its history belongs to that repository' };
71
+ const head = gitText(revParseQuery, ['HEAD'], repo)?.trim();
72
+ if (!head) return releaseUnknown('release history HEAD is unreadable');
73
+ if (gitText(revParseQuery, ['--is-shallow-repository'], repo)?.trim() !== 'false') return releaseUnknown('release history is shallow or unreadable; fetch complete release history');
74
+ return { ok: true, head };
75
+ }
76
+
77
+ /** A candidate binds annotation, peeled commit, manifest spelling and ancestry; default discovery omits ineligible tags. */
78
+ function releaseCandidate({ repo, named, current, explicit, excludeHead }) {
79
+ const kind = gitText(catFile, ['-t', `refs/tags/${named}`], repo)?.trim();
80
+ const head = gitText(revParseQuery, [`refs/tags/${named}^{commit}`], repo)?.trim();
81
+ if (!kind || !head) return releaseUnknown(`release tag ${named} is unreadable`);
82
+ if (kind !== 'tag') return ineligibleRelease(explicit, `release tag ${named} is not annotated`);
83
+ const version = versionAt(repo, head);
84
+ if (!version) return releaseUnknown(`release manifest of ${named} is unreadable`);
85
+ if (named !== `v${version}`) return ineligibleRelease(explicit, `release tag ${named} differs from its manifest version ${version}`);
86
+ const reachable = releaseAncestor(repo, head, current);
87
+ if (reachable === null) return releaseUnknown(`release ancestry of ${named} is unreadable`);
88
+ if (!reachable) return ineligibleRelease(explicit, `release tag ${named} is not merged in HEAD`);
89
+ if (excludeHead && head === current) return { ok: true, candidate: null };
90
+ return { ok: true, candidate: { tag: named, head } };
91
+ }
92
+
93
+ /** Only one unsuperseded release ancestor proves a boundary; incomparable releases hold. */
94
+ function nearestRelease(repo, candidates) {
95
+ if (!candidates.length) return releaseUnknown('no eligible annotated, manifest-bound release tag is merged in HEAD');
96
+ const nearest = [];
97
+ for (const candidate of candidates) {
98
+ const superseded = candidateSuperseded(repo, candidate, candidates);
99
+ if (superseded === null) return releaseUnknown('release ancestry between tags is unreadable');
100
+ if (!superseded) nearest.push(candidate);
101
+ }
102
+ if (nearest.length !== 1) return releaseUnknown(`release history is ambiguous: ${nearest.map((entry) => entry.tag).join(', ')}`);
103
+ return { ok: true, ...nearest[0] };
104
+ }
105
+
106
+ /** A release loses its boundary only to a proven descendant release, never to a date or version sort. */
107
+ function candidateSuperseded(repo, candidate, candidates) {
108
+ for (const other of candidates) {
109
+ if (candidate.head === other.head) continue;
110
+ const older = releaseAncestor(repo, candidate.head, other.head);
111
+ if (older === null) return null;
112
+ if (older) return true;
113
+ }
114
+ return false;
115
+ }
116
+
117
+ /** Missing Git proof is distinct from a proven non-ancestor. */
118
+ function releaseAncestor(repo, before, after) {
119
+ const result = mergeBaseQuery(['--is-ancestor', before, after], { cwd: repo });
120
+ if (result.status === 0) return true;
121
+ if (result.status === 1) return false;
122
+ return null;
123
+ }
124
+
125
+ function versionFindings({ cwd, commit, remoteCommit, version }) {
126
+ if (!remoteCommit) return [];
127
+ const remoteVersion = versionAt(cwd, remoteCommit);
128
+ if (remoteVersion === null) return [finding('RELEASE_REMOTE_HEAD_UNKNOWN', `the remote main head ${remoteCommit.slice(0, 9)} is not in this repository, so its version cannot be compared`, 'git fetch origin main')];
129
+ if (remoteVersion === version) return [finding('RELEASE_VERSION_UNCHANGED', `package.json version ${version} is the version the remote main already carries`, `bump package.json version, move the CHANGELOG section to the new version, commit, then ${CUT_COMMAND('v<new version>')}`)];
130
+ return [];
131
+ }
132
+
133
+ function changelogFindings({ cwd, commit, version, tag }) {
134
+ const changelog = gitText(show, [`${commit}:CHANGELOG.md`], cwd);
135
+ if (changelog === null) return [finding('RELEASE_CHANGELOG_SECTION', 'CHANGELOG.md is not readable at the pushed commit', 'restore CHANGELOG.md')];
136
+ const notes = releaseNotesFindings({ tags: [tag], changelog });
137
+ if (notes.length) return notes.map((n) => finding('RELEASE_CHANGELOG_SECTION', n.message, 'finish the CHANGELOG section of the release (no in-preparation mark, no open marker), commit'));
138
+ if (!changelogSection(changelog, version).found || !DATED_HEADING(version).test(changelog)) return [finding('RELEASE_CHANGELOG_SECTION', `CHANGELOG.md has no dated heading "## [${version}] — YYYY-MM-DD"`, `date the heading of the ${version} section, commit`)];
139
+ return [];
140
+ }
141
+
142
+ function tagFindings({ cwd, commit, tag }) {
143
+ const kind = gitText(catFile, ['-t', `refs/tags/${tag}`], cwd)?.trim();
144
+ const target = gitText(revParseQuery, [`refs/tags/${tag}^{commit}`], cwd)?.trim();
145
+ if (kind === 'tag' && target === commit) return [];
146
+ return [finding('RELEASE_TAG_MISSING', `no annotated tag ${tag} points at ${commit.slice(0, 9)}`, CUT_COMMAND(tag))];
147
+ }
148
+
149
+ /** The finding of a record that claims the delegated mode without the checked-out config saying so (or without naming the delegated linux-parity row), else null. */
150
+ function suiteModeFinding({ record, cwd, commit, tag }) {
151
+ if (record.suite !== 'ci') return null;
152
+ if (suiteModeOf(cwd) !== 'ci') return finding('RELEASE_SUITE_MODE', `the release record of ${commit.slice(0, 9)} says suite: ci, but this checkout's config.yaml release.suite is not ci: a record does not choose the mode`, `set release.suite: ci in config.yaml (the owner's choice), or ${CUT_COMMAND(tag)} under suite: local`);
153
+ if (!(record.delegated ?? []).some((row) => row?.name === 'linux-parity')) return finding('RELEASE_SUITE_MODE', `the suite: ci record of ${commit.slice(0, 9)} does not list linux-parity as delegated`, CUT_COMMAND(tag));
154
+ return null;
155
+ }
156
+
157
+ function receiptFindings({ cwd, commit, tag }) {
158
+ const record = readL4Record({ repo: cwd, head: commit, tag });
159
+ if (!record) return [finding('RELEASE_RECEIPT_MISSING', `no release record for exactly ${commit.slice(0, 9)} and ${tag}: the package clean-test, the scaffold e2e and the check did not run green on this commit`, CUT_COMMAND(tag))];
160
+ const mode = suiteModeFinding({ record, cwd, commit, tag });
161
+ if (mode) return [mode];
162
+ // A row reused from another commit (reusedFrom) counts: the cut reuses a row only when its input digest is identical (owner ruling speed-flow, 2026-10-25), and the record names the commit it came from.
163
+ const green = new Set((record.logs ?? []).filter((row) => row?.ok === true).map((row) => row.name));
164
+ const absent = RECEIPT_STEPS.filter((name) => !green.has(name));
165
+ return absent.length ? [finding('RELEASE_RECEIPT_INCOMPLETE', `the release record of ${commit.slice(0, 9)} has no green row for: ${absent.join(', ')}`, CUT_COMMAND(tag))] : [];
166
+ }
167
+
168
+ /**
169
+ * The findings that keep `commit` from being a release commit of the repository at `cwd`: [{code, missing, fix}], [] when it is one.
170
+ * `remoteCommit` is the remote main's head being replaced (null for a tag push or a first push); `tag` the release tag (default v<version>);
171
+ * `needTag` / `needReceipt` are false only for the cut's early look, before it has created the tag and run the suite.
172
+ */
173
+ export function releaseFindings({ cwd, commit, remoteCommit = null, tag = null, needTag = true, needReceipt = true }) {
174
+ const version = versionAt(cwd, commit);
175
+ if (!version) return [finding('RELEASE_VERSION_UNREADABLE', `package.json at ${commit.slice(0, 9)} has no readable version`, 'restore package.json')];
176
+ const expected = `v${version}`;
177
+ const named = tag ?? expected;
178
+ if (named !== expected) return [finding('RELEASE_TAG_VERSION', `the tag ${named} is not v${version}, the version of ${commit.slice(0, 9)}`, `tag v${version}, or set package.json version to ${named.replace(/^v/, '')}`)];
179
+ return [
180
+ ...versionFindings({ cwd, commit, remoteCommit, version }),
181
+ ...changelogFindings({ cwd, commit, version, tag: named }),
182
+ ...(needTag ? tagFindings({ cwd, commit, tag: named }) : []),
183
+ ...(needReceipt ? receiptFindings({ cwd, commit, tag: named }) : []),
184
+ ];
185
+ }
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ // release-push-gate.mjs - the body of the runtime repository's pre-push hook (scripts/guards/git-hooks.mjs). Git hands the hook one line
3
+ // per ref on stdin: <local ref> <local sha> <remote ref> <remote sha>. The remote main and the v* tags of the runtime move only with a
4
+ // release, so those refs are judged against the release definition (scripts/guards/release-definition.mjs), the same code `starci release cut` runs:
5
+ // refs/heads/main not deleted, a fast-forward of the remote main, and the pushed head is a release commit
6
+ // refs/tags/v* annotated, named v<version> of the commit it points at, and that commit is a release commit
7
+ // refs/heads/release/candidate-* allowed, force and delete included: the release candidate ref `starci release cut` pushes the release sha to and waits on
8
+ // (scripts/supervisor/release-candidate.mjs, OWNER RULING candidate-ci 2026-10-18); its CI is the release verdict, so it must stay open
9
+ // any other ref allowed (a lane branch, refs/backup/*, a tag that is not a release tag)
10
+ // A refusal prints `RIGHTS_PUSH_NOT_RELEASE: <ref> is closed: <what is missing> (<command that produces it>)` per ref and exits 1.
11
+ // There is no switch that opens it: a release is made by `starci release cut`, which pushes main and its tag together.
12
+ import { catFile } from '../api/git/cat-file.mjs';
13
+ import { isAncestor } from '../api/git/is-ancestor.mjs';
14
+ import { revParseQuery } from '../api/git/rev-parse-query.mjs';
15
+ import { isMain } from '../lib/is-main.mjs';
16
+ import { invocationDir } from '../lib/roots.mjs';
17
+ import { releaseFindings } from './release-definition.mjs';
18
+ import { hookErrorMessage, readInput, writeHookOutput } from './hook-io.mjs';
19
+
20
+ export const REFUSAL_CODE = 'RIGHTS_PUSH_NOT_RELEASE';
21
+ const ZERO = /^0+$/;
22
+ const RELEASE_TAG_REF = /^refs\/tags\/v\d/;
23
+ /** The release candidate refs (modules/supervisor/release-cut.yaml candidate.refPrefix): named so a later narrowing of "any other ref" never closes them. */
24
+ const CANDIDATE_REF = /^refs\/heads\/release\/candidate-[0-9A-Za-z][0-9A-Za-z.+-]*$/;
25
+
26
+ const parseLines = (text) => String(text ?? '').split(/\r?\n/).map((line) => line.trim().split(/\s+/)).filter((parts) => parts.length === 4)
27
+ .map(([localRef, localSha, remoteRef, remoteSha]) => ({ localRef, localSha, remoteRef, remoteSha }));
28
+ const describe = (findings) => findings.map((f) => `${f.missing} (${f.fix})`).join('; ');
29
+ const refuse = (ref, why) => `${REFUSAL_CODE}: ${ref} is closed: ${why}`;
30
+
31
+ function mainRefusal(cwd, { remoteRef, localSha, remoteSha }) {
32
+ if (ZERO.test(localSha)) return refuse(remoteRef, 'a push never deletes the runtime main');
33
+ const known = !ZERO.test(remoteSha);
34
+ if (known && !isAncestor(cwd, remoteSha, localSha)) return refuse(remoteRef, 'it is not a fast-forward of the remote main (fetch it first; a force push rewrites main)');
35
+ const findings = releaseFindings({ cwd, commit: localSha, remoteCommit: known ? remoteSha : null });
36
+ return findings.length ? refuse(remoteRef, `the pushed head is not a release commit: ${describe(findings)}`) : null;
37
+ }
38
+
39
+ function tagRefusal(cwd, { remoteRef, localSha }) {
40
+ if (ZERO.test(localSha)) return refuse(remoteRef, 'a push never deletes a release tag');
41
+ const kind = catFile(['-t', localSha], { cwd, timeout: 60_000 }).stdout?.trim();
42
+ if (kind !== 'tag') return refuse(remoteRef, 'a release tag is annotated: its message is the release notes');
43
+ const commit = revParseQuery([`${localSha}^{commit}`], { cwd, timeout: 60_000 }).stdout?.trim();
44
+ const findings = releaseFindings({ cwd, commit, tag: remoteRef.slice('refs/tags/'.length) });
45
+ return findings.length ? refuse(remoteRef, `the tagged commit is not a release commit: ${describe(findings)}`) : null;
46
+ }
47
+
48
+ /** The refusal lines for the pushed refs `text` (git's pre-push stdin) of the repository at `cwd`: [] when every ref may move. */
49
+ function pushRefusals({ text, cwd }) {
50
+ return parseLines(text).flatMap((ref) => {
51
+ let line = null;
52
+ if (!CANDIDATE_REF.test(ref.remoteRef)) {
53
+ if (ref.remoteRef === 'refs/heads/main') line = mainRefusal(cwd, ref);
54
+ else if (RELEASE_TAG_REF.test(ref.remoteRef)) line = tagRefusal(cwd, ref);
55
+ }
56
+ return line ? [line] : [];
57
+ });
58
+ }
59
+
60
+ /** Read a bounded Git ref list; unavailable input cannot authorize a release push. */
61
+ export async function main({ stdin = process.stdin, stderr = process.stderr, cwd = invocationDir() } = {}) {
62
+ try {
63
+ const refusals = pushRefusals({ text: await readInput(stdin), cwd });
64
+ if (refusals.length) await writeHookOutput(stderr, `${refusals.join('\n')}\n`);
65
+ return refusals.length ? 1 : 0;
66
+ } catch (error) {
67
+ await writeHookOutput(stderr, `starci guard: push input error (${hookErrorMessage(error)}); refusing the push\n`);
68
+ return 1;
69
+ }
70
+ }
71
+
72
+ if (isMain(import.meta.url)) process.exit(await main());
@@ -0,0 +1,58 @@
1
+ // release-record.mjs - the L4 record of a release: proof that the release cut ran the full suite for ONE commit. The git hooks
2
+ // (the app and runtime pre-push, scripts/guards/git-hooks.mjs) let a push of main or a v* tag through only when HEAD carries
3
+ // an annotated release tag AND this record exists for HEAD and names that tag, so a push that did not come out of the
4
+ // release cut is refused without running a single test.
5
+ //
6
+ // The record lives in the git common dir (never in the tree: it is not tracked, never dirties a checkout and is shared by
7
+ // every worktree of the repository): <git common dir>/starci-release/<head sha>.l4.json
8
+ // {schema: 'starci/l4-record@1', head, tag, suite: 'local'|'ci', delegated: [{name, why}], logs: [{name, ok, log, ms}], at}
9
+ // `suite: ci` (the owner's choice, config.yaml release.suite) lists the rows CI judges as `delegated`, never as logs: a delegated row is not green, it did not run here.
10
+ // The release cut (scripts/supervisor/release-cut.mjs, GOVERNANCE lane) calls writeL4Record after its full suite and check
11
+ // passed and before the atomic push. Pure fs over the common dir path; the git call is injected (seam `commonDir`).
12
+ import path from 'node:path';
13
+ import { revParseQuery } from '../api/git/rev-parse-query.mjs';
14
+ import { RELEASE_DEFAULTS } from '../../engine/release-config.mjs';
15
+ import { proofFileOf, readProofFile, writeProofFile } from '../gates/commit-proof.mjs';
16
+
17
+ const L4_SCHEMA = 'starci/l4-record@1';
18
+ const RELEASE_TAG = /^v\d[\w.+-]*$/;
19
+ const SHA = /^[0-9a-f]{40,64}$/;
20
+
21
+ /** The absolute git common dir of `repo`, or null. */
22
+ export function gitCommonDir(repo, { run = revParseQuery } = {}) {
23
+ const r = run(['--path-format=absolute', '--git-common-dir'], { cwd: repo });
24
+ const out = String(r.stdout ?? '').trim();
25
+ return r.status === 0 && out ? path.resolve(out) : null;
26
+ }
27
+
28
+ /** The record file of `head` under `commonDir`. */
29
+ const l4RecordPath = ({ commonDir, head }) => proofFileOf({ commonDir, sha: head, kind: 'l4' });
30
+
31
+ /**
32
+ * Write the L4 record of `head` for release tag `tag`. {ok, file} or {ok:false, reason}: a head that is not a full sha, a tag
33
+ * that is not v<version>, a failed suite step or an unreadable common dir writes nothing.
34
+ */
35
+ export function writeL4Record({ repo, head, tag, logs = [], suite = RELEASE_DEFAULTS.suite, delegated = [], candidate = null, commonDir = null, now = () => new Date(), run = revParseQuery } = {}) {
36
+ if (!SHA.test(String(head))) return { ok: false, reason: 'the head is not a full sha' };
37
+ if (!RELEASE_TAG.test(String(tag))) return { ok: false, reason: 'the tag is not a release tag v<version>' };
38
+ if (logs.some((step) => step?.ok !== true)) return { ok: false, reason: 'a suite step is not green' };
39
+ if (!['local', 'ci'].includes(suite)) return { ok: false, reason: 'the suite mode is local or ci' };
40
+ if (suite === 'ci' && !delegated.length) return { ok: false, reason: 'a suite: ci record lists the delegated rows' };
41
+ if (suite === 'local' && delegated.length) return { ok: false, reason: 'a suite: local record delegates nothing' };
42
+ const dir = commonDir ?? gitCommonDir(repo, { run });
43
+ if (!dir) return { ok: false, reason: 'the repository has no git common dir' };
44
+ const file = l4RecordPath({ commonDir: dir, head });
45
+ // candidate: the candidate CI verdict of this sha (scripts/supervisor/release-candidate.mjs), with the owner's override when the emergency path was used.
46
+ writeProofFile(file, { schema: L4_SCHEMA, head, tag, suite, delegated, logs, ...(candidate ? { candidate } : {}), at: now().toISOString() });
47
+ return { ok: true, file };
48
+ }
49
+
50
+ /** The L4 record of `head`, or null when there is none or it names another head, tag or schema. */
51
+ export function readL4Record({ repo, head, tag = null, commonDir = null, run = revParseQuery } = {}) {
52
+ const dir = commonDir ?? gitCommonDir(repo, { run });
53
+ if (!dir) return null;
54
+ try {
55
+ const record = readProofFile(l4RecordPath({ commonDir: dir, head }));
56
+ return record?.schema === L4_SCHEMA && record.head === head && (tag === null || record.tag === tag) ? record : null;
57
+ } catch { return null; }
58
+ }
@@ -0,0 +1,15 @@
1
+ // release-suite-mode.mjs - where the full suite of a release is judged, read from the checked-out owner config (config.yaml `release.suite`, engine/release-config.mjs):
2
+ // local the release cut runs the root rows and the Linux parity container (the shipped default)
3
+ // ci the owner's recorded choice (modules/kernel/owner-rulings.yaml release-suite-ci): the GitHub workflow runs the Linux parity after the push; the root rows still run locally
4
+ // The mode is read from the checkout the cut or the hook runs in. A release record names the mode it was cut under, but a record never decides it: the gate accepts a record that says `ci` only
5
+ // while this checkout's config says `ci` too. An owner file that is absent, unreadable or invalid reads as `local` (fail closed: the stricter mode).
6
+ import { inspectOwnerConfig } from '../../engine/config.mjs';
7
+ import { releaseSuiteMode } from '../../engine/release-config.mjs';
8
+
9
+ export { SUITE_MODES } from '../../engine/release-config.mjs';
10
+
11
+ /** The suite mode of the owner config under `root`: 'local' or 'ci'. */
12
+ export function suiteModeOf(root) {
13
+ const { config, invalid, error } = inspectOwnerConfig(root);
14
+ return invalid || error ? releaseSuiteMode(null) : releaseSuiteMode(config);
15
+ }