@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.
- package/CHANGELOG.md +5 -1
- package/emit/operations.mjs +1 -1
- package/emit/type-schema.mjs +2 -1
- package/lint/run.mjs +65 -24
- package/package.json +1 -1
- package/report/order.mjs +2 -0
- package/runtime/config.example.yaml +194 -0
- package/runtime/engine/by-code-unit.mjs +12 -0
- package/runtime/engine/config.mjs +222 -239
- package/runtime/engine/invalid-config.mjs +5 -5
- package/runtime/engine/model-config.mjs +90 -0
- package/runtime/engine/orca-config.mjs +3 -1
- package/runtime/engine/release-config.mjs +28 -0
- package/runtime/engine/removed-vocabulary.mjs +28 -0
- package/runtime/engine/resources-config.mjs +52 -0
- package/runtime/engine/runtime-root.mjs +17 -0
- package/runtime/engine/secrets.mjs +35 -31
- package/runtime/engine/sonar-config.mjs +21 -0
- package/runtime/engine/temp-root.mjs +32 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
- package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
- package/runtime/knowledge/hfs/rules.yaml +161 -65
- package/runtime/knowledge/hfs/slots.yaml +11 -14
- package/runtime/knowledge/patterns/be/api.yaml +7 -24
- package/runtime/knowledge/patterns/be/cli.yaml +3 -12
- package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
- package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
- package/runtime/modules/kernel/failure-codes.yaml +1 -41
- package/runtime/modules/kernel/removed-vocabulary.yaml +268 -0
- package/runtime/modules/models/registry.yaml +25 -359
- package/runtime/modules/models/runtimes.yaml +126 -265
- package/runtime/modules/models/tiers.yaml +66 -0
- package/runtime/scripts/api/fs/claim-file.mjs +30 -0
- package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
- package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
- package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
- package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
- package/runtime/scripts/api/git/lib.mjs +34 -1
- package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
- package/runtime/scripts/api/process/run-program.mjs +2 -1
- package/runtime/scripts/api/sops/lib.mjs +198 -120
- package/runtime/scripts/hfs/allows.mjs +3 -3
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
- package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
- package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
- package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
- package/runtime/scripts/hfs/architecture/config.mjs +95 -80
- package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
- package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
- package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
- package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
- package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
- package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
- package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
- package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
- package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
- package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
- package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
- package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
- package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
- package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
- package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
- package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
- package/runtime/scripts/hfs/architecture/index.mjs +164 -104
- package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
- package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
- package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
- package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
- package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
- package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
- package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
- package/runtime/scripts/hfs/architecture/required-files.mjs +121 -70
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
- package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
- package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
- package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
- package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
- package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
- package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
- package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
- package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
- package/runtime/scripts/hfs/architecture.mjs +3 -4
- package/runtime/scripts/hfs/catalog-once.mjs +15 -0
- package/runtime/scripts/hfs/check.mjs +63 -71
- package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
- package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
- package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
- package/runtime/scripts/hfs/edition-slots.mjs +5 -3
- package/runtime/scripts/hfs/linear-text.mjs +30 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
- package/runtime/scripts/hfs/path-findings.mjs +58 -38
- package/runtime/scripts/hfs/pin-findings.mjs +31 -0
- package/runtime/scripts/hfs/repo-identity.mjs +21 -4
- package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
- package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
- package/runtime/scripts/hfs/rules/cli.mjs +5 -1
- package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
- package/runtime/scripts/hfs/rules/contract.mjs +35 -27
- package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
- package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
- package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
- package/runtime/scripts/hfs/rules/database.mjs +36 -23
- package/runtime/scripts/hfs/rules/deps.mjs +64 -32
- package/runtime/scripts/hfs/rules/docker.mjs +113 -65
- package/runtime/scripts/hfs/rules/edition.mjs +118 -111
- package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
- package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
- package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
- package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
- package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
- package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
- package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
- package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
- package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
- package/runtime/scripts/hfs/rules/saga.mjs +128 -61
- package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
- package/runtime/scripts/hfs/rules/services.mjs +34 -16
- package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
- package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
- package/runtime/scripts/hfs/secret.mjs +59 -48
- package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
- package/runtime/scripts/hfs/slot-classify.mjs +65 -0
- package/runtime/scripts/hfs/slot-errors.mjs +11 -0
- package/runtime/scripts/hfs/slot-imports.mjs +76 -0
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
- package/runtime/scripts/hfs/slot-match.mjs +153 -0
- package/runtime/scripts/hfs/slot-path.mjs +8 -0
- package/runtime/scripts/hfs/slot-required.mjs +76 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
- package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
- package/runtime/scripts/hfs/slots.mjs +74 -695
- package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
- package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
- package/runtime/scripts/hfs/tree.mjs +19 -14
- package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
- package/runtime/scripts/hfs/view.mjs +1 -1
- package/runtime/scripts/lib/dockerfile.mjs +56 -33
- package/runtime/scripts/lib/env.mjs +10 -0
- package/runtime/scripts/lib/event-contract.mjs +17 -13
- package/runtime/scripts/lib/git.mjs +2 -2
- package/runtime/scripts/lib/graphql-contract.mjs +158 -322
- package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
- package/runtime/scripts/lib/i18n.mjs +3 -2
- package/runtime/scripts/lib/in-order.mjs +71 -0
- package/runtime/scripts/lib/language.mjs +3 -3
- package/runtime/scripts/lib/list.mjs +8 -1
- package/runtime/scripts/lib/path-key.mjs +38 -5
- package/runtime/scripts/lib/pid-alive.mjs +7 -0
- package/runtime/scripts/lib/regex.mjs +1 -1
- package/runtime/scripts/lib/same-text.mjs +1 -1
- package/runtime/scripts/lib/secret-patterns.mjs +6 -2
- package/runtime/scripts/lib/sleep-sync.mjs +2 -2
- package/runtime/scripts/lib/sops-envelope.mjs +95 -48
- package/runtime/scripts/lib/stack-services.mjs +1 -2
- package/runtime/scripts/lib/ts-ast.mjs +9 -0
- package/runtime/scripts/lib/walk.mjs +6 -1
- package/runtime/scripts/lib/yaml-cached.mjs +14 -0
- package/scaffold/add-cli-lite.mjs +2 -1
- package/scaffold/add-table.mjs +2 -2
- package/scaffold/add.mjs +3 -2
- package/scaffold/app.mjs +4 -2
- package/scaffold/edition-gate.mjs +25 -25
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +5 -4
- package/sync/hygiene.mjs +15 -9
- package/sync/index.mjs +7 -3
- package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
- package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
- package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
- package/templates/app/ci-workflows/sonar-steps.yml +2 -2
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
- package/templates/app/starciwork.gitignore +1 -1
- package/templates/be/image/api/Dockerfile +1 -1
- package/templates/be/image/cli/Dockerfile +1 -1
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
- package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
- package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
- package/templates/fe/image/next/Dockerfile +2 -1
- package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
- package/upgrade/index.mjs +9 -2
- package/runtime/engine/admission.mjs +0 -308
- package/runtime/engine/canonical-json.mjs +0 -10
- package/runtime/engine/db/blob.mjs +0 -315
- package/runtime/engine/db/ledger-paths.mjs +0 -83
- package/runtime/engine/db/ledger.mjs +0 -1118
- package/runtime/engine/db/machine-connection.mjs +0 -135
- package/runtime/engine/db/machine-schema.mjs +0 -84
- package/runtime/engine/db/machine.mjs +0 -1367
- package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
- package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
- package/runtime/engine/db/provider-reservations.mjs +0 -101
- package/runtime/engine/digest.mjs +0 -16
- package/runtime/engine/refuse.mjs +0 -11
- package/runtime/modules/ops/_labels.yaml +0 -53
- package/runtime/scripts/api/node/lib.mjs +0 -14
- package/runtime/scripts/api/node/spawn-node.mjs +0 -6
- package/runtime/scripts/api/process/lib.mjs +0 -111
- package/runtime/scripts/api/process/owned-process.mjs +0 -78
- package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
- package/runtime/scripts/connectors/lib.mjs +0 -488
- package/runtime/scripts/lib/clip.mjs +0 -19
- package/runtime/scripts/lib/display-names.mjs +0 -259
- package/runtime/scripts/lib/example-refs.mjs +0 -158
- package/runtime/scripts/lib/json-schema.mjs +0 -52
- package/runtime/scripts/lib/process-identity.mjs +0 -6
- package/runtime/scripts/lib/read-yaml.mjs +0 -18
- package/runtime/scripts/lib/redact.mjs +0 -161
- package/runtime/scripts/lib/sleep.mjs +0 -7
- package/runtime/scripts/lib/source-phrases.mjs +0 -34
- 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,
|
|
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
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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 }));
|