@starci/hfs 4.0.10 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/CHANGELOG.md +5 -1
  2. package/emit/operations.mjs +1 -1
  3. package/emit/type-schema.mjs +2 -1
  4. package/lint/run.mjs +65 -24
  5. package/package.json +1 -1
  6. package/report/order.mjs +2 -0
  7. package/runtime/config.example.yaml +194 -0
  8. package/runtime/engine/by-code-unit.mjs +12 -0
  9. package/runtime/engine/config.mjs +222 -239
  10. package/runtime/engine/invalid-config.mjs +5 -5
  11. package/runtime/engine/model-config.mjs +90 -0
  12. package/runtime/engine/orca-config.mjs +3 -1
  13. package/runtime/engine/release-config.mjs +28 -0
  14. package/runtime/engine/removed-vocabulary.mjs +28 -0
  15. package/runtime/engine/resources-config.mjs +52 -0
  16. package/runtime/engine/runtime-root.mjs +17 -0
  17. package/runtime/engine/secrets.mjs +35 -31
  18. package/runtime/engine/sonar-config.mjs +21 -0
  19. package/runtime/engine/temp-root.mjs +32 -0
  20. package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
  21. package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
  22. package/runtime/knowledge/hfs/rules.yaml +161 -65
  23. package/runtime/knowledge/hfs/slots.yaml +11 -14
  24. package/runtime/knowledge/patterns/be/api.yaml +7 -24
  25. package/runtime/knowledge/patterns/be/cli.yaml +3 -12
  26. package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
  27. package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
  28. package/runtime/modules/kernel/failure-codes.yaml +1 -41
  29. package/runtime/modules/kernel/removed-vocabulary.yaml +268 -0
  30. package/runtime/modules/models/registry.yaml +25 -359
  31. package/runtime/modules/models/runtimes.yaml +126 -265
  32. package/runtime/modules/models/tiers.yaml +66 -0
  33. package/runtime/scripts/api/fs/claim-file.mjs +30 -0
  34. package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
  35. package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
  36. package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
  37. package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
  38. package/runtime/scripts/api/git/lib.mjs +34 -1
  39. package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
  40. package/runtime/scripts/api/process/run-program.mjs +2 -1
  41. package/runtime/scripts/api/sops/lib.mjs +198 -120
  42. package/runtime/scripts/hfs/allows.mjs +3 -3
  43. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  44. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  45. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  46. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  47. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  48. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  49. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  50. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  51. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  52. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  53. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  54. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  55. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  56. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  57. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  58. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  59. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  60. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  61. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  62. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  63. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  64. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  65. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  66. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  67. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  68. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
  69. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  70. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  71. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  72. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  73. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  74. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  75. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  76. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  77. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  78. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  79. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  80. package/runtime/scripts/hfs/architecture/index.mjs +164 -104
  81. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  82. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  83. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  84. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  85. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  86. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  87. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  88. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  89. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  90. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  91. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  92. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  93. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  94. package/runtime/scripts/hfs/architecture/required-files.mjs +121 -70
  95. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  96. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  97. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  98. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  99. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  100. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  101. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  102. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  103. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  104. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  105. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  106. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  107. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  108. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  109. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  110. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  111. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  112. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  113. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  114. package/runtime/scripts/hfs/architecture.mjs +3 -4
  115. package/runtime/scripts/hfs/catalog-once.mjs +15 -0
  116. package/runtime/scripts/hfs/check.mjs +63 -71
  117. package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
  118. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  119. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  120. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  121. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  122. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  123. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  124. package/runtime/scripts/hfs/pin-findings.mjs +31 -0
  125. package/runtime/scripts/hfs/repo-identity.mjs +21 -4
  126. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  127. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  128. package/runtime/scripts/hfs/rules/cli.mjs +5 -1
  129. package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
  130. package/runtime/scripts/hfs/rules/contract.mjs +35 -27
  131. package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
  132. package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
  133. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
  134. package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
  135. package/runtime/scripts/hfs/rules/database.mjs +36 -23
  136. package/runtime/scripts/hfs/rules/deps.mjs +64 -32
  137. package/runtime/scripts/hfs/rules/docker.mjs +113 -65
  138. package/runtime/scripts/hfs/rules/edition.mjs +118 -111
  139. package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
  140. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
  141. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  142. package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
  143. package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
  144. package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
  145. package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
  146. package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
  147. package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
  148. package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
  149. package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
  150. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
  151. package/runtime/scripts/hfs/rules/saga.mjs +128 -61
  152. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  153. package/runtime/scripts/hfs/rules/services.mjs +34 -16
  154. package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
  155. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
  156. package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
  157. package/runtime/scripts/hfs/secret.mjs +59 -48
  158. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  159. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  160. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  161. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  162. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  163. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  164. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  165. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  166. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  167. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  168. package/runtime/scripts/hfs/slots.mjs +74 -695
  169. package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
  170. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  171. package/runtime/scripts/hfs/tree.mjs +19 -14
  172. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  173. package/runtime/scripts/hfs/view.mjs +1 -1
  174. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  175. package/runtime/scripts/lib/env.mjs +10 -0
  176. package/runtime/scripts/lib/event-contract.mjs +17 -13
  177. package/runtime/scripts/lib/git.mjs +2 -2
  178. package/runtime/scripts/lib/graphql-contract.mjs +158 -322
  179. package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
  180. package/runtime/scripts/lib/i18n.mjs +3 -2
  181. package/runtime/scripts/lib/in-order.mjs +71 -0
  182. package/runtime/scripts/lib/language.mjs +3 -3
  183. package/runtime/scripts/lib/list.mjs +8 -1
  184. package/runtime/scripts/lib/path-key.mjs +38 -5
  185. package/runtime/scripts/lib/pid-alive.mjs +7 -0
  186. package/runtime/scripts/lib/regex.mjs +1 -1
  187. package/runtime/scripts/lib/same-text.mjs +1 -1
  188. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  189. package/runtime/scripts/lib/sleep-sync.mjs +2 -2
  190. package/runtime/scripts/lib/sops-envelope.mjs +95 -48
  191. package/runtime/scripts/lib/stack-services.mjs +1 -2
  192. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  193. package/runtime/scripts/lib/walk.mjs +6 -1
  194. package/runtime/scripts/lib/yaml-cached.mjs +14 -0
  195. package/scaffold/add-cli-lite.mjs +2 -1
  196. package/scaffold/add-table.mjs +2 -2
  197. package/scaffold/add.mjs +3 -2
  198. package/scaffold/app.mjs +4 -2
  199. package/scaffold/edition-gate.mjs +25 -25
  200. package/scaffold/lite-exports.mjs +2 -1
  201. package/scaffold/service.mjs +5 -4
  202. package/sync/hygiene.mjs +15 -9
  203. package/sync/index.mjs +7 -3
  204. package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
  205. package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
  206. package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
  207. package/templates/app/ci-workflows/sonar-steps.yml +2 -2
  208. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
  209. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  210. package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
  211. package/templates/app/starciwork.gitignore +1 -1
  212. package/templates/be/image/api/Dockerfile +1 -1
  213. package/templates/be/image/cli/Dockerfile +1 -1
  214. package/templates/be/image/worker/Dockerfile +1 -1
  215. package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
  216. package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
  217. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
  218. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
  219. package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
  220. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
  221. package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
  222. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
  223. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
  224. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
  225. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
  226. package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
  227. package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
  228. package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
  229. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
  230. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  231. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
  232. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
  233. package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
  234. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  235. package/templates/fe/image/next/Dockerfile +2 -1
  236. package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
  237. package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
  238. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
  239. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
  240. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
  241. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
  242. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
  243. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
  244. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
  245. package/upgrade/index.mjs +9 -2
  246. package/runtime/engine/admission.mjs +0 -308
  247. package/runtime/engine/canonical-json.mjs +0 -10
  248. package/runtime/engine/db/blob.mjs +0 -315
  249. package/runtime/engine/db/ledger-paths.mjs +0 -83
  250. package/runtime/engine/db/ledger.mjs +0 -1118
  251. package/runtime/engine/db/machine-connection.mjs +0 -135
  252. package/runtime/engine/db/machine-schema.mjs +0 -84
  253. package/runtime/engine/db/machine.mjs +0 -1367
  254. package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
  255. package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
  256. package/runtime/engine/db/provider-reservations.mjs +0 -101
  257. package/runtime/engine/digest.mjs +0 -16
  258. package/runtime/engine/refuse.mjs +0 -11
  259. package/runtime/modules/ops/_labels.yaml +0 -53
  260. package/runtime/scripts/api/node/lib.mjs +0 -14
  261. package/runtime/scripts/api/node/spawn-node.mjs +0 -6
  262. package/runtime/scripts/api/process/lib.mjs +0 -111
  263. package/runtime/scripts/api/process/owned-process.mjs +0 -78
  264. package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
  265. package/runtime/scripts/connectors/lib.mjs +0 -488
  266. package/runtime/scripts/lib/clip.mjs +0 -19
  267. package/runtime/scripts/lib/display-names.mjs +0 -259
  268. package/runtime/scripts/lib/example-refs.mjs +0 -158
  269. package/runtime/scripts/lib/json-schema.mjs +0 -52
  270. package/runtime/scripts/lib/process-identity.mjs +0 -6
  271. package/runtime/scripts/lib/read-yaml.mjs +0 -18
  272. package/runtime/scripts/lib/redact.mjs +0 -161
  273. package/runtime/scripts/lib/sleep.mjs +0 -7
  274. package/runtime/scripts/lib/source-phrases.mjs +0 -34
  275. package/runtime/scripts/lib/sqlite.mjs +0 -21
