@starci/hfs 4.7.26 → 4.7.28

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 (45) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +2 -3
  3. package/package.json +7 -1
  4. package/release/candidate-ci.mjs +3 -21
  5. package/runtime/engine/yaml.mjs +1 -1
  6. package/runtime/knowledge/hfs/canon-pins.yaml +4 -4
  7. package/runtime/knowledge/hfs/rules.yaml +913 -637
  8. package/runtime/knowledge/hfs/slots.yaml +21 -14
  9. package/runtime/modules/kernel/failure-codes.yaml +10 -0
  10. package/runtime/packages/cli/src/catalog.generated.mjs +130 -54
  11. package/runtime/scripts/api/git/check-ignore.mjs +5 -0
  12. package/runtime/scripts/gates/runtime-host.mjs +83 -0
  13. package/runtime/scripts/gates/sonar-gate.mjs +251 -0
  14. package/runtime/scripts/gates/starcistacks-ci.mjs +55 -0
  15. package/runtime/scripts/gates/starcistacks-form.mjs +26 -0
  16. package/runtime/scripts/gates/starcistacks-runtime.mjs +2 -0
  17. package/runtime/scripts/gates/starcistacks-value-file.mjs +17 -0
  18. package/runtime/scripts/gates/starcistacks.mjs +499 -0
  19. package/runtime/scripts/hfs/app-wiring.mjs +2 -2
  20. package/runtime/scripts/hfs/architecture/supabase.mjs +1 -1
  21. package/runtime/scripts/hfs/check.mjs +4 -1
  22. package/runtime/scripts/hfs/coverage-from.mjs +1 -1
  23. package/runtime/scripts/hfs/manifest-shape.mjs +1 -3
  24. package/runtime/scripts/hfs/rule-catalog.mjs +7 -10
  25. package/runtime/scripts/hfs/rules/a15-findings.mjs +1 -1
  26. package/runtime/scripts/hfs/rules/assets.mjs +1 -1
  27. package/runtime/scripts/hfs/rules/bypass-markers.mjs +24 -0
  28. package/runtime/scripts/hfs/rules/frontend-tree.mjs +2 -2
  29. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +1 -1
  30. package/runtime/scripts/lib/bypass-markers.mjs +116 -0
  31. package/runtime/scripts/lib/check-code-loosening.mjs +128 -0
  32. package/runtime/scripts/lib/example-refs.mjs +205 -0
  33. package/runtime/scripts/lib/failure-code-reads.mjs +100 -0
  34. package/runtime/scripts/lib/gate-loosening.mjs +52 -8
  35. package/runtime/scripts/lib/json-formats.mjs +34 -0
  36. package/runtime/scripts/lib/managed-json.mjs +1 -1
  37. package/runtime/scripts/lib/package-at.mjs +1 -1
  38. package/runtime/scripts/lib/properties.mjs +16 -0
  39. package/runtime/scripts/lib/provision-control-failure.mjs +32 -0
  40. package/runtime/scripts/lib/sonar-new-code.mjs +58 -0
  41. package/runtime/scripts/lib/starciwork-boundary.mjs +164 -0
  42. package/runtime/scripts/lib/work-edition.mjs +40 -0
  43. package/sync/format.mjs +1 -1
  44. package/sync/hygiene.mjs +16 -4
  45. package/templates/app/hooks/husky/pre-commit +8 -7
@@ -538,6 +538,7 @@ slots:
538
538
  profiles: [app]
539
539
  exemplifiedBy:
540
540
  - packages/hfs/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml
541
+ - {path: packages/hfs/src/hfs-release-provenance.spec.mjs, edition: full}
541
542
  editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite manages its Supabase database deploy workflow; full owns an optional provider-specific workflow."
542
543
  # The database deploy workflow: workflow_dispatch only (like app.ci-e2e); starci app sync renders it under the managed ci group.
543
544
  path: ".github/workflows/db-deploy.yml"
