@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
package/CHANGELOG.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 4.2.0 - 2026-10-09
4
+
5
+ - Changed: the lint runner (`lint/run.mjs`) and the bundled runtime copies follow the runtime of 1.0.0-alpha.9. Version 4.1.0 on the registry carries the copies of an earlier runtime.
6
+
7
+ ## 4.1.0 - 2026-10-08
4
8
 
5
9
  - Breaking (runtime alpha.4 CLI unification): remove the `hfs` bin and expose `main(argv, io)` for the sole `starci app ...` grammar; the app flags `--repo` and `--root` are replaced by the global `--cwd` flag.
6
10
  - Changed (contract change `coverage-modules-logic`): the coverage scope is derived once from the slot manifest (`coverage: required|none` on every tracked be slot, `ruleParams.be.logicRoles`; `scripts/hfs/coverage-scope.mjs`). `hfs sync` renders it into `be/jest.config.js` (`starciJestConfig({ coverage })`), `sonar.coverage.exclusions` and `codecov.yml` (the measured roots, plus one Codecov component per service app and one `platform`, each at 100); `coverageScope`, `coverageExclusions` and `HFS_SYNC_COVERAGE_SCOPE` are gone. New R204 `HFS_COVERAGE_SCOPE_DRIFT`: a hand edit of any of the three coverage statements is drift. `hfs scaffold app` renders with the source it just wrote.
@@ -70,7 +70,7 @@ export function readOperations({ ts, program, file }) {
70
70
  const output = guard('output', member('output'));
71
71
  const refusalType = member('refusal');
72
72
  let refusal = [];
73
- if (!(refusalType.flags & ts.TypeFlags.Never)) {
73
+ if ((refusalType.flags & ts.TypeFlags.Never) === 0) {
74
74
  const members = refusalType.isUnion() ? refusalType.types : [refusalType];
75
75
  if (!members.every((each) => each.isStringLiteral())) fail(`refusal must be a closed union of string literals, found ${checker.typeToString(refusalType)}`);
76
76
  refusal = members.map((each) => each.value).sort();
@@ -7,11 +7,12 @@
7
7
  * object type with no members and no index signature, and a union with `undefined` where a member could not simply be absent.
8
8
  * Pure: takes the TypeScript module and a checker.
9
9
  */
10
+ import { byCodeUnit } from '../report/order.mjs';
10
11
 
11
12
  /** Sorts object keys at every depth, so equal schemas print equal text. */
12
13
  export function stable(value) {
13
14
  if (Array.isArray(value)) return value.map(stable);
14
- if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])]));
15
+ if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort(byCodeUnit).map((key) => [key, stable(value[key])]));
15
16
  return value;
16
17
  }
17
18
 
package/lint/run.mjs CHANGED
@@ -18,11 +18,12 @@
18
18
  import fs from 'node:fs';
19
19
  import path from 'node:path';
20
20
  import { createRequire } from 'node:module';
21
- import { spawnSync } from 'node:child_process';
21
+ import { spawn } from 'node:child_process';
22
22
  import { fileURLToPath, pathToFileURL } from 'node:url';
23
23
  import { linterReport, mergeReports, sonarReport, sourceRootsOf } from '../report/sonar.mjs';
24
24
  import { SIDES, appRelativeMessages, loadSlotManifest, readRepoDeclaration } from '../runtime/scripts/hfs/slots.mjs';
25
25
  import { STYLE_GLOB } from '../sync/index.mjs';
26
+ import { byCodeUnit } from '../report/order.mjs';
26
27
  import { braceVariants, globExpression } from '../runtime/scripts/lib/glob.mjs';
27
28
 
28
29
  export const LINT_SCHEMA = 'starci/lint@1';