@@ -0,0 +1,66 @@
1
+ schema: starci/model-tiers@1
2
+ purpose: >-
3
+ The ONE owner of which model takes which work. A tier is an ordered chain of members (agent + model, optional
4
+ effort); the first member with tokens is taken. Tiers are named by capability. A model may sit in several tiers.
5
+ Adding a tier or a member is a line here (or in config.yaml models.tiers) and needs no code.
6
+ tiers:
7
+ frontier:
8
+ - {agent: claude, model: claude-opus-5-5}
9
+ - {agent: codex, model: gpt-6.1-sol, effort: max}
10
+ high:
11
+ - {agent: claude, model: claude-sonnet-5-5}
12
+ - {agent: codex, model: gpt-6.1-sol, effort: high}
13
+ medium:
14
+ - {agent: devin, model: swe-2-max}
15
+ - {agent: codex, model: gpt-6.1-sol, effort: medium}
16
+ low:
17
+ - {agent: devin, model: swe-2-max}
18
+ - {agent: codex, model: gpt-6-luna, effort: low}
19
+ imagegen:
20
+ - {agent: codex, model: gpt-6.1-sol, effort: high}
21
+ # Highest first: a provider with no frontier member takes its member of the first tier that has one.
22
+ tierOrder: [frontier, high, medium, low]
23
+ # Seat -> tier. A seat is a long-lived agent role; an op takes the tier of its difficulty.
24
+ seats:
25
+ supervisor: frontier
26
+ planner: frontier
27
+ validator: frontier
28
+ kernelManager: frontier
29
+ kernel: high
30
+ worker: high
31
+ critic: frontier
32
+ # The kernel's own model calls (selection.yaml kernelFunctionKinds) take the tier of their seat.
33
+ kindSeats:
34
+ model.assessGoal: planner
35
+ model.planOp: planner
36
+ model.decide: kernelManager
37
+ model.manageWorkflow: kernelManager
38
+ model.validateOp: validator
39
+ model.critiqueGoal: validator
40
+ model.classifyScreen: kernelManager
41
+ model.presentOwnerQuestion: kernelManager
42
+ judge: validator
43
+ difficulty:
44
+ insane: frontier
45
+ hard: high
46
+ medium: medium
47
+ easy: low
48
+ # A kind named here takes that tier whatever its difficulty; the drawing ops take the tier of their difficulty like every core op.
49
+ kindTiers: {}
50
+ # A tier no seat and no op takes: only a headless call made by one op is made on it (`calls`). A tier not named here is a seat tier.
51
+ tierUse:
52
+ imagegen: call
53
+ # Headless calls: the call tier whose chain the call is picked from (the same picker: hard filter, bias, balance, token use),
54
+ # the longest one call may run, and the most images and reference images it takes.
55
+ calls:
56
+ imagegen: {tier: imagegen, timeoutMs: 600000, maxImages: 8, maxReferences: 6}
57
+ # Balance over the recent picks of one tier, running seats included.
58
+ balance:
59
+ maxStreak: 3
60
+ maxSharePercent: 70
61
+ # Token use of the chosen member, in percent of its most used quota window. Automatic picks skip a member at
62
+ # `reservePercent`; an owner bias naming the member keeps it usable up to `biasPercent`; `exhaustedPercent` is never used.
63
+ usage:
64
+ reservePercent: 90
65
+ biasPercent: 95
66
+ exhaustedPercent: 100
@@ -0,0 +1,30 @@
1
+ // claim-file.mjs — an exclusive writer claim on one file: a lock file beside it names the claiming process, so two processes never rewrite the same file at once
2
+ // (the sealed secret writer, scripts/hfs/secret.mjs). The claim of a process that no longer exists is replaced.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { pidAlive } from '../../lib/pid-alive.mjs';
6
+
7
+ const UNREADABLE_HELD_MS = 10_000;
8
+ const holderOf = (lock) => {
9
+ try { const pid = JSON.parse(fs.readFileSync(lock, 'utf8')).pid; return Number.isInteger(pid) ? pid : null; } catch { return null; }
10
+ };
11
+ /** A lock a claimant created but has not yet written is held for a moment: only an old unreadable lock is a crashed claim. */
12
+ const freshlyCreated = (lock) => { try { return Date.now() - fs.statSync(lock).mtimeMs < UNREADABLE_HELD_MS; } catch { return false; } };
13
+
14
+ /** {ok: true, release} once this process holds the claim of `file`; {ok: false, holder} while a live process (this one included) holds it. `pid` is the claiming process. */
15
+ export function claimFile(file, { pid = process.pid } = {}) {
16
+ const lock = path.join(path.dirname(file), `.${path.basename(file)}.lock`);
17
+ fs.mkdirSync(path.dirname(file), { recursive: true });
18
+ for (let attempt = 0; attempt < 2; attempt += 1) {
19
+ try {
20
+ fs.writeFileSync(lock, JSON.stringify({ pid }), { flag: 'wx', mode: 0o600 });
21
+ return { ok: true, release: () => { if (holderOf(lock) === pid) fs.rmSync(lock, { force: true }); } };
22
+ } catch (error) {
23
+ if (error?.code !== 'EEXIST') throw error;
24
+ const holder = holderOf(lock);
25
+ if (holder !== null ? pidAlive(holder) : freshlyCreated(lock)) return { ok: false, holder };
26
+ fs.rmSync(lock, { force: true });
27
+ }
28
+ }
29
+ return { ok: false, holder: null };
30
+ }
@@ -0,0 +1,27 @@
1
+ // ensure-temp-root.mjs — the temp root exists and takes writes before anything is placed in it or a child is started with TEMP/TMP/TMPDIR pointing at it.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { tempRoot, TEMP_ROOT_ENV } from '../../../engine/temp-root.mjs';
5
+
6
+ const TEMP_ROOT_UNUSABLE = 'TEMP_ROOT_UNUSABLE';
7
+ const writable = new Set();
8
+
9
+ const unusable = (root, reason) => Object.assign(new Error(`${TEMP_ROOT_UNUSABLE}: the temp root ${root} ${reason}; set roots.temp in config.yaml (or ${TEMP_ROOT_ENV}) to a directory the runtime can create and write - it is not replaced by the operating-system temp directory`), { code: TEMP_ROOT_UNUSABLE, root });
10
+
11
+ /** Writes and removes one probe file in `root`: a directory that exists can still refuse writes. */
12
+ function probeWritable(root) {
13
+ const probe = path.join(root, `.starci-write-probe-${process.pid}`);
14
+ fs.writeFileSync(probe, '');
15
+ fs.rmSync(probe, { force: true });
16
+ }
17
+
18
+ /** The temp root of `env`, created when it is missing. Throws an Error with code TEMP_ROOT_UNUSABLE when it cannot be created or written. */
19
+ export function ensureTempRoot({ env = process.env } = {}) {
20
+ const root = tempRoot({ env });
21
+ try {
22
+ fs.mkdirSync(root, { recursive: true });
23
+ if (!writable.has(root)) probeWritable(root);
24
+ } catch (error) { throw unusable(root, `cannot be created or written (${error.code ?? error.message})`); }
25
+ writable.add(root);
26
+ return root;
27
+ }
@@ -6,6 +6,7 @@ import path from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { samePath } from '../../lib/path-key.mjs';
8
8
  import { realpathOr } from '../../lib/fs-kind.mjs';