@@ -548,7 +549,6 @@ slots:
548
549
  liteManagedBy: ci-workflows-lite
549
550
  rules: [HFS_MANAGED_FILE_DRIFT]
550
551
  since: 2.1.0
551
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
552
552
  - id: app.ci-stack-deploy
553
553
  since: 3.3.0
554
554
  profiles: [app]
@@ -687,7 +687,8 @@ slots:
687
687
  tier: none
688
688
  tests: none
689
689
  rules: [HFS_PLAINTEXT_SECRET, HFS_SCRIPT_ONE_OFF, BE_SPEC_PLACEMENT, HFS_REPO_LOCAL_CHECK]
690
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
690
+ exemplifiedBy:
691
+ - {path: packages/hfs/src/hfs-slot-ownership.spec.mjs, edition: lite}
691
692
  - id: app.sides
692
693
  since: 3.1.0
693
694
  profiles: [app]
@@ -810,6 +811,7 @@ slots:
810
811
  profiles: [app]
811
812
  exemplifiedBy:
812
813
  - packages/hfs/templates/app/skeleton-lite/supabase/
814
+ - {path: packages/hfs/src/hfs-database-access.spec.mjs, edition: full}
813
815
  editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): Supabase is required in lite; full enables it only through a declared connection."
814
816
  # The one Supabase project directory, at the app root beside be/, fe/ and .starcistacks (never under a side).
815
817
  path: "supabase/"
@@ -820,11 +822,11 @@ slots:
820
822
  tier: none
821
823
  tests: none
822
824
  since: 2.1.0
823
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
824
825
  - id: app.supabase.config
825
826
  profiles: [app]
826
827
  exemplifiedBy:
827
828
  - packages/hfs/templates/app/skeleton-lite/supabase/config.toml
829
+ - {path: packages/hfs/src/db-config-policy.spec.mjs, edition: full}
828
830
  editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite requires Supabase configuration; full requires it only when Supabase is declared."
829
831
  # The project configuration: auth keys are env(NAME) references, never literals (L08 judges its policy keys).
830
832
  path: "supabase/config.toml"
@@ -835,11 +837,11 @@ slots:
835
837
  tier: none
836
838
  tests: none
837
839
  since: 2.1.0
838
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
839
840
  - id: app.supabase.migrations
840
841
  profiles: [app]
841
842
  exemplifiedBy:
842
843
  - packages/hfs/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql
844
+ - {path: packages/hfs/src/db-migration-shape.spec.mjs, edition: full}
843
845
  editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite uses Supabase schema authority and requires a migration; full opts into that provider."
844
846
  # The schema authority of an app on Supabase: forward-only SQL migrations named <timestamp14>_<kebab-name>.sql,
845
847
  # one file per change, immutable once on origin/main (L02-L08 judge them).
@@ -852,7 +854,6 @@ slots:
852
854
  tier: none
853
855
  tests: none
854
856
  since: 2.1.0
855
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
856
857
  - id: app.supabase.migrations.archive
857
858
  profiles: [app]
858
859
  # The migrations an existing app retired at its cutover (owner ruling 2026-10-13, migration baseline policy): the live schema is
@@ -888,7 +889,8 @@ slots:
888
889
  tier: none
889
890
  tests: none
890
891
  since: 2.1.0
891
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
892
+ exemplifiedBy:
893
+ - {path: packages/hfs/src/db-types-returned-collector.spec.mjs, edition: full}
892
894
  - id: app.supabase.templates
893
895
  profiles: [app]
894
896
  path: "supabase/templates/*.html"
@@ -1025,7 +1027,8 @@ slots:
1025
1027
  tests: none
1026
1028
  coverage: none
1027
1029
  rules: [HFS_UNUSED_SYMBOL]
1028
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
1030
+ exemplifiedBy:
1031
+ - {path: packages/hfs/src/hfs-unused-symbol-ledger.spec.mjs, editions: [lite, full]}
1029
1032
  - id: repo.packages