@@ -65,8 +66,22 @@ export function parseLintArgs(argv) {
65
66
  export const BOUND_ENV = 'STARCI_LINT_BOUND';
66
67
  export const linterBoundArgs = () => ['--require', fileURLToPath(new URL('./bound-sys.cjs', import.meta.url))];
67
68
 
68
- /** A linter's json results through the app's own install, run from `cwd` (a side folder): `{ results }` or `{ error }`. */
69
- function runLinter({ cwd, pkg, bin, args, bound = null }) {
69
+ /** The child `node` ran with `args` from `cwd`: its exit status, signal, spawn error and the text of both streams, once it closes. */
70
+ function runNode(args, options) {
71
+ return new Promise((resolve) => {
72
+ const child = spawn(process.execPath, args, { ...options, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
73
+ const out = [];
74
+ const err = [];
75
+ let failure;
76
+ child.stdout.on('data', (chunk) => out.push(chunk));
77
+ child.stderr.on('data', (chunk) => err.push(chunk));
78
+ child.on('error', (error) => { failure = error; });
79
+ child.on('close', (status, signal) => resolve({ status, signal, error: failure, stdout: Buffer.concat(out).toString('utf8'), stderr: Buffer.concat(err).toString('utf8') }));
80
+ });
81
+ }
82
+
83
+ /** A linter's json results through the app's own install, run from `cwd` (a side folder): `{ results }` or `{ error }`. The child runs while the caller goes on. */
84
+ async function runLinter({ cwd, pkg, bin, args, bound = null }) {
70
85
  let entry;
71
86
  try {
72
87
  const manifest = createRequire(path.join(cwd, 'package.json')).resolve(`${pkg}/package.json`);
@@ -75,7 +90,7 @@ function runLinter({ cwd, pkg, bin, args, bound = null }) {
75
90
  } catch {
76
91
  return { error: `${pkg} is not installed for ${cwd}` };
77
92
  }
78
- const run = spawnSync(process.execPath, [...(bound ? linterBoundArgs() : []), entry, ...args], { cwd, encoding: 'utf8', maxBuffer: 512 * 1024 * 1024, ...(bound ? { env: { ...process.env, [BOUND_ENV]: bound } } : {}) });
93
+ const run = await runNode([...(bound ? linterBoundArgs() : []), entry, ...args], { cwd, ...(bound ? { env: { ...process.env, [BOUND_ENV]: bound } } : {}) });
79
94
  // ESLint prints its report on stdout; stylelint uses stderr. JSON does not prove a completed child: only exits 0/1
80
95
  // without a spawn error or signal are measurable, and exit 1 must carry findings.
81
96
  let results;
@@ -137,7 +152,7 @@ async function sourceFiles({ repoRoot, cwd, side, scope }) {
137
152
  }
138
153
  };
139
154
  visit(scope ?? '');
140
- return files.sort();
155
+ return files.sort(byCodeUnit);
141
156
  }
142
157
 
143
158
  /** Every requested source needs an explicit, well-formed result; suppressed findings and foreign paths cannot witness a completed lint. */
@@ -164,6 +179,29 @@ function checkedEslintResults({ results, expected, cwd, scope, changed }) {
164
179
  return { results: valid, errors };
165
180
  }
166
181
 
182
+ /** Starts the ESLint child of every side that has work and returns one record per side: skipped, a scope error, or the running child. */
183
+ async function launchEslint({ app, repoRoot, opts, changed, existing, workspace, onFe }) {
184
+ const launches = [];
185
+ for (const side of app ? SIDES : []) {
186
+ if (workspace !== null && side !== STYLE_SIDE) { launches.push({ side, skipped: `outside --workspace ${workspace}` }); continue; }
187
+ const sources = onSide(existing, side).filter((file) => /\.(?:[cm]?[jt]sx?)$/.test(file));
188
+ if (changed !== null && !sources.length) { launches.push({ side, skipped: 'no changed source file' }); continue; }
189
+ const cwd = path.join(repoRoot, side);
190
+ let expected;
191
+ try { expected = changed ? sources : await sourceFiles({ repoRoot, cwd, side, scope: onFe }); }
192
+ catch (error) { launches.push({ side, scopeError: `eslint source scope is unavailable in ${side}/: ${String(error?.message ?? error)}` }); continue; }
193
+ launches.push({ side, cwd, expected, run: runLinter({ cwd, pkg: 'eslint', bin: 'eslint', args: ['--format', 'json', ...(opts.fix ? ['--fix'] : []), ...(changed ? sources : [onFe ?? '.'])], bound: repoRoot }) });
194
+ }
195
+ return launches;
196
+ }
197
+
198
+ /** Starts stylelint over the fe stylesheets (the running child), or returns null when no stylesheet is in scope. */
199
+ function launchStylelint({ repoRoot, opts, changed, existing, onFe }) {
200
+ const styles = onSide(existing, STYLE_SIDE).filter((file) => file.endsWith('.css'));
201
+ if (changed !== null && !styles.length) return null;
202
+ return runLinter({ cwd: path.join(repoRoot, STYLE_SIDE), pkg: 'stylelint', bin: 'stylelint', args: [...(changed ? styles : [onFe ? `${onFe}/src/**/*.css` : STYLE_GLOB]), '--formatter', 'json', ...(opts.fix ? ['--fix'] : [])] });
203
+ }
204
+
167
205
  /**
168
206
  * Run the whole lint of the app at `repoRoot`. `hfsCheck(repoRoot)` returns the `starci app check` result (`{ findings, tracked }`); the CLI
169
207
  * injects it. Returns `{ report, sonar, exit }`; `sonar` is the merged Generic Issue Import document.
@@ -190,22 +228,24 @@ export async function lintRepository({ repoRoot, opts, hfsCheck, trackedFiles =
190
228
  /** The workspace folder relative to the fe side folder (apps/<app> or packages/<pkg>). */
191
229
  const onFe = workspace === null ? null : workspace.slice(STYLE_SIDE.length + 1);
192
230
 
231
+ // The linters and the app check are independent: every child starts first, the app check runs while they work, and the results are read in a fixed order.
232
+ const eslintLaunches = await launchEslint({ app, repoRoot, opts, changed, existing, workspace, onFe });
233
+ const styleLaunch = app ? launchStylelint({ repoRoot, opts, changed, existing, onFe }) : null;
234
+ const checkRun = app ? Promise.resolve().then(() => hfsCheck(repoRoot)).then((value) => ({ value }), (error) => ({ error })) : null;
235
+
193
236
  // 1. ESLint, once per side, from the side folder with that side's config.
194
237
  engines.eslint = { sides: {} };
195
- for (const side of app ? SIDES : []) {
196
- if (workspace !== null && side !== STYLE_SIDE) { engines.eslint.sides[side] = { files: 0, skipped: `outside --workspace ${workspace}` }; continue; }
197
- const sources = onSide(existing, side).filter((file) => /\.(?:[cm]?[jt]sx?)$/.test(file));
198
- if (changed !== null && !sources.length) { engines.eslint.sides[side] = { files: 0, skipped: 'no changed source file' }; continue; }
199
- const cwd = path.join(repoRoot, side);
200
- let expected;
201
- try { expected = changed ? sources : await sourceFiles({ repoRoot, cwd, side, scope: onFe }); }
202
- catch (error) { errors.push(`eslint source scope is unavailable in ${side}/: ${String(error?.message ?? error)}`); engines.eslint.sides[side] = { files: 0 }; continue; }
203
- const linted = runLinter({ cwd, pkg: 'eslint', bin: 'eslint', args: ['--format', 'json', ...(opts.fix ? ['--fix'] : []), ...(changed ? sources : [onFe ?? '.'])], bound: repoRoot });
238
+ const eslintRuns = await Promise.all(eslintLaunches.map((launch) => launch.run ?? null));
239
+ eslintLaunches.forEach((launch, at) => {
240
+ const { side } = launch;
241
+ if (launch.skipped) { engines.eslint.sides[side] = { files: 0, skipped: launch.skipped }; return; }
242
+ if (launch.scopeError) { errors.push(launch.scopeError); engines.eslint.sides[side] = { files: 0 }; return; }
243
+ const linted = eslintRuns[at];
204
244
  if (linted.error) errors.push(linted.error);
205
245
  if (linted.results) {
206
- const checked = checkedEslintResults({ results: linted.results, expected, cwd, scope: onFe, changed: changed !== null });
207
- errors.push(...checked.errors);
208
- linted.results = checked.results;
246
+ const checkedResults = checkedEslintResults({ results: linted.results, expected: launch.expected, cwd: launch.cwd, scope: onFe, changed: changed !== null });
247
+ errors.push(...checkedResults.errors);
248
+ linted.results = checkedResults.results;
209
249
  // The side canon names side-relative paths in its messages; the report names every path from the app root.
210
250
  const appRelative = appRelativeMessages(side, path.join(repoRoot, side));
211
251
  for (const result of linted.results) for (const message of result.messages ?? []) message.message = appRelative(message.message);
@@ -213,13 +253,12 @@ export async function lintRepository({ repoRoot, opts, hfsCheck, trackedFiles =
213
253
  findings.push(...eslintFindings(linted.results, repoRoot));
214
254
  }
215
255
  engines.eslint.sides[side] = { files: linted.results?.length ?? 0 };
216
- }
256
+ });
217
257
 
218
258
  // 3. stylelint over the fe side's stylesheets.
219
259
  if (app) {
220
- const styles = onSide(existing, STYLE_SIDE).filter((file) => file.endsWith('.css'));
221
- if (changed === null || styles.length) {
222
- const linted = runLinter({ cwd: path.join(repoRoot, STYLE_SIDE), pkg: 'stylelint', bin: 'stylelint', args: [...(changed ? styles : [onFe ? `${onFe}/src/**/*.css` : STYLE_GLOB]), '--formatter', 'json', ...(opts.fix ? ['--fix'] : [])] });
260
+ if (styleLaunch) {
261
+ const linted = await styleLaunch;
223
262
  if (linted.error) errors.push(linted.error);
224
263
  if (linted.results) {
225
264
  const appRelative = appRelativeMessages(STYLE_SIDE, path.join(repoRoot, STYLE_SIDE));
@@ -233,8 +272,10 @@ export async function lintRepository({ repoRoot, opts, hfsCheck, trackedFiles =
233
272
 
234
273
  // 2. The app check: root and sides.
235
274
  let checked = { findings: [] };
236
- if (app) {
237
- try { checked = await hfsCheck(repoRoot); } catch (error) { errors.push(`starci app check could not run: ${String(error?.message ?? error)}`); }
275
+ if (checkRun) {
276
+ const settled = await checkRun;
277
+ if (settled.error) errors.push(`starci app check could not run: ${String(settled.error?.message ?? settled.error)}`);
278
+ else checked = settled.value;
238
279
  }
239
280
  const repoErrors = checked.findings.filter((finding) => finding.level === 'error');
240
281
  // A workspace lint keeps only the app findings inside the workspace; the rest belong to the root lint of the whole app.
@@ -256,7 +297,7 @@ export async function lintRepository({ repoRoot, opts, hfsCheck, trackedFiles =
256
297
  linterReport('eslint', raw.eslint, { root: repoRoot, sourceRoots, tracked }),
257
298
  linterReport('stylelint', raw.stylelint, { root: repoRoot, sourceRoots, tracked }),
258
299
  ]);
259
- const report = { schema: LINT_SCHEMA, ok: findings.length === 0 && errors.length === 0, repoRoot, changed: changed ? [...changed].sort() : null, workspace, counts: { error: findings.length }, engines, errors, findings };
300
+ const report = { schema: LINT_SCHEMA, ok: findings.length === 0 && errors.length === 0, repoRoot, changed: changed ? [...changed].sort(byCodeUnit) : null, workspace, counts: { error: findings.length }, engines, errors, findings };
260
301
  return { report, sonar, exit: errors.length ? 2 : findings.length ? 1 : 0 };
261
302
  }
262
303
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/hfs",
3
- "version": "4.0.10",
3
+ "version": "4.2.0",
4
4
  "description": "The StarCi app command implementation for scaffolding, linting, checking and synchronizing one product repository.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,2 @@
1
+ // order.mjs - the one code-unit comparator of @starci/hfs: the order Array.prototype.sort gives without a compare function (UTF-16 code units), named so the digests, emitted files and reports that depend on it stay byte-identical.
2
+ export const byCodeUnit = (a, b) => { const l = String(a), r = String(b); return l < r ? -1 : l > r ? 1 : 0; };
@@ -0,0 +1,194 @@
1
+ # config.example.yaml — seed for the per-project owner config.
2
+ # The installer copies this file to the gitignored `config.yaml` next to it;
3
+ # owners edit that copy. Everything below is read at runtime — no key here is
4
+ # decorative. Kernel route precedence: explicit `--agent` flag >
5
+ # `kernel` below > the tier chain (modules/models/tiers.yaml).
6
+ language: vi
7
+ model: null
8
+ effort: medium
9
+ # Explicit current owner adoption is required; shipped defaults grant no trust or purge authority.
10
+ # launchTrust: {profile: automatic, approvedBy: owner, approvalRef: '<current approval>', roots: ['<exact absolute repository root>']}
11
+ # profile: declined preserves an explicit decision; provider-side declines also remain authoritative.
12
+ launchTrust: null
13
+ # retention: {workflowPurge: {approvedBy: owner, approvalRef: '<current approval>', repos: ['<exact absolute ledger-owner repo>']}}
14
+ retention: null
15
+ # kernel — the long-lived [Kernel] seat. It takes the `high` tier of modules/models/tiers.yaml (an ordered chain of
16
+ # members; the picker takes the first member with tokens after your bias and the balance step; docs/config-format.md).
17
+ # {agent?, model?, effort?}: agent and/or model PIN the seat — the bias `only`, keeping the tier's model of that agent
18
+ # (an explicit `--agent` flag is the same bias and wins). effort: the seat's effort; null takes the tier member's effort.
19
+ kernel: {effort: high}
20
+ # parallel — the one knob for how wide the workers run. `gear` indexes the agent
21
+ # table in modules/models/runtimes.yaml allocation.slicing.size: a long (`l`)
22
+ # operation asks for the `l.agents[gear]` agents, an extra-long (`xl`) one for
23
+ # `xl.agents[gear]`; `s` and `m` operations are always one agent. Declared gears
24
+ # are allocation.slicing.gears — an undeclared value fails closed. Absent means
25
+ # the first declared gear. A gear raises only what `starci kernel estimate` requests; it
26
+ # never raises a pool's maxParallel, maxParallelOps or budgets.maxOps below.
27
+ parallel: {gear: 1}
28
+ # supervisor — the optional chat digest cadence. pollIntervalMs controls
29
+ # scripts/supervisor/poll.mjs when run continuously (its floor is the config
30
+ # validation of engine/config.mjs); null uses scripts/machine/home.mjs
31
+ # DEFAULTS, and --interval-ms overrides this key.
32
+ # The reconciler engine owns host runtime cadence and restores seats after reboot.
33
+ # repos: managed product ledger-owner repositories for the reconciler and Supervisor tools,
34
+ # relative to the source root (the directory holding this skill) or absolute.
35
+ # --repo on the poll command line can add another. stallMinutes (optional,
36
+ # integer >= 5) marks a running workflow STALLED in scripts/supervisor/stall.mjs;
37
+ # the Workflow controller turns actionable findings into Decision Items. The [Supervisor]
38
+ # kernel (docs/supervisor.md) also takes, optional, defaults in home.mjs DEFAULTS:
39
+ # kernel: {agent, model, effort} its agent pin (default: the kernel pin below)
40
+ # workers: {base, max} the adaptive [Worker] cap, 0 to 10 (0 = the Supervisor runs no fix worker)
41
+ # landGate: {mode: shared} shared lets running lanes commit directly; exclusive = gate only. A land
42
+ # fast-forwards LOCAL main only: the remote main moves with a release (starci release cut)
43
+ supervisor:
44
+ # mode: chat (default) - the owner's desktop chat session is the Supervisor: it owns channel 'main'
45
+ # and reads Decision Items; nothing starts a [Supervisor] kernel. kernel - the optional [Supervisor]
46
+ # Orca kernel (scripts/supervisor/start-supervisor.mjs), kept alive by the reconciler Host controller.
47
+ # See docs/supervisor.md.
48
+ # mode: chat
49
+ pollIntervalMs: null
50
+ repos: []
51
+ # frozenMinutes: <n> # Host seat check: a busy frame unchanged this long is frozen, not busy
52
+ # landGate: {mode: shared|exclusive}
53
+ # delegation — while the owner is away, a named delegate (e.g. the supervisor chat) may
54
+ # answer owner asks until the given time; excluded classes stay owner-only. null = none.
55
+ # delegation: {asks: supervisor, until: 2026-09-23T09:00:00+07:00,
56
+ # excludes: [credentials, oauth-consent, payments, legal, payment-provider]}
57
+ delegation: null
58
+ # budgets — the owner's ceiling, surfaced by every plan before `ok`. Null = unbounded.
59
+ budgets:
60
+ maxOps: null # max concurrent dispatched ops one workflow may hold open;
61
+ # route/dispatch refuse `max-ops` at min(this, maxParallelOps)
62
+ # allocation — the owner's default grant for a pool whose capacity is an explicit grant (Devin).
63
+ # grants: '<pool>=<slots>@<role>+<role>' — the default grant every workflow gets. Once a grants list is
64
+ # declared it is the whole set: a pool whose registry.yaml capacityAuthority is explicit-workflow-quota
65
+ # (Devin) routes only for the roles and up to the running slots a grant names ([] closes it).
66
+ allocation:
67
+ grants: [devin-agent=10@implement+verify+write]
68
+ # models — optional overrides of the shipped model tiers (modules/models/tiers.yaml owns the defaults). Every key is optional:
69
+ # tiers: {<tier>: [{agent, model, effort?}, ...]} replaces or adds one tier's ordered member chain
70
+ # seats: {<seat>: <tier>} remaps a seat (supervisor, kernel, worker, planner, validator, kernelManager)
71
+ # balance: {maxStreak, maxSharePercent} a head member picked maxStreak times in a row, or holding more than
72
+ # maxSharePercent of the tier's running seats, yields to the next member
73
+ # usage: {reservePercent, biasPercent} a member at reservePercent of its tokens is skipped; an owner bias
74
+ # naming it keeps it usable up to biasPercent; never above it
75
+ models: {}
76
+ # connectors — the owner's public ask channel (docs/connectors.md). Everything is off by default.
77
+ # One local gateway (scripts/connectors/ask-gateway.mjs) fronts every serve-ask form; a Cloudflare
78
+ # tunnel (scripts/connectors/tunnel.mjs) publishes the gateway; when serve-ask binds a form, the
79
+ # kernel's ask path sends the owner one Telegram message (scripts/connectors/telegram.mjs) with the
80
+ # question, its options and the public link. Telegram carries owner asks only.
81
+ # SECRETS NEVER GO IN THIS FILE: tokenEnv/botTokenEnv NAME an environment variable holding the token
82
+ # (or <NAME>_FILE pointing at a custody file); a value that is not an UPPER_SNAKE env var name is refused.
83
+ # Host credentials use the canonical runtime root secret.env; docs/host-secrets.md owns setup.
84
+ # Supplied environment values win, including blank values. Missing local file adds no values.
85
+ # repos: ledger-owner repository roots whose asks go public, relative to the source root (the
86
+ # directory holding this skill) or absolute. [] = the source root plus every .workspaces project owner.
87
+ # gateway.port: the one fixed local port the tunnel points at (outside the 6969..7069 form band).
88
+ # cloudflare.mode: off | quick | named.
89
+ # quick — `cloudflared tunnel --url`, a random https://*.trycloudflare.com host, no account, no auth,
90
+ # a new host on every restart. Good for decisions only.
91
+ # named — a fixed `hostname`, authenticated one of two ways: `tunnel` (UUID) + `credentialsFile`
92
+ # (the JSON `cloudflared tunnel create` wrote; the manager writes its own ingress config mapping
93
+ # hostname -> the gateway and never reads ~/.cloudflared/config.yml), or a remotely managed tunnel
94
+ # token in `tokenEnv` (its public hostname is routed to http://127.0.0.1:<gateway.port> in the
95
+ # Cloudflare dashboard).
96
+ # cloudflare.access: whether a Cloudflare Access application guards `hostname` (informational; false
97
+ # prints a warning). Recommended for anything beyond yes/no decisions.
98
+ # telegram.enabled / chatId: the owner's chat with the bot (BotFather token in botTokenEnv). Send the
99
+ # bot /start, then `starci connect telegram discover-chat` prints the chat id.
100
+ # telegram.exposeCredentialAsks: false keeps a credential/secret ask off the public link — the message
101
+ # says to answer it on this machine and carries only the localhost link, and the gateway refuses it.
102
+ connectors:
103
+ repos: []
104
+ gateway: {port: 7070}
105
+ cloudflare: {mode: off, tunnel: null, credentialsFile: null, tokenEnv: CLOUDFLARE_TUNNEL_TOKEN, hostname: null, access: false}
106
+ telegram:
107
+ enabled: false
108
+ botTokenEnv: TELEGRAM_BOT_TOKEN
109
+ chatId: null
110
+ exposeCredentialAsks: false
111
+ # asks — owner asks that carry a recommended option (question.recommended, or exactly one option
112
+ # marked "(recommended)" or its Vietnamese equivalents). autoAcceptRecommended: true answers such an
113
+ # ask with its recommendation instead of serving the form: the ledger records ask-answered with
114
+ # answeredBy auto-recommended, the kernel is woken, and Telegram gets one plain message naming the
115
+ # pick. A later owner answer supersedes it. excludes lists the ask classes that always reach the owner:
116
+ # credential — any ask with secret fields, or of kind credential, account, access or consent;
117
+ # handover — every handover.review ask (always excluded, even when removed from this list);
118
+ # draw-review — the opt-out for drawings (interface.draw, scripts/work/draw-review.mjs): unlisted, a
119
+ # drawing the owner did not ask to review is accepted without the owner (its accept option is the
120
+ # recommendation); a drawing the owner asked for (a prior owner redraw of the record, the owner
121
+ # opened its form, or question.ownerRequested) always reaches the owner;
122
+ # or any ask kind: information, authority, business-decision, irreversible-confirmation, ...
123
+ asks:
124
+ autoAcceptRecommended: false
125
+ excludes: [credential, irreversible-confirmation, handover]
126
+ # uat — the machine-wide UAT ceiling. At most maxConcurrent UAT runs (a browser session, a player, the
127
+ # app it drives) execute at once on this host, across every workflow; the rest wait queued for a slot
128
+ # (scripts/uat/uat-slots.mjs, lock files under <runtime>/uat-slots). A slot whose holder died is
129
+ # reclaimed. STARCI_UAT_MAX_CONCURRENT overrides it for one process tree.
130
+ uat:
131
+ maxConcurrent: 10
132
+ # orca — the owner's Orca app settings the runtime must know (engine/orca-config.mjs orcaSettings). maxWorkerDepth MUST equal
133
+ # the worker depth set in the Orca app: the deepest worker Orca starts under the owner's chat (chat 0, Kernel or
134
+ # Supervisor 1, op or [Worker] 2, draw critic 3). Orca exposes no read of it, so the runtime trusts this value and refuses
135
+ # a deeper launch before worker-start (worker-depth-exceeded). `start --check` with STARCI_ORCA_LIVE=1 measures Orca's
136
+ # real limit and turns red when the two differ.
137
+ orca:
138
+ maxWorkerDepth: 4
139
+ # debugLoop — the debug watcher. Workflow DEBUG is a /loop of the chat that started the workflow (the /starci skill sets it up in
140
+ # Claude Code and Codex; no seat, no background agent, nothing in the runtime schedules it). Each tick runs the read-only
141
+ # `starci debug digest`. interval is the loop cadence (<n>s | <n>m | <n>h, default `10m` = `debugLoop.interval`); worktreeLimit is the
142
+ # per-repository core-watch alert threshold (default `40` = `debugLoop.worktreeLimit`). Both keys are optional (engine/config.mjs DEBUG_LOOP_DEFAULTS).
143
+ debugLoop:
144
+ interval: 10m
145
+ worktreeLimit: 40
146
+ # reconciler — the host's one reconciler engine (scripts/reconciler/engine.mjs; contract
147
+ # modules/reconciler/reconciler.yaml). Each controller runs off | shadow (reads, records what it would do as
148
+ # reconciler.would rows, runs nothing) | active (acts; the old loop yields that duty via scripts/reconciler/owns.mjs).
149
+ # profile — the operational default of every controller, so a host is never left all-shadow by accident:
150
+ # operational job, host, workflow, resource active (they settle jobs, start services and seats, wake stalled
151
+ # workflows, throttle); gc, workers, learning shadow. The `start` skill applies and checks it.
152
+ # observe every controller shadow (read-only; a debugging or audit host).
153
+ # (absent) only the explicit controllers.<name>.mode entries count; an unnamed controller is off.
154
+ # controllers.<name>.mode overrides the profile for that one controller (e.g. gc: {mode: active}).
155
+ # Rollback of a controller: set its mode to off (the old loops take the duty back within
156
+ # allocation.reconciler.heartbeatStaleMs). Numbers: modules/models/runtimes.yaml allocation.reconciler.
157
+ reconciler:
158
+ enabled: true
159
+ profile: operational
160
+ controllers: {}
161
+ # specs.unit / specs.e2e in workflows (the `specs` map near the top; owner 2026-09-28 "speed up development;
162
+ # test later when asked"; scripts/route/spec-deferral.mjs). Each op brief's policy.specsToggle says what it does:
163
+ # unit: false - backend.implement, interface.implement and code.refactor run no jest/vitest/test:ci/coverage
164
+ # and demand no changed-line coverage (Sonar still runs: bugs, smells, security gate);
165
+ # test.author legs are never dispatched; review/handover gates do not count unit tests or coverage.
166
+ # e2e: false - e2e.verify legs and e2e test authoring are never dispatched; uat.verify is not e2e.
167
+ # A leg that is not dispatched settles `deferred: specs.<class>=false` with no attempt spent and the workflow
168
+ # proceeds past it; starci kernel status lists testsDeferred and `starci kernel run-deferred-tests --workflow <id> [--kind unit|e2e]`
169
+ # runs them later. Read on every wake: flipping a switch needs no restart.
170
+ # roots — where this host keeps the archive, the lane worktrees and its temporary files, when the owner moves them off the defaults.
171
+ # All keys are optional absolute directories; env STARCI_ARCHIVE_ROOT / STARCI_LANES_ROOT / STARCI_TEMP_ROOT win over them, and an
172
+ # absent key means <starciLocalRoot>/archive (<runtime root>/.runtime/archive), the per-user profile lanes directory, kept out of the checkout (scripts/machine/home.mjs lanesRoot),
173
+ # and the OS temp directory (engine/temp-root.mjs tempRoot; the processes the runtime starts get it as TEMP/TMP/TMPDIR). No host location lives in
174
+ # a tracked file: set the real directories only in the gitignored config.yaml copy, e.g.
175
+ # roots: {archive: <absolute directory>, lanes: <absolute directory>, temp: <absolute directory>}
176
+ # sonar — the SonarCloud organization the release's Sonar proof of the example apps analyses in (scripts/supervisor/release-sonarcloud.mjs). It is configuration, not a secret,
177
+ # so it lives here and not in secret.env; the project keys are <organization>_starci-example-<app>. The environment variable SONAR_ORGANIZATION wins over it (CI reads the
178
+ # repository variable of the same name). Empty until the owner sets it in the gitignored config.yaml copy, e.g.
179
+ # sonar: {organization: <the organization key shown on sonarcloud.io>}
180
+ sonar: {organization: null}
181
+ # resources — the host capacity floors the dispatch gate enforces, over the shipped numbers of modules/models/runtimes.yaml allocation.resources
182
+ # (scripts/machine/host-resources.mjs; docs/config-format.md "Host resource floors"). Every key is optional (a number above 0, or null):
183
+ # minFreeDiskGb free GB a drive holding the temp root or the repository must keep
184
+ # minFreeDiskPct the same floor as a percentage of the drive's size; with minFreeDiskGb the larger requirement applies
185
+ # minFreeRamPct free-RAM percentage below which no new heavy op starts
186
+ # set them only in the gitignored config.yaml copy, e.g.
187
+ # resources: {minFreeDiskGb: 5, minFreeDiskPct: 1, minFreeRamPct: 10}
188
+ # release — where the full test suite of a release is judged (scripts/guards/release-suite-mode.mjs; docs/releasing.md "Where the suite runs"). One key:
189
+ # suite: local the shipped default: `starci release cut` runs the root suite and the Linux parity container before it pushes, and the pre-push gate asks for their green rows
190
+ # suite: ci the owner's recorded choice (2026-10-09): no local full suite and no Linux container; the GitHub ci workflow runs the suite (with coverage, Codecov, SonarCloud) AFTER the push.
191
+ # The cut still runs npm run check, npm run test:packages, the specs affected by the release, the live Orca smokes and the example apps; the record lists the root suite as
192
+ # delegated, never green. A release can reach GitHub and its tag before the suite has run; a red CI is fixed forward with the next pre-release (starci release ci-status).
193
+ # "none" is refused: the suite does not vanish, it moves to CI. Set it only in the gitignored config.yaml copy: release: {suite: ci}
194
+ release: {suite: local}
@@ -0,0 +1,12 @@
1
+ // by-code-unit.mjs - the comparator of a bare `Array.prototype.sort()`, owned beside the engine files that digest and canonicalize (scripts/lib/list.mjs re-exports it).
2
+
3
+ /**
4
+ * The comparator of a bare `Array.prototype.sort()`: values compared as strings by UTF-16 code unit. Pass it to `sort` and
5
+ * `toSorted` so the order is stated, and stays byte-identical to the default order that digests and canonical output depend on.
6
+ */
7
+ export const byCodeUnit = (a, b) => {
8
+ const left = String(a);
9
+ const right = String(b);
10
+ if (left < right) return -1;
11
+ return left > right ? 1 : 0;
12
+ };