9
+ import { tempRoot } from '../../../engine/temp-root.mjs';
9
10
 
10
11
  const SKILL_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
11
12
 
@@ -37,7 +38,7 @@ function strictlyInsideReal(p, root) {
37
38
  export function forbiddenRoot(p, { checkoutsUnder = null, hold } = {}) {
38
39
  const resolved = path.resolve(p);
39
40
  if (path.parse(resolved).root === resolved || samePath(path.dirname(resolved), resolved)) return 'a filesystem root';
40
- for (const [name, dir] of [['the home directory', os.homedir()], ['the temp directory', os.tmpdir()], ['the runtime', SKILL_ROOT],
41
+ for (const [name, dir] of [['the home directory', os.homedir()], ['the temp directory', os.tmpdir()], ['the configured temp root', tempRoot()], ['the runtime', SKILL_ROOT],
41
42
  ['the repository hosting the runtime', path.dirname(SKILL_ROOT)], ['the repositories root', path.dirname(path.dirname(SKILL_ROOT))]]) {
42
43
  if (dir && samePath(path.resolve(dir), resolved)) return name;
43
44
  }
@@ -0,0 +1,11 @@
1
+ // make-temp-dir.mjs — a fresh temporary directory under the temp root (engine/temp-root.mjs tempRoot).
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { ensureTempRoot } from './ensure-temp-root.mjs';
5
+
6
+ /** A new directory `<prefix><random>` under `parent` (default the temp root, ensureTempRoot: TEMP_ROOT_UNUSABLE when it cannot be made), which is created when it is missing. */
7
+ export function makeTempDir(prefix, { env = process.env, parent = undefined } = {}) {
8
+ const root = parent ?? ensureTempRoot({ env });
9
+ fs.mkdirSync(root, { recursive: true });
10
+ return fs.mkdtempSync(path.join(root, prefix));
11
+ }
@@ -41,7 +41,7 @@ const retrying = (fn, retries) => {
41
41
  };
42
42
  const removeFile = (p, retries) => retrying(() => {
43
43
  try { fs.unlinkSync(p); } catch (error) {
44
- if (WIN && (error?.code === 'EPERM' || error?.code === 'EACCES')) { fs.chmodSync(p, 0o666); fs.unlinkSync(p); } else throw error;
44
+ if (WIN && (error?.code === 'EPERM' || error?.code === 'EACCES')) { fs.chmodSync(p, 0o600); fs.unlinkSync(p); } else throw error;
45
45
  }
46
46
  }, retries);
47
47
 
@@ -65,40 +65,57 @@ export function safeRemove(root, { retries = 5, checkoutsUnder = null, hold } =
65
65
  // the root itself or strictly under the root's real path. A link is unlinked above (the link only); any other
66
66
  // entry that resolves outside the tree is refused, whatever made it look like a plain entry.
67
67
  const rootReal = realpathOr(target);
68
- const insideRoot = (p) => { const real = realpathOr(p); if (!real || !rootReal) return false; if (same(real, rootReal)) return true;
69
- const rel = path.relative(rootReal, real); return Boolean(rel) && !rel.startsWith('..') && !path.isAbsolute(rel); };
70
- const walk = (dir, st, parentReal) => {
71
- if (isLinkLike(dir, { parentReal, stat: st })) {
72
- if (unlinkOnly(dir)) { out.removed.links += 1; return true; }
73
- fail(dir, { code: 'LINK_STUCK', message: 'a link could not be unlinked; nothing above it is removed' });
74
- return false;
75
- }
76
- if (!insideRoot(dir)) {
77
- fail(dir, { code: 'OUTSIDE_ROOT', message: `refusing to delete ${dir}: its real path ${realpathOr(dir) ?? '?'} is outside ${rootReal ?? target}` });
78
- return false;
79
- }
80
- if (!st.isDirectory()) {
81
- const error = removeFile(dir, retries);
82
- if (error) { fail(dir, error); return false; }
83
- out.removed.files += 1; return true;
84
- }
85
- const real = realpathOr(dir);
86
- if (!real) { fail(dir, { code: 'REALPATH', message: 'cannot resolve the directory' }); return false; }
87
- let entries;
88
- try { entries = fs.readdirSync(dir); } catch (error) { fail(dir, error); return false; }
89
- let clear = true;
90
- for (const name of entries) {
91
- const child = path.join(dir, name);
92
- let childStat;
93
- try { childStat = fs.lstatSync(child); } catch (error) { if (error?.code !== 'ENOENT') { fail(child, error); clear = false; } continue; }
94
- if (!walk(child, childStat, real)) clear = false;
95
- }
96
- if (!clear) return false;
97
- const error = retrying(() => fs.rmdirSync(dir), retries);
98
- if (error) { fail(dir, error); return false; }
99
- out.removed.dirs += 1; return true;
100
- };
101
- walk(target, st, null);
68
+ const ctx = { out, fail, retries, rootReal, target };
69
+ walk(ctx, target, st, null);
102
70
  out.ok = !fs.existsSync(target) && (() => { try { fs.lstatSync(target); return false; } catch { return true; } })();
103
71
  return out;
104
72
  }
73
+
74
+ function insideRoot({ rootReal }, p) {
75
+ const real = realpathOr(p);
76
+ if (!real || !rootReal) return false;
77
+ if (same(real, rootReal)) return true;
78
+ const rel = path.relative(rootReal, real);
79
+ return Boolean(rel) && !rel.startsWith('..') && !path.isAbsolute(rel);
80
+ }
81
+
82
+ // Removes each child of `dir` (real path `real`); false when any child stays.
83
+ function removeChildren(ctx, dir, entries, real) {
84
+ let clear = true;
85
+ for (const name of entries) {
86
+ const child = path.join(dir, name);
87
+ let childStat;
88
+ try { childStat = fs.lstatSync(child); } catch (error) { if (error?.code !== 'ENOENT') { ctx.fail(child, error); clear = false; } continue; }
89
+ if (!walk(ctx, child, childStat, real)) clear = false;
90
+ }
91
+ return clear;
92
+ }
93
+
94
+ function walkDirectory(ctx, dir) {
95
+ const { out, fail, retries } = ctx;
96
+ const real = realpathOr(dir);
97
+ if (!real) { fail(dir, { code: 'REALPATH', message: 'cannot resolve the directory' }); return false; }
98
+ let entries;
99
+ try { entries = fs.readdirSync(dir); } catch (error) { fail(dir, error); return false; }
100
+ if (!removeChildren(ctx, dir, entries, real)) return false;
101
+ const error = retrying(() => fs.rmdirSync(dir), retries);
102
+ if (error) { fail(dir, error); return false; }
103
+ out.removed.dirs += 1; return true;
104
+ }
105
+
106
+ function walk(ctx, dir, st, parentReal) {
107
+ const { out, fail, retries, rootReal, target } = ctx;
108
+ if (isLinkLike(dir, { parentReal, stat: st })) {
109
+ if (unlinkOnly(dir)) { out.removed.links += 1; return true; }
110
+ fail(dir, { code: 'LINK_STUCK', message: 'a link could not be unlinked; nothing above it is removed' });
111
+ return false;
112
+ }
113
+ if (!insideRoot(ctx, dir)) {
114
+ fail(dir, { code: 'OUTSIDE_ROOT', message: `refusing to delete ${dir}: its real path ${realpathOr(dir) ?? '?'} is outside ${rootReal ?? target}` });
115
+ return false;
116
+ }
117
+ if (st.isDirectory()) return walkDirectory(ctx, dir);
118
+ const error = removeFile(dir, retries);
119
+ if (error) { fail(dir, error); return false; }
120
+ out.removed.files += 1; return true;
121
+ }
@@ -7,8 +7,12 @@
7
7
  // (diff.mjs, ls-files.mjs, worktree-*.mjs, ...), and a caller folds the spawn result it gets back with the pure
8
8
  // helpers of scripts/lib/git.mjs (gitResultOf, gitOutputOf).
9
9
  import { assertMutationFence } from '../../lib/mutation-fence.mjs';
10
+ import fs from 'node:fs';
11
+ import os from 'node:os';
12
+ import path from 'node:path';
10
13
  import { spawnSync } from 'node:child_process';
11
14
  import { withoutGitLocalEnv } from '../../lib/git.mjs';
15
+ import { withTempEnv } from '../../../engine/temp-root.mjs';
12
16
 
13
17
  /**
14
18
  * Spawn `file` (the git binary) once with `args`: utf8 text, a hidden window, never a shell.
@@ -20,7 +24,8 @@ export const gitSpawn = (file, args, options = {}) => {
20
24
  assertMutationFence({ kind: 'git-effect', args });
21
25
  const spawn = { encoding: 'utf8', windowsHide: true, ...options };
22
26
  if (spawn.encoding === 'buffer') spawn.encoding = null;
23
- return spawnSync(file, args, spawn);
27
+ // tempChildEnv copies the env (node adds NODE_V8_COVERAGE to the env object it is given: a frozen caller env must not throw under coverage) and points TEMP/TMP/TMPDIR at the temp root
28
+ return spawnSync(file, args, withTempEnv(spawn));
24
29
  };
25
30
 
26
31
  /**
@@ -42,3 +47,31 @@ export const gitRunner = (git = null) => (args, opts = {}) => {
42
47
  const r = git ? git(args, opts) : runGit(args, { timeout: 300_000, maxBuffer: 64 * 1024 * 1024, ...opts });
43
48
  return { ok: r?.ok ?? (!r?.error && r?.status === 0), stdout: String(r?.stdout ?? r?.out ?? '').trim(), stderr: String(r?.stderr ?? r?.err ?? r?.error?.message ?? r?.error ?? '').trim() };
44
49
  };
50
+
51
+ let cachedGit;
52
+ const gitPathOf = ({ env, platform, home, exists }) => {
53
+ const p = platform === 'win32' ? path.win32 : path.posix;
54
+ const pathKey = Object.keys(env).find((name) => name.toLowerCase() === 'path');
55
+ const shim = p.join(home, '.starci', 'bin');
56
+ const same = (a, b) => (platform === 'win32' ? p.resolve(a).toLowerCase() === p.resolve(b).toLowerCase() : p.resolve(a) === p.resolve(b));
57
+ const binary = platform === 'win32' ? 'git.exe' : 'git';
58
+ for (const raw of String(pathKey ? env[pathKey] : '').split(platform === 'win32' ? ';' : ':')) {
59
+ const dir = raw.replace(/^"(.*)"$/, '$1');
60
+ const file = dir && p.isAbsolute(dir) && !same(dir, shim) ? p.join(dir, binary) : null;
61
+ if (file && exists(file)) return file;
62
+ }
63
+ return null;
64
+ };
65
+ /**
66
+ * The absolute path of the real git binary, or null: the first PATH directory holding git (git.exe on Windows) that is not
67
+ * the StarCi shim directory <home>/.starci/bin (a seat's guard shim is never the git the runtime itself runs). A bare name is never
68
+ * spawned, so a writable directory cannot be reached by name lookup at spawn time. Resolved once per process; options (env,
69
+ * platform, home, exists) are for a spec and bypass the cache.
70
+ */
71
+ export const gitExecutable = (options = null) => {
72
+ if (!options && cachedGit !== undefined) return cachedGit;
73
+ const { env = process.env, platform = process.platform, home = os.homedir(), exists = (file) => fs.statSync(file, { throwIfNoEntry: false })?.isFile() === true } = options ?? {};
74
+ const found = gitPathOf({ env, platform, home, exists });
75
+ if (!options) cachedGit = found;
76
+ return found;
77
+ };
@@ -22,6 +22,18 @@ const executableNames = (program, { env, platform }) => {
22
22
  return [...new Set(names)];
23
23
  };
24
24
 
25
+ const firstExecutableHit = (entry, names, stat, realpath) => {
26
+ for (const executable of names) {
27
+ const candidate = path.join(entry, executable);
28
+ try {
29
+ if (stat(candidate).isFile()) {
30
+ try { return realpath(candidate); } catch { return path.resolve(candidate); }
31
+ }
32
+ } catch { /* try the next executable spelling or PATH entry */ }
33
+ }
34
+ return null;
35
+ };
36
+
25
37
  /** Resolve `program` from PATH while excluding <home>/.starci/bin by canonical directory identity. */
26
38
  export function resolveRealTool(program, { env = process.env, home = os.homedir(), platform = process.platform,
27
39
  stat = fs.statSync, realpath = fs.realpathSync.native } = {}) {
@@ -33,14 +45,8 @@ export function resolveRealTool(program, { env = process.env, home = os.homedir(
33
45
  for (const rawEntry of pathValue(env).split(delimiter).filter(Boolean)) {
34
46
  const entry = rawEntry.replace(/^"(.*)"$/, '$1');
35
47
  if (canonical(entry, { platform, realpath }) === shimKey) continue;
36
- for (const executable of names) {
37
- const candidate = path.join(entry, executable);
38
- try {
39
- if (stat(candidate).isFile()) {
40
- try { return realpath(candidate); } catch { return path.resolve(candidate); }
41
- }
42
- } catch { /* try the next executable spelling or PATH entry */ }
43
- }
48
+ const hit = firstExecutableHit(entry, names, stat, realpath);
49
+ if (hit) return hit;
44
50
  }
45
51
  return null;
46
52
  }
@@ -2,6 +2,7 @@
2
2
  // assisted run (scripts/uat/assisted-runner.mjs), resolved to its file by scripts/uat/launch.mjs launchFor. Returns the
3
3
  // spawnSync result {status, stdout, stderr, error, signal} (utf8 text, a hidden window).
4
4
  import { spawnSync } from 'node:child_process';
5
+ import { withTempEnv } from '../../../engine/temp-root.mjs';
5
6
 
6
7
  /** Runs `file args`; options (cwd, env, timeout, stdio, input) pass through. */
7
- export const runProgram = (file, args, options = {}) => spawnSync(file, args, { encoding: 'utf8', windowsHide: true, ...options });
8
+ export const runProgram = (file, args, options = {}) => spawnSync(file, args, withTempEnv({ encoding: 'utf8', windowsHide: true, ...options }));