1030
1033
  since: 3.1.0
1031
1034
  profiles: [be, fe]
@@ -1183,8 +1186,9 @@ slots:
1183
1186
  tests: none
1184
1187
  coverage: none
1185
1188
  why: the MCP tool catalog an app's MCP server publishes at one contract major (`<server>@<major>`), the `tools/list` result its clients rely on, committed as a cross-repo interface; the canon emits no MCP catalog, so its shape is judged (R302 HFS_MCP_TOOLS_SNAPSHOT)
1186
- exampleNa: "owner:mcp-tools-snapshot-no-example (2026-10-21): no example app serves an MCP server; the package spec judges the shape"
1187
1189
  rules: [HFS_MCP_TOOLS_SNAPSHOT]
1190
+ exemplifiedBy:
1191
+ - {path: packages/hfs/src/mcp-tools-snapshot.spec.mjs, editions: [lite, full]}
1188
1192
 
1189
1193
  # ----- BE: apps (one slot per app kind; a new kind = a new slot, minor bump) ---------------------------
1190
1194
  # The files of every app (owner ruling 2026-10-14, R245 and R246, since 3.1.0), each with a line budget that
@@ -1449,7 +1453,8 @@ slots:
1449
1453
  coverage: none
1450
1454
  rules: [BE_KIND_ISOLATION, BE_FEATURE_IMPORTS_FEATURE, BE_FEATURE_NOT_COMPOSED]
1451
1455
  since: 1.0.0
1452
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
1456
+ exemplifiedBy:
1457
+ - {path: packages/hfs/src/hfs-stacks-swarm.spec.mjs, edition: lite}
1453
1458
  - id: be.feature.realtime.graphql
1454
1459
  profiles: [be]
1455
1460
  editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite uses only the in-process socket.io gateway; the GraphQL subscription door remains full-only."
@@ -1653,7 +1658,8 @@ slots:
1653
1658
  coverage: required
1654
1659
  budget: {indexExports: 60}
1655
1660
  rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_MODULE_SHAPE, BE_INFRA_OWNER]
1656
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
1661
+ exemplifiedBy:
1662
+ - {path: packages/eslint/be/webhook-handoff-inbox.spec.mjs, edition: lite}
1657
1663
  - id: be.platform.queue
1658
1664
  since: 3.1.0
1659
1665
  profiles: [be]
@@ -1792,7 +1798,8 @@ slots:
1792
1798
  tests: unit-beside # a measured slot: <name>.projection.spec.ts beside its projection is legal (the file owes 100 per file; a spec of its own is optional)
1793
1799
  coverage: required
1794
1800
  rules: [BE_PROJECTION_WRITE_OWNER, BE_PROJECTION_SHAPE, BE_TIER_DIRECTION, BE_CONTEXT_COUPLING]
1795
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
1801
+ exemplifiedBy:
1802
+ - {path: packages/eslint/be/jobs-projections-coverage.spec.mjs, edition: lite}
1796
1803
  - id: be.integrations
1797
1804
  since: 3.1.0
1798
1805
  profiles: [be]
@@ -1822,6 +1829,7 @@ slots:
1822
1829
  profiles: [be]
1823
1830
  exemplifiedBy:
1824
1831
  - packages/hfs/templates/be/skeleton-lite/src/modules/integrations/supabase/
1832
+ - {path: packages/hfs/src/be-supabase-jwt-verified.spec.mjs, edition: full}
1825
1833
  editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite requires the Supabase Auth and Storage integration; full opts in through a connection."
1826
1834
  # The only be import of @supabase/* (L10): the Auth admin and Storage admin client, plus the JWKS-based verification of
1827
1835
  # the Supabase access token the guard chain reads (L16). The connection itself is ordinary Postgres (the shared
@@ -1839,7 +1847,6 @@ slots:
1839
1847
  rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT]
1840
1848
  coverage: required
1841
1849
  since: 2.1.0
1842
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
1843
1850
 
1844
1851
  # ----- BE: tests (owner test layout 2026-09-30). Unit specs are <name>.service.spec.ts beside their service and nothing else; everything else
1845
1852
  # lives under src/tests/, one folder per kind, and the folder and the file suffix always agree (a file whose suffix does
@@ -1872,7 +1879,6 @@ slots:
1872
1879
  overridden in the DI container. It is the only test location that may import typeorm's DataSource or testcontainers,
1873
1880
  call migrate/runMigrations/synchronize, or write process.env.
1874
1881
  rules: [BE_TEST_TOPOLOGY, BE_TEST_CONSTRAINTS_ON]
1875
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
1876
1882
 
1877
1883
  - id: be.tests.world.kit
1878
1884
  since: 3.1.0
@@ -2269,7 +2275,8 @@ slots:
2269
2275
  tests: none
2270
2276
  why: the canonical GraphQL client output of codegen is local and never committed; this specific slot overrides the tracked API parent
2271
2277
  rules: [FE_WIRE_GENERATED]
2272
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
2278
+ exemplifiedBy:
2279
+ - {path: packages/eslint/fe/r52-wire-generated.spec.mjs, editions: [lite, full]}
2273
2280
  - id: fe.transport.client
2274
2281
  since: 3.1.0
2275
2282
  profiles: [fe]
@@ -3044,6 +3044,16 @@ HFS_AUDIT_STEP_MISSING:
3044
3044
  owner: op-retry
3045
3045
  kind: check-finding
3046
3046
 
3047
+ HFS_BYPASS_MARKER:
3048
+ title: "A product repository holds no bypass of a check"
3049
+ title_vi: "Kho có đoạn bỏ qua một bước kiểm tra"
3050
+ meaning_vi: "Kho có đoạn làm cho một bước kiểm tra qua mà không thỏa nó ở `<f>`: chú thích tắt luật (eslint, stylelint, prettier, biome), `ts-ignore`/`ts-expect-error`/`ts-nocheck`, `NOSONAR`, bỏ qua độ phủ istanbul/c8/v8, bài test bị bỏ qua hoặc chỉ chạy riêng, hoặc lỗi bị nuốt trong hook, script hay workflow (`|| true`, `continue-on-error`, `set +e`, `HUSKY=0`, `--no-verify`, `fail_ci_if_error: false`)."
3051
+ causes_vi:
3052
+ - "Vi phạm luật R303: một lần bỏ qua kiểm tra luôn là lỗi (phán quyết của chủ sở hữu loosening-always-red); kiểm tra được thỏa bằng cách sửa nguyên nhân, không phải bỏ qua. Giao diện (FE) không có test nhưng mã nguồn vẫn bị soát các chú thích tắt luật."
3053
+ nextStep_vi: "Op vừa tạo ra bản này sẽ được chạy lại để sửa theo lời nhắn của bộ kiểm tra: xóa đoạn bỏ qua và sửa nguyên nhân thật (mã, kiểu dữ liệu, test, bước chạy)."
3054
+ owner: op-retry
3055
+ kind: check-finding
3056
+
3047
3057
  HFS_CANON_COPY_DRIFT:
3048
3058
  title: "A hash-sensitive canon file differs from its template"
3049
3059
  title_vi: "Tệp canon nhạy hash lệch khỏi template"
@@ -1,7 +1,7 @@
1
1
  // GENERATED by scripts/cli/gen-catalog.mjs — do not edit
2
2
  export const CATALOG = {
3
3
  "schema": "starci/cli-catalog@1",
4
- "hash": "5bec1a5a568a8294dd40e3e09b7b97693660b8a7c273fcd35594aea87196257a",
4
+ "hash": "466ffd40fe4a003c976ac306bcb14639b1b57c947112e7899ef717a819e571f5",
5
5
  "global": [
6
6
  {
7
7
  "name": "json",
@@ -1248,7 +1248,7 @@ export const CATALOG = {
1248
1248
  "requireCi true: push --execute sends HEAD to refs/heads/release/candidate-<version> first and waits for its ci.yml run",
1249
1249
  "green moves the branch and the tag in one atomic push; red refuses RELEASE_CANDIDATE_CI_RED naming the failing jobs",
1250
1250
  "no run, or no end in time, refuses RELEASE_CANDIDATE_CI_TIMEOUT; unreadable GitHub refuses RELEASE_CI_UNKNOWN; nothing reaches the branch",
1251
- "only the owner's starci release ci-override --sha <sha> --reason <text> record lets a red candidate through; there is no --no-ci-check flag",
1251
+ "a red candidate never passes: there is no override and no --no-ci-check flag",
1252
1252
  "gate refuses a dirty tree, HEAD other than --source, a moved HEAD and a tree dirtied by the gates; record and tag only when all gates are green",
1253
1253
  "a gate with summary jest is red on failed tests and on a coverage threshold miss even when the process exits 0",
1254
1254
  "push needs the annotated tag and L4 record of HEAD, runs git push --dry-run --atomic first and never uses --no-verify"
@@ -8466,56 +8466,6 @@ export const CATALOG = {
8466
8466
  },
8467
8467
  "runtimeSide": "tree"
8468
8468
  },
8469
- "ci-override": {
8470
- "summary": "the owner accepts a red candidate CI for one SHA (emergency path) and records who and why",
8471
- "owner": "runtime",
8472
- "flags": [
8473
- {
8474
- "name": "sha",
8475
- "type": "string",
8476
- "summary": "required full 40 lowercase hex SHA of the red CI run"
8477
- },
8478
- {
8479
- "name": "reason",
8480
- "type": "string",
8481
- "summary": "required nonempty owner reason, recorded verbatim after trimming"
8482
- },
8483
- {
8484
- "name": "repo",
8485
- "type": "string",
8486
- "summary": "repository receiving the override (default current directory)"
8487
- }
8488
- ],
8489
- "exit": {
8490
- "0": "the override record and runtime owner ruling were written",
8491
- "2": "RIGHTS_OWNER_ONLY (a bound role or non-TTY stdin), bad usage, confirmation mismatch, or write failure"
8492
- },
8493
- "json": "none",
8494
- "examples": [
8495
- "starci release ci-override --sha 0123456789abcdef0123456789abcdef01234567 --reason \"Owner accepts this CI failure for the release\"",
8496
- "starci release ci-override --repo ../my-app --sha 0123456789abcdef0123456789abcdef01234567 --reason \"Owner accepts the failed run\""
8497
- ],
8498
- "editions": [
8499
- "full"
8500
- ],
8501
- "impl": {
8502
- "script": "scripts/supervisor/release-ci-override-cli.mjs"
8503
- },
8504
- "conventions": [
8505
- "OWNER RULING candidate-ci (2026-10-18): a release reaches main only when the candidate CI of its own sha is green;",
8506
- "this verb is the one documented EMERGENCY path for a red candidate.",
8507
- "The override is owner-only, records who and why, and agents cannot run it (OWNER RULING alpha20-ci-override-owner-only).",
8508
- "Every bound role is refused by the command guard and by this script. The owner must use a terminal and type the first 12 SHA characters exactly.",
8509
- "Writes <git-common-dir>/starci-release/<sha>.ci-override.json, schema starci/ci-override@1,",
8510
- "with sha, reason, by (git user.name and OS user), at, and runUrl (empty if GitHub is unavailable).",
8511
- "release cut and app release push read that record for the exact candidate sha only; the release record logs its use.",
8512
- "For the runtime it also appends ci-override-<sha12> under rulings in modules/kernel/owner-rulings.yaml as the audit trail.",
8513
- "App repositories receive only the record.",
8514
- "An override applies only to its exact SHA. It never permits a release whose CI is unknown or timed out.",
8515
- "Existing ruling IDs with another reason refuse."
8516
- ],
8517
- "runtimeSide": "tree"
8518
- },
8519
8469
  "ci-status": {
8520
8470
  "summary": "the verdict of the GitHub ci workflow, Codecov and Sonar for a release; red is recorded for Debug",
8521
8471
  "owner": "runtime",
@@ -8662,8 +8612,7 @@ export const CATALOG = {
8662
8612
  "No run within the grace, or no end within the wait, refuses RELEASE_CANDIDATE_CI_TIMEOUT.",
8663
8613
  "A red candidate keeps its ref for diagnosis; after a cut the candidate ref is left on the remote, naming the released sha.",
8664
8614
  "Only on green is the annotated tag created and main plus the tag pushed atomically; the release record logs the candidate verdict.",
8665
- "Emergency path, owner-only: starci release ci-override --sha <HEAD> --reason <text> lets a red candidate through.",
8666
- "The release record logs its use."
8615
+ "There is no override: a red candidate never passes."
8667
8616
  ],
8668
8617
  "runtimeSide": "tree"
8669
8618
  },
@@ -8986,6 +8935,133 @@ export const CATALOG = {
8986
8935
  "summary": "resolve runtime operations from structured routing inputs",
8987
8936
  "owner": "runtime",
8988
8937
  "verbs": {
8938
+ "model": {
8939
+ "summary": "resolve which tier members may take a workload from the declarative model-selection rules",
8940
+ "owner": "runtime",
8941
+ "flags": [
8942
+ {
8943
+ "name": "kind",
8944
+ "type": "string",
8945
+ "summary": "operation kind the workload runs"
8946
+ },
8947
+ {
8948
+ "name": "role",
8949
+ "type": "enum",
8950
+ "enum": [
8951
+ "implement",
8952
+ "verify",
8953
+ "decide",
8954
+ "plan",
8955
+ "write"
8956
+ ],
8957
+ "summary": "role of the workload (default is the role of the kind in kinds.yaml)"
8958
+ },
8959
+ {
8960
+ "name": "domain",
8961
+ "type": "string",
8962
+ "summary": "domain of the workload"
8963
+ },
8964
+ {
8965
+ "name": "risk",
8966
+ "type": "enum",
8967
+ "enum": [
8968
+ "low",
8969
+ "medium",
8970
+ "high",
8971
+ "critical"
8972
+ ],
8973
+ "summary": "risk of the workload"
8974
+ },
8975
+ {
8976
+ "name": "floor",
8977
+ "type": "enum",
8978
+ "enum": [
8979
+ "probation",
8980
+ "standard",
8981
+ "high",
8982
+ "critical"
8983
+ ],
8984
+ "summary": "lowest qualification a pick may carry"
8985
+ },
8986
+ {
8987
+ "name": "tools",
8988
+ "type": "list",
8989
+ "summary": "tool the workload needs; comma list or repeatable"
8990
+ },
8991
+ {
8992
+ "name": "context-tokens",
8993
+ "type": "string",
8994
+ "summary": "context size in tokens the workload needs"
8995
+ },
8996
+ {
8997
+ "name": "external",
8998
+ "type": "boolean",
8999
+ "summary": "the workload has external effects and probation is forbidden"
9000
+ },
9001
+ {
9002
+ "name": "unapproved",
9003
+ "type": "boolean",
9004
+ "summary": "the workflow is not approved and probation is forbidden"
9005
+ },
9006
+ {
9007
+ "name": "no-review",
9008
+ "type": "boolean",
9009
+ "summary": "no fresh independent review is planned"
9010
+ },
9011
+ {
9012
+ "name": "no-checks",
9013
+ "type": "boolean",
9014
+ "summary": "the op declares no machine checks"
9015
+ },
9016
+ {
9017
+ "name": "difficulty",
9018
+ "type": "enum",
9019
+ "enum": [
9020
+ "easy",
9021
+ "medium",
9022
+ "hard",
9023
+ "insane"
9024
+ ],
9025
+ "summary": "measured difficulty (default medium, required with --plan)"
9026
+ },
9027
+ {
9028
+ "name": "plan",
9029
+ "type": "boolean",
9030
+ "summary": "what-if view that walks the tier chain and annotates missing or stale qualification evidence instead of refusing"
9031
+ },
9032
+ {
9033
+ "name": "repo",
9034
+ "type": "string",
9035
+ "summary": "repository whose ledger provider-health circuit marks a model unavailable"
9036
+ },
9037
+ {
9038
+ "name": "models-dir",
9039
+ "type": "string",
9040
+ "summary": "models directory (default is modules/models)"
9041
+ },
9042
+ {
9043
+ "name": "verbose",
9044
+ "type": "boolean",
9045
+ "summary": "print the per-candidate rejection reasons"
9046
+ }
9047
+ ],
9048
+ "exit": {
9049
+ "0": "a member is picked",
9050
+ "1": "no eligible model",
9051
+ "2": "bad usage"
9052
+ },
9053
+ "json": "flag",
9054
+ "examples": [
9055
+ "starci route model --kind interface.draw --plan --json"
9056
+ ],
9057
+ "editions": [
9058
+ "full"
9059
+ ],
9060
+ "impl": {
9061
+ "script": "scripts/route/route-model.mjs"
9062
+ },
9063
+ "runtimeSide": "host"
9064
+ },
8989
9065
  "op": {
8990
9066
  "summary": "resolve the operation that serves a structured routing request",
8991
9067
  "owner": "runtime",
@@ -0,0 +1,5 @@
1
+ // check-ignore.mjs — `git check-ignore <args>`: whether paths are ignored (-q exits 0 for an ignored path).
2
+ import { runGit } from './lib.mjs';
3
+
4
+ /** The spawn result {status, stdout, stderr, error} of `git check-ignore ...args`; options are runGit's (cwd or dir, config, timeout, maxBuffer, input, encoding, env, git). */
5
+ export const checkIgnore = (args = [], options = {}) => runGit(['check-ignore', ...args], options);
@@ -0,0 +1,83 @@
1
+ // runtime-host.mjs - the repository that hosts this runtime and how a stack declaration's `repository` names resolve on
2
+ // this machine. One home for scripts/gates/starcistacks.mjs (the services contract) and scripts/gates/sonar-local.mjs
3
+ // (the Sonar helper), so both read a declaration's `{repository, path}` the same way.
4
+ //
5
+ // The runtime host is the repository whose checkout holds the runtime's MAIN checkout (`<host>/.claude`), found from git
6
+ // identity, never from the folder this runtime tree happens to sit in: a lane worktree of the runtime
7
+ // (<lanes root>/<lane>/<name>) resolves to the same host as the main checkout. STARCI_SOURCE_ROOT overrides it.
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { skillRoot, starciSourceRoot } from '../../engine/runtime-root.mjs';
11
+ import { isDir } from '../lib/fs-kind.mjs';
12
+ import { repositoryHome, repositoryName } from '../hfs/repo-identity.mjs';
13
+ import { secretEnv } from '../../engine/secrets.mjs';
14
+ import { revParseQuery } from '../api/git/rev-parse-query.mjs';
15
+ import { gitOutputOf } from '../lib/git.mjs';
16
+
17
+ /** The repository hosting this runtime: STARCI_SOURCE_ROOT, else the folder holding the runtime's main checkout. */
18
+ export function runtimeHostRoot(env = process.env) {
19
+ if (env.STARCI_SOURCE_ROOT) return starciSourceRoot(env);
20
+ return path.dirname(repositoryHome(skillRoot));
21
+ }
22
+
23
+ /** The canonical runtime main root of a runtime checkout or lane (its secret.env lives there); throws when the root is not a verified Git top level. */
24
+ export function verifiedRuntimeMain(runtimeRoot, env) {
25
+ if (typeof runtimeRoot !== 'string' || !path.isAbsolute(runtimeRoot))
26
+ throw new TypeError('runtimeSecretEnv requires an absolute runtime root');
27
+ const stat = fs.lstatSync(runtimeRoot);
28
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('runtime root must be a regular directory');
29
+ const root = fs.realpathSync.native(runtimeRoot), marker = path.join(root, '.git');
30
+ let gitMarker;
31
+ try { gitMarker = fs.lstatSync(marker); } catch (error) {
32
+ if (error?.code === 'ENOENT') return root;
33
+ throw error;
34
+ }
35
+ if (gitMarker.isSymbolicLink() || (!gitMarker.isFile() && !gitMarker.isDirectory()))
36
+ throw new Error('runtime Git identity is unavailable');
37
+ const topLevel = cwd => fs.realpathSync.native(path.resolve(cwd,
38
+ gitOutputOf(revParseQuery(['--show-toplevel'], { cwd, env })).trim()));
39
+ if (!same(topLevel(root), root)) throw new Error('runtime must be its Git top level');
40
+ const main = fs.realpathSync.native(repositoryHome(root)), mainMarker = fs.lstatSync(path.join(main, '.git'));
41
+ if (!mainMarker.isDirectory() || mainMarker.isSymbolicLink() || !same(topLevel(main), main))
42
+ throw new Error('runtime main Git identity is unavailable');
43
+ return main;
44
+ }
45
+
46
+ /**
47
+ * Load host-local credentials from this runtime's verified main tree, or its installed physical tree.
48
+ * A lane never reads its own alternate file. The actual environment wins; no process state is mutated.
49
+ */
50
+ export function runtimeSecretEnv(env = process.env, runtimeRoot = skillRoot) {
51
+ return secretEnv(verifiedRuntimeMain(runtimeRoot, env), env);
52
+ }
53
+
54
+ const same = (a, b) => (process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b);
55
+
56
+ /**
57
+ * The checkout of the repository a declaration names: the declaring repository itself, the runtime host, or a sibling
58
+ * checkout of either (by repository identity, so a worktree and its main checkout are one repository). Null when the
59
+ * repository is not checked out on this machine.
60
+ */
61
+ export function resolveDeclaredRepository(name, { fromRepo } = {}) {
62
+ const wanted = typeof name === 'string' && name.trim() ? name.trim() : null;
63
+ if (!wanted) return null;
64
+ const candidates = [...(fromRepo ? [path.resolve(fromRepo)] : []), runtimeHostRoot()];
65
+ for (const dir of candidates) if (same(repositoryName(dir), wanted) && isDir(dir)) return dir;
66
+ for (const dir of candidates) {
67
+ const sibling = path.join(path.dirname(repositoryHome(dir)), wanted);
68
+ if (isDir(sibling)) return sibling;
69
+ }
70
+ return null;
71
+ }
72
+
73
+ /**
74
+ * A custody path of a declared repository as a file on this machine. A path under `.claude/` of the runtime host is a member of
75
+ * the runtime tree in use (this checkout, a lane worktree included), the same rule a host-owned stack root `.claude/ext/<service>`
76
+ * follows; any other path is joined to the repository's checkout. A path with a `..` segment is refused (null).
77
+ */
78
+ export function resolveCustodyFile(repo, rel) {
79
+ if (!repo || !rel || String(rel).split(/[\\/]/).includes('..')) return null;
80
+ const posix = String(rel).replaceAll('\\', '/');
81
+ if (posix.startsWith('.claude/') && same(path.resolve(repo), path.resolve(runtimeHostRoot()))) return path.join(skillRoot, posix.slice('.claude/'.length));
82
+ return path.join(repo, rel);
83
+ }