Skip to content

build(repo): run all dependency code in the sandbox, systemfsoftware packages from Nix - #52

Merged
kiro-systemf[bot] merged 122 commits into
mainfrom
lake1/nix-sandbox
Oct 7, 2026
Merged

kiro-systemf[bot] merged 122 commits into
mainfrom
lake1/nix-sandbox

Conversation

@systemfsoftware-maker

@systemfsoftware-maker systemfsoftware-maker commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Every entry point that runs dependency code now runs inside prm's deny-by-default launcher (sandbox), and every @systemfsoftware/* package the systemfsoftware monorepo publishes comes from its flake, not from npm (Kiro rulings, 2026-10-06). The layer sits on main, below #50, and stays a draft while check:sfs-sources is red (below).

What changes

  • Packages from Nix. The flake takes systemfsoftware at main (locked at 8a4b543) and pnpm-release-management (prm) at main (locked at 537d17c, which carries prm chore(deps): bump the other-minor-patch group across 1 directory with 5 updates #14, chore(deps): bump the other-minor-patch group across 1 directory with 5 updates #20 and chore(deps): bump turbo from 2.10.12 to 2.11.4 in the turborepo-minor-patch group across 1 directory #24) as inputs; systemfsoftware follows the starter's prm. The 18 packages the starter uses from the systemfsoftware monorepo are file:.sfs-deps/*.tgz catalog entries with overrides, and their release-age exclusions are gone.
  • Hashless pnpm store. The launcher's offline store is prm's lib.mkPnpmConsumerStore, given the lockfile, pnpm-workspace.yaml, every package.json and the .sfs-deps tarballs. Every tarball is its own fixed-output fetch keyed by its lockfile integrity, and the starter carries no store hash, so a lockfile change, a Dependabot bump included, needs no hash edit.
  • Everything through the launcher. Package scripts (pnpm dev and pnpm journeys included), the git hooks (from a linked worktree too), dprint's plugins, and every CI job: install, gates, journeys, commitlint, changeset check, mutation, and release. CI jobs install through .github/actions/dev-shell, offline from the Nix store.
  • No host installs. ignoreScripts stops a host pnpm install from running lifecycle scripts, and verifyDepsBeforeRun: warn stops pnpm installing on the host before a script.
  • Linux only (Ryan's standing rule via Kiro, cycle 52). The flake builds Linux systems only (from ci(ci): move mutation to a release gate on main #35). supportedArchitectures resolves optional packages for linux only. The launcher is prm's, unpatched: prm chore(deps): bump turbo from 2.10.12 to 2.11.4 in the turborepo-minor-patch group across 1 directory #24 carries the linked-worktree change the starter used to patch in.
  • The gate this layer waits on. check:sfs-sources fails while any @systemfsoftware/* lockfile entry resolves from the registry. It is red by design: @systemfsoftware/stryker-js 15.0.1 and @systemfsoftware/stryker-js-vitest-runner 8.0.3 resolve from npm until systemfsoftware/stryker-js-effect ships them as flake outputs (stryker-js-effect#196).

Declared under CONST-W3

commitlint.config.ts is an evaluator surface (AGENTS.md), and this layer changes it: stagedFiles no longer turns a failed git diff --cached into an empty list. On main, an index git cannot read passes as "no staged files"; here git's error reaches stderr and commitlint fails. The sandbox proof at sandbox-proofs/git-hooks.test.ts:138 ("commitlint fails with git's error when git cannot read the index") needs it, and with main's version that step fails. Kiro authorized the change (cycle 35, F4 follow-up, restored in b9854fe) and owns the instrument.

Proofs

  • (a) Lockfile change, no hash edit. On the starter's earlier store: in a throwaway worktree, @types/node went from 24.19.1 to 25.9.9 (catalog and lockfile only), the store built, and on a fresh tree the sandboxed pnpm bootstrap installed from the launcher's store view: 660 reused, 0 downloaded. On prm's consumer store: feat(repo): give the site worker one d1 database, emulated locally under pnpm dev #63 adds the site's test toolchain (374 lockfile lines), and pnpm bootstrap builds the store and installs with no hash edit.
  • (b) Tampered lockfile fails. On prm's consumer store at 770abef: one digest changed in error-stack-parser-es's integrity gives hash mismatch in fixed-output derivation '…error-stack-parser-es-1.0.5.tgz.drv', specified sha512-6qu…, got sha512-5qu…, and the store does not build.
  • Host install guard. Fresh tree, before: the host printed its own store (/root/.local/share/pnpm/store/v11) ahead of the bootstrap script. After: a warning, then the install inside the sandbox.
  • Cloud boundary (used by feat(repo): deploy the site from one command #50 and feat(repo): preview every pull request and deploy production after the release gate #51). With fake credentials, bin/cloud reached api.cloudflare.com (400) and a preview's workers.dev host (200), and the launcher refused example.com. Of GH_TOKEN, SECRET_THING and the Cloudflare variables, only the declared ones crossed.

Gate

Gate at e4614c4, clean worktree, Linux, sandboxed:

$ pnpm bootstrap                    # exit 0
$ pnpm check:ci                     # exit 1, only check:sfs-sources:
check-sfs-sources: 2 @systemfsoftware/* package(s) resolve from the npm registry, not the systemfsoftware flake
 Tasks:    8 successful, 8 total   # lint, typecheck, test, build
 Tasks:    1 successful, 1 total   # dist
ok | 1 passed (9 steps) | 0 failed  # sandbox proofs
$ pnpm journeys                     # exit 0
Tests  1 passed (1)

On a local commit with the same tree, nix build of .#pnpm-store, .#sandbox and .#sandbox-proofs exits 0, the dev shell starts, and sandbox -- changeset-management check <merge-base> exits 0 ("no publishable package changed").

CI run 37667193245 on e4614c4: 6 of 7 jobs pass (format, lint, typecheck, test, dist, journeys); check (sfs-sources) fails with the message above. Commitlint (37667193141) and Changeset Check (37667194022) pass.

Found while landing it

  • systemfsoftware#606's tarball build is not pure. With the Nix sandbox off, six tarballs (effect-gherkin-spec, effect-schema-law, effect-schema-vite, effect-spec-runtime, trace-spec, vitest) pack a full CHANGELOG.md the sandboxed build does not see. CI runs sandboxed, so the lockfile records the sandboxed bytes. A host with the sandbox off has to build .#sfs-deps with --option sandbox true.
  • CI cost. Each job builds the store from 1243 per-tarball fetches, with no Nix binary cache. The consumer store fetches every tarball the lockfile names: narrowing supportedArchitectures to Linux left the count at 1243.

Cycles 61 and 62 (conductor rulings)

  • F2 (fbcd048): the Changeset Check caller passes devshell: true, so prm's shared workflow installs through the starter's own bootstrap script instead of a plain pnpm install that cannot read the .sfs-deps tarballs (ERR_PNPM_TARBALL_READ_LOCAL_TARBALL). Declared under CONST-W3: .github/workflows/ is an evaluator surface, edited at the conductor's direction. Changeset Check is green on this layer (37648415465) and on feat(repo): add a guestbook example feature over rpc and d1 #65 (37648460198); both job logs show nix develop --command pnpm run bootstrap running $ sandbox -- pnpm install && sandbox -- pnpm rebuild --config.ignore-scripts=false && sandbox -- pnpm run prepare.
  • F5 (ffd6832): the root mutation script is gone. No workflow called it: the release gate runs turbo run mutation --filter=<package> per package.
  • Re-pin (b3eef8f, cycle 62): systemfsoftware moves from 497dd37 to main 8a4b543 (#659 and #660, the macOS drops). pnpm-release-management followed the rev systemfsoftware main pins (5eb4c5d) until cycle 77 (below). The published tarballs did not change: the sandboxed pnpm bootstrap installs against the unchanged lockfile.

Cycles 74 to 77 (conductor rulings)

@systemfsoftware-maker systemfsoftware-maker changed the title lake1/nix sandbox build(repo): run all dependency code in the sandbox, systemfsoftware packages from Nix Oct 6, 2026
@systemfsoftware-maker
systemfsoftware-maker marked this pull request as ready for review October 6, 2026 19:52
@systemfsoftware-maker
systemfsoftware-maker added this pull request to stack #54 October 6, 2026 19:52
@systemfsoftware-maker
systemfsoftware-maker marked this pull request as draft October 6, 2026 19:52
@systemfsoftware-maker
systemfsoftware-maker removed this pull request from stack #54 October 6, 2026 21:56
@systemfsoftware-maker
systemfsoftware-maker added this pull request to stack #59 October 6, 2026 21:57
@systemfsoftware-maker
systemfsoftware-maker removed this pull request from stack #59 October 6, 2026 22:13
@systemfsoftware-maker
systemfsoftware-maker added this pull request to stack #61 October 6, 2026 22:13
@systemfsoftware-maker
systemfsoftware-maker force-pushed the lake1/nix-sandbox branch 2 times, most recently from dbd6a07 to 0dd6458 Compare October 6, 2026 22:44
@kiro-systemf
kiro-systemf Bot changed the base branch from lake1/csp to main October 7, 2026 17:07
…anagement input

Conductor ruling, cycle 71: since pnpm-release-management#11 (aa712d9), devshell: true runs nix develop --command sandbox -- changeset-management check with the release-tools in the caller's dev shell. The pinned input (5eb4c5d, followed from systemfsoftware main) already exports packages.<system>.release-tools, so the lock does not move
Conductor rulings, cycles 74 and 77: pnpm-release-management main 537d17c carries #14, #20 (the consumer store) and #24 (the linked-worktree change), so the starter's own copy of that change (nix/patches/sandbox-linked-worktree-git.patch) and its applyPatches wiring go. pnpm-release-management becomes a direct input on main, locked to 537d17c; systemfsoftware follows it, and its nixpkgs, comment-checker and importPnpmLock follow the starter's
starter-verify c73: withoutSandboxOnPath removed only the first directory that held sandbox, so with two launchers on PATH the hooks still found one and the refusal step failed falsely. It now drops every PATH entry that provides sandbox. Two launchers on PATH: the old helper fails the step, the new one passes; with the hooks' sandbox prefix stripped, the step fails
Conductor ruling, cycle 83: stryker-js-effect main f06f244 (#196) publishes its 17 members as tarballs with an index.json. The flake adds it as an input (locked to f06f244, following the starter's nixpkgs, comment-checker, importPnpmLock, pnpm-release-management and systemfsoftware), and .sfs-deps now holds both flakes' tarballs with one merged index.json. Every @systemfsoftware/stryker-* member is a file:.sfs-deps catalog entry with an override, the same mechanism as the systemfsoftware packages, plus one for the stryker-js-vm-runner npm alias that stryker-js 17.0.2 declares, so check:sfs-sources passes with no npm-sourced @systemfsoftware/* package
7857171 (lake1/nix-sandbox merged into lake1/deploy) was pushed to lake1/nix-sandbox by mistake during the cycle 83 merge-up. This restores the layer's tree to fb4f8a1 exactly; lake1/deploy reverts this revert so it keeps its own content

@kiro-systemf kiro-systemf Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified: 9/9 green on 6dca2fa incl. sfs-sources (all @systemfsoftware/* from flakes), starter-verify delta met, 0 threads, hunt clean.

@kiro-systemf
kiro-systemf Bot marked this pull request as ready for review October 7, 2026 19:52
@kiro-systemf
kiro-systemf Bot merged commit 7a0ba78 into main Oct 7, 2026
9 checks passed
systemfsoftware-maker added a commit that referenced this pull request Oct 7, 2026
Conductor ruling, cycle 87: the stryker packages come from the stryker-js-effect flake since #52, so the message names both flakes
kiro-systemf Bot pushed a commit that referenced this pull request Oct 7, 2026
* ci(ci): move mutation to a release gate on main

check:ci no longer runs stryker. The release gate on push to main plans one
shard per workspace package that declares a mutation script, refuses an
empty set, and runs each shard at break 100 on the fleet with its
incremental report cached. The checker drops
prioritizePerformanceOverAccuracy, which stryker-js 15 removes. Operator
approval: Kiro, 2026-10-05 (GATE1)

* ci(ci): run the release gate on github-hosted runners

The plan and per-package mutation jobs move from the self-hosted fleet,
which admits only private repositories, to ubuntu-latest. Mutation stays
one parallel job per package.

Operator approval: Kiro, 2026-10-05 (GATE1)

* fix(ci): refuse a mutation shard whose mutate globs match no files

A package with a mutation script whose mutate globs match nothing
reports zero mutants and passes a break-100 threshold vacuously
(review finding #9). Each package now declares its globs once, as
stryker.mutate in package.json; its Stryker config reads them and the
release gate's shard planner expands them. The planner fails red at
plan time naming the package, its directory and its globs when they
match no file, including a package with no declared globs. Its Deno
tests are ordinary tests: a turbo root task, test:scripts, runs them
through the dev shell's deno, and both pnpm test and check:ci run it,
so a local check:ci covers the planner exactly as CI does

* ci(ci): pass the release gate with a notice when there are no decisions to mutate

A decision is a *.workflow.ts file. When no workspace package has one, the planner emits an empty shard list and a 'No decisions to mutate' notice, and the mutation job is skipped. When decisions exist and no package declares a mutation script, it still refuses the empty set, naming how many decisions went unmutated. Kiro ruling, 2026-10-06

* deps(deps): move to effect 4 stable at exact pins

Every catalog entry is an exact version: effect 4.0.1, the
systemfsoftware toolchain on its Effect 4 stable majors (recommended preset
4.0.0, stryker-js 15.0.1, vitest 2.0.0, gherkin-spec 7.0.1) and current
tooling. minimumReleaseAge 1440 is explicit, so the policy is strict;
the exclude list holds the effect name patterns and one exact entry per
resolved systemfsoftware package. oxlint-tsgolint stays 7.0.2001, the
exact peer the presets require. turbo 2.11 writes an agent guidance block
into AGENTS.md unless agentGuidance is false, so turbo.json opts out.
Grader package majors move here because they require Effect 4 stable
(CONST-W3 declared)

* build(repo): move lint, test and mutation settings to the root

oxlint.shared.ts, vitest.shared.ts and stryker.shared.ts hold the house
settings; each package config extends them and declares only its own
mutate set and aliases. Both lint overrides are gone: preset 4.0.0 already
accepts Gherkin step bodies and build-config imports. The root lint file is
named oxlint.shared.ts so lint-staged's nearest-config walk never treats
the repository root as a package. Turbo inputs track the shared files.
Operator approval: Kiro, 2026-10-05 (GATE1)

* build(repo): declare the effect/http unstable-api opt-in once at the root

Root tsconfig.base.json extends the bundler/dom preset and allows exactly
effect/http through the effect language service. effect 4.0.1 ships HTTP
only as unstable and global-fetch-in-effect bans the fetch alternative.
Operator approval: Kiro, 2026-10-05 (GATE1)

* feat(repo): serve the starter site from one worker

One Cloudflare Worker serves the TanStack Start site, defined with Alchemy and run locally by pnpm dev through alchemy dev. The e2e journeys run against that local app

* ci(ci): run the e2e journeys against pnpm dev on linux and macos

bin/journeys starts the app with pnpm dev, runs the journeys against it and stops it with one SIGINT to its process group, so alchemy's local sidecar is not orphaned. The dev port is strict, so a stale server fails the run instead of moving it to another port

* refactor(repo): delete the hello seed package

packages/starter and every trace of it leave in one commit: the README
sections that describe it, the trusted-publisher note in .changeset, and
the catalog entries no remaining package uses (rimraf, tsdown).

npm publishing goes with it. The release job keeps capture, release-note
assertion, tagging and GitHub Releases and drops build, OIDC and pnpm
publish. A version in a manifest with no matching <name>@v<version> tag is
owed; no script queries registry.npmjs.org.
Operator approval: Kiro, 2026-10-05 (release.yml)

* feat(repo): enforce a strict nonce csp with trusted types

The Worker draws a fresh nonce per request, hands it to TanStack Start for its scripts and sends a strict Content-Security-Policy with Trusted Types. A browser journey in Chromium loads the home page under that policy with no violations, and removing the Trusted Types directive fails it

* build(repo): run all dependency code in the sandbox, systemfsoftware packages from nix

Every script that runs dependency code (install, build, test, dev, the journeys and the git hooks) goes through prm's sandbox launcher, systemfsoftware packages come from the systemfsoftware flake as tarballs, and the pnpm store is built from per-tarball fetches. check:sfs-sources stays red on the two Stryker packages, which still resolve from npm until systemfsoftware/stryker-js-effect ships them as flake outputs

* feat(repo): deploy the site from one command

pnpm run deploy deploys the Worker with Alchemy to the adopter's own Cloudflare account through bin/cloud, the one place Cloudflare credentials enter the sandbox; cloud stages keep their state in Cloudflare. A production domain is adopter configuration: SITE_DOMAIN, read by alchemy.run.ts on the prod stage. The template holds no credentials

* ci(ci): pin the shared release tooling to main, as main does

* build(repo): take systemfsoftware from main and pnpm-release-management from its lock

systemfsoftware main 497dd37 fetches each workspace tarball as its own fixed-output derivation, so the tarballs match across linux and darwin; the pre-squash #606 commit hashed pnpm's whole store, which differs on darwin. pnpm-release-management now follows systemfsoftware's lock (5eb4c5d). The sandbox proofs take iplConfigHook and the whole nix tree, as prm's own flake does; the lockfile records the new tarballs' integrity

* build(repo): build the sandbox's pnpm store with pnpm-release-management's consumer store

pnpm 12 checks TLS with the platform verifier, which on macOS refuses mitm-cache's per-build certificate (UnknownIssuer on every registry tarball); prm's mkPnpmConsumerStore replays the fetches over plain HTTP for pnpm 12, still checking each tarball's lockfile integrity. The starter's own store derivation goes

* ci(ci): run the release gate only on pushes to main

starter-verify F10 and F13 (Kiro ruling, cycle 35): drop workflow_dispatch so mutation runs on push to main only; the README no longer claims Cell workflows, Cell.provide, tenant-bound store ports or compile-time phase markers, and no longer tells readers to run pnpm mutation locally

* chore(repo): refuse to run the journeys when port 1337 is already taken

starter-verify F6 (Kiro ruling, cycle 35): a stale server on :1337 would answer the journeys instead of this run's pnpm dev. Proof: with a python http.server on 127.0.0.1:1337, bin/journeys prints the refusal and exits 1; with the port free, pnpm journeys passes 2 of 2

* test(repo): drop the plain-fetch tests from the journeys suite

starter-verify F9 (Kiro ruling, cycle 35): home.integration.test.ts fetched / without a browser, and request-timeout.integration.test.ts only tested the fetch fixture's own deadline. The strict-CSP browser journey already asserts / answers 200, and #62 adds the health journey. Nothing else imports site.fixture.ts, so it goes too. pnpm journeys: 1 of 1 passed; e2e typecheck and lint clean

* chore(repo): restore commitlint.config.ts as main has it

starter-verify F4 (Kiro ruling, cycle 35): commitlint.config.ts is a read-only evaluator surface, so #52 returns it byte-for-byte to main's version. No gate needs the edit: from a linked worktree, the sandboxed commit-msg hook still sees the staged files and refuses a fix(...) commit of this tooling-only diff (type-matches-diff-shape)

* chore(repo): fail commitlint closed when git cannot read the index

Kiro ruling (cycle 35, F4 follow-up), declared per CONST-W3 in #52: commitlint.config.ts is an evaluator surface, and this restores #52's tightening of it that F4 reverted. stagedFiles no longer catches git's failure and returns no files; it lets the error through, so an unreadable index fails the commit instead of passing type-matches-diff-shape as no staged files. The sandbox proof sandbox-proofs/git-hooks.test.ts:135 depends on it; with main's version it fails with 'git failed silently'

* ci(ci): let the macos chromium download follow the chrome for testing redirect

Kiro ruling (cycle 35), declared per CONST-W3 in #52: ci.yml's macOS Playwright install step gains exactly --allow-host storage.googleapis.com. cdn.playwright.dev answers Chrome for Testing builds with a 307 to storage.googleapis.com/chrome-for-testing-public/..., which the sandbox refused, so the download failed with 403 (run 37566060960, job 112614085077) after the macOS dev shell started building

* build(repo): drop darwin from the flake and the toolchain

Ryan's standing rule via Kiro (cycle 52): no macOS anywhere. The flake builds x86_64-linux and aarch64-linux only; dprint pins only Linux archives; the comment-checker sandbox loses its sandbox-exec branch; AGENTS.md says CI is Linux only

* ci(ci): run the journeys on linux only

Ryan's standing rule via Kiro (cycle 52): no macOS legs. The journeys job runs on ubuntu-latest alone

* ci(ci): drop the macos chromium install step

Ryan's standing rule via Kiro (cycle 52): journeys run on Linux only, where Chromium comes from the dev shell

* build(repo): drop darwin from the sandbox patch and supported architectures

Ryan's standing rule via Kiro (cycle 52): the linked-worktree sandbox patch keeps only its bubblewrap hunks (the seatbelt profile hunks go), and pnpm resolves optional packages for linux only. The macOS Playwright install step, with the storage.googleapis.com allowance, left with the merge of #49

* chore(repo): drop the journeys busy-port guard

Conductor ruling, cycle 61, F4: inside the sandbox's network namespace the guard can never fire, and that isolation already keeps a stale host server from answering the journeys

* build(repo): show which tests ran in the turbo test logs

Conductor ruling, cycle 61, F6: the test task's outputLogs is new-only, so CI logs show each executed test

* ci(ci): install for the changeset check through the dev shell's bootstrap

Conductor ruling, cycle 61, F2: the plain install outside Nix cannot read the .sfs-deps tarballs (ERR_PNPM_TARBALL_READ_LOCAL_TARBALL). devshell: true makes prm's shared workflow run the starter's own bootstrap script, which installs through sandbox. Evaluator-surface edit at the conductor's direction (CONST-W3)

* build(repo): drop the root mutation script no workflow calls

Conductor ruling, cycle 61, F5: the release gate runs turbo run mutation per package directly; nothing calls the root pnpm mutation

* deps(deps): move @effect/tsgo to 0.50.0

Conductor ruling, cycle 62: the same bump pnpm-release-management#31 took. effect and every @effect/* stay at 4.0.1; none resolves lower. tsgo 0.50.0 reports nothing new: typecheck and lint are clean uncached. @systemfsoftware/oxlint-config-recommended 4.0.0 still pulls its own @effect/tsgo 0.45.0

* build(repo): take systemfsoftware main 8a4b543

Conductor ruling, cycle 62: systemfsoftware input moves from 497dd37 to main 8a4b543 (#659, #660, the macOS drops). pnpm-release-management still follows the rev systemfsoftware main pins (5eb4c5d). The published tarballs are byte-identical: the sandboxed bootstrap installs against the unchanged lockfile

* build(repo): name the changesets versioning strategy, move @effect/tsgo to 0.51.0

Conductor ruling, cycle 67: prm main (#9, ca932ae) now requires versioning.strategy. The starter versions each workspace package from .changeset intents, so it takes changesets. Followed prm README: "`changesets` versioning drives the changesets libraries per package: the assembled release plan decides each member's bump, workspace dependents move with it, and the consumed intents are removed". No other field is required under that strategy. @effect/tsgo 0.50.0 -> 0.51.0 (npm latest, 2026-10-07T09:37Z); effect stays 4.0.1. Uncached check:ci and journeys pass with no new diagnostics

* build(repo): give the dev shell release-tools from the pnpm-release-management input

Conductor ruling, cycle 71: since pnpm-release-management#11 (aa712d9), devshell: true runs nix develop --command sandbox -- changeset-management check with the release-tools in the caller's dev shell. The pinned input (5eb4c5d, followed from systemfsoftware main) already exports packages.<system>.release-tools, so the lock does not move

* build(repo): take the sandbox from pnpm-release-management unpatched

Conductor rulings, cycles 74 and 77: pnpm-release-management main 537d17c carries #14, #20 (the consumer store) and #24 (the linked-worktree change), so the starter's own copy of that change (nix/patches/sandbox-linked-worktree-git.patch) and its applyPatches wiring go. pnpm-release-management becomes a direct input on main, locked to 537d17c; systemfsoftware follows it, and its nixpkgs, comment-checker and importPnpmLock follow the starter's

* test(repo): drop every sandbox launcher from PATH in the hooks proof

starter-verify c73: withoutSandboxOnPath removed only the first directory that held sandbox, so with two launchers on PATH the hooks still found one and the refusal step failed falsely. It now drops every PATH entry that provides sandbox. Two launchers on PATH: the old helper fails the step, the new one passes; with the hooks' sandbox prefix stripped, the step fails

* build(repo): take the stryker packages from the stryker-js-effect flake

Conductor ruling, cycle 83: stryker-js-effect main f06f244 (#196) publishes its 17 members as tarballs with an index.json. The flake adds it as an input (locked to f06f244, following the starter's nixpkgs, comment-checker, importPnpmLock, pnpm-release-management and systemfsoftware), and .sfs-deps now holds both flakes' tarballs with one merged index.json. Every @systemfsoftware/stryker-* member is a file:.sfs-deps catalog entry with an override, the same mechanism as the systemfsoftware packages, plus one for the stryker-js-vm-runner npm alias that stryker-js 17.0.2 declares, so check:sfs-sources passes with no npm-sourced @systemfsoftware/* package

* revert(repo): take lake1/deploy's merge back off lake1/nix-sandbox

7857171 (lake1/nix-sandbox merged into lake1/deploy) was pushed to lake1/nix-sandbox by mistake during the cycle 83 merge-up. This restores the layer's tree to fb4f8a1 exactly; lake1/deploy reverts this revert so it keeps its own content
kiro-systemf Bot pushed a commit that referenced this pull request Oct 7, 2026
…e release gate (#51)

* ci(ci): move mutation to a release gate on main

check:ci no longer runs stryker. The release gate on push to main plans one
shard per workspace package that declares a mutation script, refuses an
empty set, and runs each shard at break 100 on the fleet with its
incremental report cached. The checker drops
prioritizePerformanceOverAccuracy, which stryker-js 15 removes. Operator
approval: Kiro, 2026-10-05 (GATE1)

* ci(ci): run the release gate on github-hosted runners

The plan and per-package mutation jobs move from the self-hosted fleet,
which admits only private repositories, to ubuntu-latest. Mutation stays
one parallel job per package.

Operator approval: Kiro, 2026-10-05 (GATE1)

* fix(ci): refuse a mutation shard whose mutate globs match no files

A package with a mutation script whose mutate globs match nothing
reports zero mutants and passes a break-100 threshold vacuously
(review finding #9). Each package now declares its globs once, as
stryker.mutate in package.json; its Stryker config reads them and the
release gate's shard planner expands them. The planner fails red at
plan time naming the package, its directory and its globs when they
match no file, including a package with no declared globs. Its Deno
tests are ordinary tests: a turbo root task, test:scripts, runs them
through the dev shell's deno, and both pnpm test and check:ci run it,
so a local check:ci covers the planner exactly as CI does

* ci(ci): pass the release gate with a notice when there are no decisions to mutate

A decision is a *.workflow.ts file. When no workspace package has one, the planner emits an empty shard list and a 'No decisions to mutate' notice, and the mutation job is skipped. When decisions exist and no package declares a mutation script, it still refuses the empty set, naming how many decisions went unmutated. Kiro ruling, 2026-10-06

* deps(deps): move to effect 4 stable at exact pins

Every catalog entry is an exact version: effect 4.0.1, the
systemfsoftware toolchain on its Effect 4 stable majors (recommended preset
4.0.0, stryker-js 15.0.1, vitest 2.0.0, gherkin-spec 7.0.1) and current
tooling. minimumReleaseAge 1440 is explicit, so the policy is strict;
the exclude list holds the effect name patterns and one exact entry per
resolved systemfsoftware package. oxlint-tsgolint stays 7.0.2001, the
exact peer the presets require. turbo 2.11 writes an agent guidance block
into AGENTS.md unless agentGuidance is false, so turbo.json opts out.
Grader package majors move here because they require Effect 4 stable
(CONST-W3 declared)

* build(repo): move lint, test and mutation settings to the root

oxlint.shared.ts, vitest.shared.ts and stryker.shared.ts hold the house
settings; each package config extends them and declares only its own
mutate set and aliases. Both lint overrides are gone: preset 4.0.0 already
accepts Gherkin step bodies and build-config imports. The root lint file is
named oxlint.shared.ts so lint-staged's nearest-config walk never treats
the repository root as a package. Turbo inputs track the shared files.
Operator approval: Kiro, 2026-10-05 (GATE1)

* build(repo): declare the effect/http unstable-api opt-in once at the root

Root tsconfig.base.json extends the bundler/dom preset and allows exactly
effect/http through the effect language service. effect 4.0.1 ships HTTP
only as unstable and global-fetch-in-effect bans the fetch alternative.
Operator approval: Kiro, 2026-10-05 (GATE1)

* feat(repo): serve the starter site from one worker

One Cloudflare Worker serves the TanStack Start site, defined with Alchemy and run locally by pnpm dev through alchemy dev. The e2e journeys run against that local app

* ci(ci): run the e2e journeys against pnpm dev on linux and macos

bin/journeys starts the app with pnpm dev, runs the journeys against it and stops it with one SIGINT to its process group, so alchemy's local sidecar is not orphaned. The dev port is strict, so a stale server fails the run instead of moving it to another port

* feat(repo): preview every pull request and deploy production after the release gate

In an adopter's copy, never the template, a same-repo pull request deploys as its pr-<N> stage, gets its URL in one PR comment, runs the e2e journeys against it and is destroyed when it closes. Main deploys production once every mutation shard passes, to SITE_DOMAIN when that repository variable is set. The deployed journeys run through pnpm journeys:deployed, whose sandbox reaches only the site and gets no Cloudflare credentials

* refactor(repo): delete the hello seed package

packages/starter and every trace of it leave in one commit: the README
sections that describe it, the trusted-publisher note in .changeset, and
the catalog entries no remaining package uses (rimraf, tsdown).

npm publishing goes with it. The release job keeps capture, release-note
assertion, tagging and GitHub Releases and drops build, OIDC and pnpm
publish. A version in a manifest with no matching <name>@v<version> tag is
owed; no script queries registry.npmjs.org.
Operator approval: Kiro, 2026-10-05 (release.yml)

* feat(repo): enforce a strict nonce csp with trusted types

The Worker draws a fresh nonce per request, hands it to TanStack Start for its scripts and sends a strict Content-Security-Policy with Trusted Types. A browser journey in Chromium loads the home page under that policy with no violations, and removing the Trusted Types directive fails it

* build(repo): run all dependency code in the sandbox, systemfsoftware packages from nix

Every script that runs dependency code (install, build, test, dev, the journeys and the git hooks) goes through prm's sandbox launcher, systemfsoftware packages come from the systemfsoftware flake as tarballs, and the pnpm store is built from per-tarball fetches. check:sfs-sources stays red on the two Stryker packages, which still resolve from npm until systemfsoftware/stryker-js-effect ships them as flake outputs

* feat(repo): deploy the site from one command

pnpm run deploy deploys the Worker with Alchemy to the adopter's own Cloudflare account through bin/cloud, the one place Cloudflare credentials enter the sandbox; cloud stages keep their state in Cloudflare. A production domain is adopter configuration: SITE_DOMAIN, read by alchemy.run.ts on the prod stage. The template holds no credentials

* ci(ci): deploy production after a passing or skipped mutation job

With no decisions to mutate the release gate skips the mutation job, which used to skip the deploy that needs it. Production now deploys when the plan passed and the mutation job passed or was skipped, and never when either failed or the run was cancelled

* ci(ci): pin the shared release tooling to main, as main does

* build(repo): take systemfsoftware from main and pnpm-release-management from its lock

systemfsoftware main 497dd37 fetches each workspace tarball as its own fixed-output derivation, so the tarballs match across linux and darwin; the pre-squash #606 commit hashed pnpm's whole store, which differs on darwin. pnpm-release-management now follows systemfsoftware's lock (5eb4c5d). The sandbox proofs take iplConfigHook and the whole nix tree, as prm's own flake does; the lockfile records the new tarballs' integrity

* build(repo): build the sandbox's pnpm store with pnpm-release-management's consumer store

pnpm 12 checks TLS with the platform verifier, which on macOS refuses mitm-cache's per-build certificate (UnknownIssuer on every registry tarball); prm's mkPnpmConsumerStore replays the fetches over plain HTTP for pnpm 12, still checking each tarball's lockfile integrity. The starter's own store derivation goes

* ci(ci): run the release gate only on pushes to main

starter-verify F10 and F13 (Kiro ruling, cycle 35): drop workflow_dispatch so mutation runs on push to main only; the README no longer claims Cell workflows, Cell.provide, tenant-bound store ports or compile-time phase markers, and no longer tells readers to run pnpm mutation locally

* chore(repo): refuse to run the journeys when port 1337 is already taken

starter-verify F6 (Kiro ruling, cycle 35): a stale server on :1337 would answer the journeys instead of this run's pnpm dev. Proof: with a python http.server on 127.0.0.1:1337, bin/journeys prints the refusal and exits 1; with the port free, pnpm journeys passes 2 of 2

* test(repo): drop the plain-fetch tests from the journeys suite

starter-verify F9 (Kiro ruling, cycle 35): home.integration.test.ts fetched / without a browser, and request-timeout.integration.test.ts only tested the fetch fixture's own deadline. The strict-CSP browser journey already asserts / answers 200, and #62 adds the health journey. Nothing else imports site.fixture.ts, so it goes too. pnpm journeys: 1 of 1 passed; e2e typecheck and lint clean

* chore(repo): restore commitlint.config.ts as main has it

starter-verify F4 (Kiro ruling, cycle 35): commitlint.config.ts is a read-only evaluator surface, so #52 returns it byte-for-byte to main's version. No gate needs the edit: from a linked worktree, the sandboxed commit-msg hook still sees the staged files and refuses a fix(...) commit of this tooling-only diff (type-matches-diff-shape)

* ci(ci): stop running the journeys against deployed sites

starter-verify F11 (Kiro ruling, cycle 35): deployed-site journeys are not in the spec, so they come out: bin/journeys-deployed, the journeys:deployed script, the journeys and artifact-upload steps in previews.yml and release-gate.yml (the uploads only carried what those journeys wrote), the README line and the AGENTS.md clause. Deploy, preview comment and destroy are unchanged; the preview job is named for what it does now

* chore(repo): fail commitlint closed when git cannot read the index

Kiro ruling (cycle 35, F4 follow-up), declared per CONST-W3 in #52: commitlint.config.ts is an evaluator surface, and this restores #52's tightening of it that F4 reverted. stagedFiles no longer catches git's failure and returns no files; it lets the error through, so an unreadable index fails the commit instead of passing type-matches-diff-shape as no staged files. The sandbox proof sandbox-proofs/git-hooks.test.ts:135 depends on it; with main's version it fails with 'git failed silently'

* ci(ci): let the macos chromium download follow the chrome for testing redirect

Kiro ruling (cycle 35), declared per CONST-W3 in #52: ci.yml's macOS Playwright install step gains exactly --allow-host storage.googleapis.com. cdn.playwright.dev answers Chrome for Testing builds with a 307 to storage.googleapis.com/chrome-for-testing-public/..., which the sandbox refused, so the download failed with 403 (run 37566060960, job 112614085077) after the macOS dev shell started building

* build(repo): drop darwin from the flake and the toolchain

Ryan's standing rule via Kiro (cycle 52): no macOS anywhere. The flake builds x86_64-linux and aarch64-linux only; dprint pins only Linux archives; the comment-checker sandbox loses its sandbox-exec branch; AGENTS.md says CI is Linux only

* ci(ci): run the journeys on linux only

Ryan's standing rule via Kiro (cycle 52): no macOS legs. The journeys job runs on ubuntu-latest alone

* ci(ci): drop the macos chromium install step

Ryan's standing rule via Kiro (cycle 52): journeys run on Linux only, where Chromium comes from the dev shell

* build(repo): drop darwin from the sandbox patch and supported architectures

Ryan's standing rule via Kiro (cycle 52): the linked-worktree sandbox patch keeps only its bubblewrap hunks (the seatbelt profile hunks go), and pnpm resolves optional packages for linux only. The macOS Playwright install step, with the storage.googleapis.com allowance, left with the merge of #49

* chore(repo): drop the journeys busy-port guard

Conductor ruling, cycle 61, F4: inside the sandbox's network namespace the guard can never fire, and that isolation already keeps a stale host server from answering the journeys

* build(repo): show which tests ran in the turbo test logs

Conductor ruling, cycle 61, F6: the test task's outputLogs is new-only, so CI logs show each executed test

* ci(ci): install for the changeset check through the dev shell's bootstrap

Conductor ruling, cycle 61, F2: the plain install outside Nix cannot read the .sfs-deps tarballs (ERR_PNPM_TARBALL_READ_LOCAL_TARBALL). devshell: true makes prm's shared workflow run the starter's own bootstrap script, which installs through sandbox. Evaluator-surface edit at the conductor's direction (CONST-W3)

* build(repo): drop the root mutation script no workflow calls

Conductor ruling, cycle 61, F5: the release gate runs turbo run mutation per package directly; nothing calls the root pnpm mutation

* deps(deps): move @effect/tsgo to 0.50.0

Conductor ruling, cycle 62: the same bump pnpm-release-management#31 took. effect and every @effect/* stay at 4.0.1; none resolves lower. tsgo 0.50.0 reports nothing new: typecheck and lint are clean uncached. @systemfsoftware/oxlint-config-recommended 4.0.0 still pulls its own @effect/tsgo 0.45.0

* build(repo): take systemfsoftware main 8a4b543

Conductor ruling, cycle 62: systemfsoftware input moves from 497dd37 to main 8a4b543 (#659, #660, the macOS drops). pnpm-release-management still follows the rev systemfsoftware main pins (5eb4c5d). The published tarballs are byte-identical: the sandboxed bootstrap installs against the unchanged lockfile

* build(repo): name the changesets versioning strategy, move @effect/tsgo to 0.51.0

Conductor ruling, cycle 67: prm main (#9, ca932ae) now requires versioning.strategy. The starter versions each workspace package from .changeset intents, so it takes changesets. Followed prm README: "`changesets` versioning drives the changesets libraries per package: the assembled release plan decides each member's bump, workspace dependents move with it, and the consumed intents are removed". No other field is required under that strategy. @effect/tsgo 0.50.0 -> 0.51.0 (npm latest, 2026-10-07T09:37Z); effect stays 4.0.1. Uncached check:ci and journeys pass with no new diagnostics

* build(repo): give the dev shell release-tools from the pnpm-release-management input

Conductor ruling, cycle 71: since pnpm-release-management#11 (aa712d9), devshell: true runs nix develop --command sandbox -- changeset-management check with the release-tools in the caller's dev shell. The pinned input (5eb4c5d, followed from systemfsoftware main) already exports packages.<system>.release-tools, so the lock does not move

* build(repo): take the sandbox from pnpm-release-management unpatched

Conductor rulings, cycles 74 and 77: pnpm-release-management main 537d17c carries #14, #20 (the consumer store) and #24 (the linked-worktree change), so the starter's own copy of that change (nix/patches/sandbox-linked-worktree-git.patch) and its applyPatches wiring go. pnpm-release-management becomes a direct input on main, locked to 537d17c; systemfsoftware follows it, and its nixpkgs, comment-checker and importPnpmLock follow the starter's

* test(repo): drop every sandbox launcher from PATH in the hooks proof

starter-verify c73: withoutSandboxOnPath removed only the first directory that held sandbox, so with two launchers on PATH the hooks still found one and the refusal step failed falsely. It now drops every PATH entry that provides sandbox. Two launchers on PATH: the old helper fails the step, the new one passes; with the hooks' sandbox prefix stripped, the step fails

* build(repo): take the stryker packages from the stryker-js-effect flake

Conductor ruling, cycle 83: stryker-js-effect main f06f244 (#196) publishes its 17 members as tarballs with an index.json. The flake adds it as an input (locked to f06f244, following the starter's nixpkgs, comment-checker, importPnpmLock, pnpm-release-management and systemfsoftware), and .sfs-deps now holds both flakes' tarballs with one merged index.json. Every @systemfsoftware/stryker-* member is a file:.sfs-deps catalog entry with an override, the same mechanism as the systemfsoftware packages, plus one for the stryker-js-vm-runner npm alias that stryker-js 17.0.2 declares, so check:sfs-sources passes with no npm-sourced @systemfsoftware/* package

* revert(repo): take lake1/deploy's merge back off lake1/nix-sandbox

7857171 (lake1/nix-sandbox merged into lake1/deploy) was pushed to lake1/nix-sandbox by mistake during the cycle 83 merge-up. This restores the layer's tree to fb4f8a1 exactly; lake1/deploy reverts this revert so it keeps its own content
kiro-systemf Bot pushed a commit that referenced this pull request Oct 7, 2026
* ci(ci): move mutation to a release gate on main

check:ci no longer runs stryker. The release gate on push to main plans one
shard per workspace package that declares a mutation script, refuses an
empty set, and runs each shard at break 100 on the fleet with its
incremental report cached. The checker drops
prioritizePerformanceOverAccuracy, which stryker-js 15 removes. Operator
approval: Kiro, 2026-10-05 (GATE1)

* ci(ci): run the release gate on github-hosted runners

The plan and per-package mutation jobs move from the self-hosted fleet,
which admits only private repositories, to ubuntu-latest. Mutation stays
one parallel job per package.

Operator approval: Kiro, 2026-10-05 (GATE1)

* fix(ci): refuse a mutation shard whose mutate globs match no files

A package with a mutation script whose mutate globs match nothing
reports zero mutants and passes a break-100 threshold vacuously
(review finding #9). Each package now declares its globs once, as
stryker.mutate in package.json; its Stryker config reads them and the
release gate's shard planner expands them. The planner fails red at
plan time naming the package, its directory and its globs when they
match no file, including a package with no declared globs. Its Deno
tests are ordinary tests: a turbo root task, test:scripts, runs them
through the dev shell's deno, and both pnpm test and check:ci run it,
so a local check:ci covers the planner exactly as CI does

* ci(ci): pass the release gate with a notice when there are no decisions to mutate

A decision is a *.workflow.ts file. When no workspace package has one, the planner emits an empty shard list and a 'No decisions to mutate' notice, and the mutation job is skipped. When decisions exist and no package declares a mutation script, it still refuses the empty set, naming how many decisions went unmutated. Kiro ruling, 2026-10-06

* deps(deps): move to effect 4 stable at exact pins

Every catalog entry is an exact version: effect 4.0.1, the
systemfsoftware toolchain on its Effect 4 stable majors (recommended preset
4.0.0, stryker-js 15.0.1, vitest 2.0.0, gherkin-spec 7.0.1) and current
tooling. minimumReleaseAge 1440 is explicit, so the policy is strict;
the exclude list holds the effect name patterns and one exact entry per
resolved systemfsoftware package. oxlint-tsgolint stays 7.0.2001, the
exact peer the presets require. turbo 2.11 writes an agent guidance block
into AGENTS.md unless agentGuidance is false, so turbo.json opts out.
Grader package majors move here because they require Effect 4 stable
(CONST-W3 declared)

* build(repo): move lint, test and mutation settings to the root

oxlint.shared.ts, vitest.shared.ts and stryker.shared.ts hold the house
settings; each package config extends them and declares only its own
mutate set and aliases. Both lint overrides are gone: preset 4.0.0 already
accepts Gherkin step bodies and build-config imports. The root lint file is
named oxlint.shared.ts so lint-staged's nearest-config walk never treats
the repository root as a package. Turbo inputs track the shared files.
Operator approval: Kiro, 2026-10-05 (GATE1)

* build(repo): declare the effect/http unstable-api opt-in once at the root

Root tsconfig.base.json extends the bundler/dom preset and allows exactly
effect/http through the effect language service. effect 4.0.1 ships HTTP
only as unstable and global-fetch-in-effect bans the fetch alternative.
Operator approval: Kiro, 2026-10-05 (GATE1)

* feat(repo): serve the starter site from one worker

One Cloudflare Worker serves the TanStack Start site, defined with Alchemy and run locally by pnpm dev through alchemy dev. The e2e journeys run against that local app

* ci(ci): run the e2e journeys against pnpm dev on linux and macos

bin/journeys starts the app with pnpm dev, runs the journeys against it and stops it with one SIGINT to its process group, so alchemy's local sidecar is not orphaned. The dev port is strict, so a stale server fails the run instead of moving it to another port

* feat(repo): serve an effect httpapi and its openapi document from the site worker

The site Worker routes /api/ to an Effect HttpApi: GET /api/health answers {"status":"ok"}, and /api/openapi.json serves the OpenAPI 3.1 document HttpApiBuilder generates. Every other path still goes to TanStack Start under the strict CSP. The root unstable-API opt-in gains exactly effect/http-api, where Effect 4.0.1 ships HttpApi (Kiro ruling, 2026-10-06). A journey reads the API description and calls the health check against pnpm dev

* feat(repo): preview every pull request and deploy production after the release gate

In an adopter's copy, never the template, a same-repo pull request deploys as its pr-<N> stage, gets its URL in one PR comment, runs the e2e journeys against it and is destroyed when it closes. Main deploys production once every mutation shard passes, to SITE_DOMAIN when that repository variable is set. The deployed journeys run through pnpm journeys:deployed, whose sandbox reaches only the site and gets no Cloudflare credentials

* feat(repo): call the site worker through effect rpc instead of httpapi

Ryan ruling via Kiro 2026-10-07: one RpcGroup served by RpcServer over HTTP at /api/rpc, a typed RpcClient in the page; a Health procedure replaces /api/health and the OpenAPI document goes. The home page shows the Worker's health and a browser journey reads it. The unstable-API opt-in lists effect/rpc instead of effect/http-api

* refactor(repo): delete the hello seed package

packages/starter and every trace of it leave in one commit: the README
sections that describe it, the trusted-publisher note in .changeset, and
the catalog entries no remaining package uses (rimraf, tsdown).

npm publishing goes with it. The release job keeps capture, release-note
assertion, tagging and GitHub Releases and drops build, OIDC and pnpm
publish. A version in a manifest with no matching <name>@v<version> tag is
owed; no script queries registry.npmjs.org.
Operator approval: Kiro, 2026-10-05 (release.yml)

* feat(repo): enforce a strict nonce csp with trusted types

The Worker draws a fresh nonce per request, hands it to TanStack Start for its scripts and sends a strict Content-Security-Policy with Trusted Types. A browser journey in Chromium loads the home page under that policy with no violations, and removing the Trusted Types directive fails it

* build(repo): run all dependency code in the sandbox, systemfsoftware packages from nix

Every script that runs dependency code (install, build, test, dev, the journeys and the git hooks) goes through prm's sandbox launcher, systemfsoftware packages come from the systemfsoftware flake as tarballs, and the pnpm store is built from per-tarball fetches. check:sfs-sources stays red on the two Stryker packages, which still resolve from npm until systemfsoftware/stryker-js-effect ships them as flake outputs

* feat(repo): deploy the site from one command

pnpm run deploy deploys the Worker with Alchemy to the adopter's own Cloudflare account through bin/cloud, the one place Cloudflare credentials enter the sandbox; cloud stages keep their state in Cloudflare. A production domain is adopter configuration: SITE_DOMAIN, read by alchemy.run.ts on the prod stage. The template holds no credentials

* ci(ci): deploy production after a passing or skipped mutation job

With no decisions to mutate the release gate skips the mutation job, which used to skip the deploy that needs it. Production now deploys when the plan passed and the mutation job passed or was skipped, and never when either failed or the run was cancelled

* refactor(repo): name the site's procedure health

* ci(ci): pin the shared release tooling to main, as main does

* build(repo): take systemfsoftware from main and pnpm-release-management from its lock

systemfsoftware main 497dd37 fetches each workspace tarball as its own fixed-output derivation, so the tarballs match across linux and darwin; the pre-squash #606 commit hashed pnpm's whole store, which differs on darwin. pnpm-release-management now follows systemfsoftware's lock (5eb4c5d). The sandbox proofs take iplConfigHook and the whole nix tree, as prm's own flake does; the lockfile records the new tarballs' integrity

* build(repo): build the sandbox's pnpm store with pnpm-release-management's consumer store

pnpm 12 checks TLS with the platform verifier, which on macOS refuses mitm-cache's per-build certificate (UnknownIssuer on every registry tarball); prm's mkPnpmConsumerStore replays the fetches over plain HTTP for pnpm 12, still checking each tarball's lockfile integrity. The starter's own store derivation goes

* ci(ci): run the release gate only on pushes to main

starter-verify F10 and F13 (Kiro ruling, cycle 35): drop workflow_dispatch so mutation runs on push to main only; the README no longer claims Cell workflows, Cell.provide, tenant-bound store ports or compile-time phase markers, and no longer tells readers to run pnpm mutation locally

* chore(repo): refuse to run the journeys when port 1337 is already taken

starter-verify F6 (Kiro ruling, cycle 35): a stale server on :1337 would answer the journeys instead of this run's pnpm dev. Proof: with a python http.server on 127.0.0.1:1337, bin/journeys prints the refusal and exits 1; with the port free, pnpm journeys passes 2 of 2

* test(repo): drop the plain-fetch tests from the journeys suite

starter-verify F9 (Kiro ruling, cycle 35): home.integration.test.ts fetched / without a browser, and request-timeout.integration.test.ts only tested the fetch fixture's own deadline. The strict-CSP browser journey already asserts / answers 200, and #62 adds the health journey. Nothing else imports site.fixture.ts, so it goes too. pnpm journeys: 1 of 1 passed; e2e typecheck and lint clean

* chore(repo): restore commitlint.config.ts as main has it

starter-verify F4 (Kiro ruling, cycle 35): commitlint.config.ts is a read-only evaluator surface, so #52 returns it byte-for-byte to main's version. No gate needs the edit: from a linked worktree, the sandboxed commit-msg hook still sees the staged files and refuses a fix(...) commit of this tooling-only diff (type-matches-diff-shape)

* ci(ci): stop running the journeys against deployed sites

starter-verify F11 (Kiro ruling, cycle 35): deployed-site journeys are not in the spec, so they come out: bin/journeys-deployed, the journeys:deployed script, the journeys and artifact-upload steps in previews.yml and release-gate.yml (the uploads only carried what those journeys wrote), the README line and the AGENTS.md clause. Deploy, preview comment and destroy are unchanged; the preview job is named for what it does now

* fix(repo): send every rpc call to the served path in one round trip

starter-verify F12 (Kiro ruling, cycle 35): RpcClient.layerProtocolHttp prepends its url to the protocol's empty request path, and joining '/api/rpc' with '' gives '/api/rpc/'. The Worker serves '/api/rpc' exactly, so every call took a 307 to the served path. The client now builds the protocol with makeProtocolHttp over a client whose requests are set to SITE_RPC_PATH. Probe (Chromium against pnpm dev, every /api/rpc* request and response on a home-page load): before, POST /api/rpc/ -> 307 -> POST /api/rpc -> 200 per call; after, POST /api/rpc -> 200. Site typecheck and lint clean; pnpm journeys green

* chore(repo): fail commitlint closed when git cannot read the index

Kiro ruling (cycle 35, F4 follow-up), declared per CONST-W3 in #52: commitlint.config.ts is an evaluator surface, and this restores #52's tightening of it that F4 reverted. stagedFiles no longer catches git's failure and returns no files; it lets the error through, so an unreadable index fails the commit instead of passing type-matches-diff-shape as no staged files. The sandbox proof sandbox-proofs/git-hooks.test.ts:135 depends on it; with main's version it fails with 'git failed silently'

* ci(ci): let the macos chromium download follow the chrome for testing redirect

Kiro ruling (cycle 35), declared per CONST-W3 in #52: ci.yml's macOS Playwright install step gains exactly --allow-host storage.googleapis.com. cdn.playwright.dev answers Chrome for Testing builds with a 307 to storage.googleapis.com/chrome-for-testing-public/..., which the sandbox refused, so the download failed with 403 (run 37566060960, job 112614085077) after the macOS dev shell started building

* build(repo): drop darwin from the flake and the toolchain

Ryan's standing rule via Kiro (cycle 52): no macOS anywhere. The flake builds x86_64-linux and aarch64-linux only; dprint pins only Linux archives; the comment-checker sandbox loses its sandbox-exec branch; AGENTS.md says CI is Linux only

* ci(ci): run the journeys on linux only

Ryan's standing rule via Kiro (cycle 52): no macOS legs. The journeys job runs on ubuntu-latest alone

* ci(ci): drop the macos chromium install step

Ryan's standing rule via Kiro (cycle 52): journeys run on Linux only, where Chromium comes from the dev shell

* build(repo): drop darwin from the sandbox patch and supported architectures

Ryan's standing rule via Kiro (cycle 52): the linked-worktree sandbox patch keeps only its bubblewrap hunks (the seatbelt profile hunks go), and pnpm resolves optional packages for linux only. The macOS Playwright install step, with the storage.googleapis.com allowance, left with the merge of #49

* chore(repo): drop the journeys busy-port guard

Conductor ruling, cycle 61, F4: inside the sandbox's network namespace the guard can never fire, and that isolation already keeps a stale host server from answering the journeys

* build(repo): show which tests ran in the turbo test logs

Conductor ruling, cycle 61, F6: the test task's outputLogs is new-only, so CI logs show each executed test

* ci(ci): install for the changeset check through the dev shell's bootstrap

Conductor ruling, cycle 61, F2: the plain install outside Nix cannot read the .sfs-deps tarballs (ERR_PNPM_TARBALL_READ_LOCAL_TARBALL). devshell: true makes prm's shared workflow run the starter's own bootstrap script, which installs through sandbox. Evaluator-surface edit at the conductor's direction (CONST-W3)

* build(repo): drop the root mutation script no workflow calls

Conductor ruling, cycle 61, F5: the release gate runs turbo run mutation per package directly; nothing calls the root pnpm mutation

* deps(deps): move @effect/tsgo to 0.50.0

Conductor ruling, cycle 62: the same bump pnpm-release-management#31 took. effect and every @effect/* stay at 4.0.1; none resolves lower. tsgo 0.50.0 reports nothing new: typecheck and lint are clean uncached. @systemfsoftware/oxlint-config-recommended 4.0.0 still pulls its own @effect/tsgo 0.45.0

* build(repo): take systemfsoftware main 8a4b543

Conductor ruling, cycle 62: systemfsoftware input moves from 497dd37 to main 8a4b543 (#659, #660, the macOS drops). pnpm-release-management still follows the rev systemfsoftware main pins (5eb4c5d). The published tarballs are byte-identical: the sandboxed bootstrap installs against the unchanged lockfile

* build(repo): name the changesets versioning strategy, move @effect/tsgo to 0.51.0

Conductor ruling, cycle 67: prm main (#9, ca932ae) now requires versioning.strategy. The starter versions each workspace package from .changeset intents, so it takes changesets. Followed prm README: "`changesets` versioning drives the changesets libraries per package: the assembled release plan decides each member's bump, workspace dependents move with it, and the consumed intents are removed". No other field is required under that strategy. @effect/tsgo 0.50.0 -> 0.51.0 (npm latest, 2026-10-07T09:37Z); effect stays 4.0.1. Uncached check:ci and journeys pass with no new diagnostics

* build(repo): give the dev shell release-tools from the pnpm-release-management input

Conductor ruling, cycle 71: since pnpm-release-management#11 (aa712d9), devshell: true runs nix develop --command sandbox -- changeset-management check with the release-tools in the caller's dev shell. The pinned input (5eb4c5d, followed from systemfsoftware main) already exports packages.<system>.release-tools, so the lock does not move

* build(repo): take the sandbox from pnpm-release-management unpatched

Conductor rulings, cycles 74 and 77: pnpm-release-management main 537d17c carries #14, #20 (the consumer store) and #24 (the linked-worktree change), so the starter's own copy of that change (nix/patches/sandbox-linked-worktree-git.patch) and its applyPatches wiring go. pnpm-release-management becomes a direct input on main, locked to 537d17c; systemfsoftware follows it, and its nixpkgs, comment-checker and importPnpmLock follow the starter's

* test(repo): drop every sandbox launcher from PATH in the hooks proof

starter-verify c73: withoutSandboxOnPath removed only the first directory that held sandbox, so with two launchers on PATH the hooks still found one and the refusal step failed falsely. It now drops every PATH entry that provides sandbox. Two launchers on PATH: the old helper fails the step, the new one passes; with the hooks' sandbox prefix stripped, the step fails

* build(repo): take the stryker packages from the stryker-js-effect flake

Conductor ruling, cycle 83: stryker-js-effect main f06f244 (#196) publishes its 17 members as tarballs with an index.json. The flake adds it as an input (locked to f06f244, following the starter's nixpkgs, comment-checker, importPnpmLock, pnpm-release-management and systemfsoftware), and .sfs-deps now holds both flakes' tarballs with one merged index.json. Every @systemfsoftware/stryker-* member is a file:.sfs-deps catalog entry with an override, the same mechanism as the systemfsoftware packages, plus one for the stryker-js-vm-runner npm alias that stryker-js 17.0.2 declares, so check:sfs-sources passes with no npm-sourced @systemfsoftware/* package

* revert(repo): take lake1/deploy's merge back off lake1/nix-sandbox

7857171 (lake1/nix-sandbox merged into lake1/deploy) was pushed to lake1/nix-sandbox by mistake during the cycle 83 merge-up. This restores the layer's tree to fb4f8a1 exactly; lake1/deploy reverts this revert so it keeps its own content
kiro-systemf Bot pushed a commit that referenced this pull request Oct 7, 2026
…der pnpm dev (#63)

* ci(ci): move mutation to a release gate on main

check:ci no longer runs stryker. The release gate on push to main plans one
shard per workspace package that declares a mutation script, refuses an
empty set, and runs each shard at break 100 on the fleet with its
incremental report cached. The checker drops
prioritizePerformanceOverAccuracy, which stryker-js 15 removes. Operator
approval: Kiro, 2026-10-05 (GATE1)

* ci(ci): run the release gate on github-hosted runners

The plan and per-package mutation jobs move from the self-hosted fleet,
which admits only private repositories, to ubuntu-latest. Mutation stays
one parallel job per package.

Operator approval: Kiro, 2026-10-05 (GATE1)

* fix(ci): refuse a mutation shard whose mutate globs match no files

A package with a mutation script whose mutate globs match nothing
reports zero mutants and passes a break-100 threshold vacuously
(review finding #9). Each package now declares its globs once, as
stryker.mutate in package.json; its Stryker config reads them and the
release gate's shard planner expands them. The planner fails red at
plan time naming the package, its directory and its globs when they
match no file, including a package with no declared globs. Its Deno
tests are ordinary tests: a turbo root task, test:scripts, runs them
through the dev shell's deno, and both pnpm test and check:ci run it,
so a local check:ci covers the planner exactly as CI does

* ci(ci): pass the release gate with a notice when there are no decisions to mutate

A decision is a *.workflow.ts file. When no workspace package has one, the planner emits an empty shard list and a 'No decisions to mutate' notice, and the mutation job is skipped. When decisions exist and no package declares a mutation script, it still refuses the empty set, naming how many decisions went unmutated. Kiro ruling, 2026-10-06

* deps(deps): move to effect 4 stable at exact pins

Every catalog entry is an exact version: effect 4.0.1, the
systemfsoftware toolchain on its Effect 4 stable majors (recommended preset
4.0.0, stryker-js 15.0.1, vitest 2.0.0, gherkin-spec 7.0.1) and current
tooling. minimumReleaseAge 1440 is explicit, so the policy is strict;
the exclude list holds the effect name patterns and one exact entry per
resolved systemfsoftware package. oxlint-tsgolint stays 7.0.2001, the
exact peer the presets require. turbo 2.11 writes an agent guidance block
into AGENTS.md unless agentGuidance is false, so turbo.json opts out.
Grader package majors move here because they require Effect 4 stable
(CONST-W3 declared)

* build(repo): move lint, test and mutation settings to the root

oxlint.shared.ts, vitest.shared.ts and stryker.shared.ts hold the house
settings; each package config extends them and declares only its own
mutate set and aliases. Both lint overrides are gone: preset 4.0.0 already
accepts Gherkin step bodies and build-config imports. The root lint file is
named oxlint.shared.ts so lint-staged's nearest-config walk never treats
the repository root as a package. Turbo inputs track the shared files.
Operator approval: Kiro, 2026-10-05 (GATE1)

* build(repo): declare the effect/http unstable-api opt-in once at the root

Root tsconfig.base.json extends the bundler/dom preset and allows exactly
effect/http through the effect language service. effect 4.0.1 ships HTTP
only as unstable and global-fetch-in-effect bans the fetch alternative.
Operator approval: Kiro, 2026-10-05 (GATE1)

* feat(repo): serve the starter site from one worker

One Cloudflare Worker serves the TanStack Start site, defined with Alchemy and run locally by pnpm dev through alchemy dev. The e2e journeys run against that local app

* ci(ci): run the e2e journeys against pnpm dev on linux and macos

bin/journeys starts the app with pnpm dev, runs the journeys against it and stops it with one SIGINT to its process group, so alchemy's local sidecar is not orphaned. The dev port is strict, so a stale server fails the run instead of moving it to another port

* feat(repo): serve an effect httpapi and its openapi document from the site worker

The site Worker routes /api/ to an Effect HttpApi: GET /api/health answers {"status":"ok"}, and /api/openapi.json serves the OpenAPI 3.1 document HttpApiBuilder generates. Every other path still goes to TanStack Start under the strict CSP. The root unstable-API opt-in gains exactly effect/http-api, where Effect 4.0.1 ships HttpApi (Kiro ruling, 2026-10-06). A journey reads the API description and calls the health check against pnpm dev

* feat(repo): give the site worker one d1 database, emulated locally under pnpm dev

alchemy.run.ts declares a D1 database and binds it to the Worker as DB. alchemy dev creates and serves it locally with no cloud account, and each cloud stage gets its own. The Worker reads the binding from cloudflare:workers, typed through Alchemy's InferEnv of the Site, so a binding renamed on one side fails the typecheck. The Health procedure now answers ok only when the database answers SELECT 1, and the home page shows unreachable when it does not. Queries go through the D1 binding itself, with no effect/sql or @effect/sql-d1

* feat(repo): preview every pull request and deploy production after the release gate

In an adopter's copy, never the template, a same-repo pull request deploys as its pr-<N> stage, gets its URL in one PR comment, runs the e2e journeys against it and is destroyed when it closes. Main deploys production once every mutation shard passes, to SITE_DOMAIN when that repository variable is set. The deployed journeys run through pnpm journeys:deployed, whose sandbox reaches only the site and gets no Cloudflare credentials

* feat(repo): call the site worker through effect rpc instead of httpapi

Ryan ruling via Kiro 2026-10-07: one RpcGroup served by RpcServer over HTTP at /api/rpc, a typed RpcClient in the page; a Health procedure replaces /api/health and the OpenAPI document goes. The home page shows the Worker's health and a browser journey reads it. The unstable-API opt-in lists effect/rpc instead of effect/http-api

* docs(repo): name the token rights the d1 deploy needs

* refactor(repo): delete the hello seed package

packages/starter and every trace of it leave in one commit: the README
sections that describe it, the trusted-publisher note in .changeset, and
the catalog entries no remaining package uses (rimraf, tsdown).

npm publishing goes with it. The release job keeps capture, release-note
assertion, tagging and GitHub Releases and drops build, OIDC and pnpm
publish. A version in a manifest with no matching <name>@v<version> tag is
owed; no script queries registry.npmjs.org.
Operator approval: Kiro, 2026-10-05 (release.yml)

* feat(repo): enforce a strict nonce csp with trusted types

The Worker draws a fresh nonce per request, hands it to TanStack Start for its scripts and sends a strict Content-Security-Policy with Trusted Types. A browser journey in Chromium loads the home page under that policy with no violations, and removing the Trusted Types directive fails it

* build(repo): run all dependency code in the sandbox, systemfsoftware packages from nix

Every script that runs dependency code (install, build, test, dev, the journeys and the git hooks) goes through prm's sandbox launcher, systemfsoftware packages come from the systemfsoftware flake as tarballs, and the pnpm store is built from per-tarball fetches. check:sfs-sources stays red on the two Stryker packages, which still resolve from npm until systemfsoftware/stryker-js-effect ships them as flake outputs

* feat(repo): deploy the site from one command

pnpm run deploy deploys the Worker with Alchemy to the adopter's own Cloudflare account through bin/cloud, the one place Cloudflare credentials enter the sandbox; cloud stages keep their state in Cloudflare. A production domain is adopter configuration: SITE_DOMAIN, read by alchemy.run.ts on the prod stage. The template holds no credentials

* ci(ci): deploy production after a passing or skipped mutation job

With no decisions to mutate the release gate skips the mutation job, which used to skip the deploy that needs it. Production now deploys when the plan passed and the mutation job passed or was skipped, and never when either failed or the run was cancelled

* refactor(repo): name the site's procedure health

* ci(ci): pin the shared release tooling to main, as main does

* build(repo): take systemfsoftware from main and pnpm-release-management from its lock

systemfsoftware main 497dd37 fetches each workspace tarball as its own fixed-output derivation, so the tarballs match across linux and darwin; the pre-squash #606 commit hashed pnpm's whole store, which differs on darwin. pnpm-release-management now follows systemfsoftware's lock (5eb4c5d). The sandbox proofs take iplConfigHook and the whole nix tree, as prm's own flake does; the lockfile records the new tarballs' integrity

* build(repo): build the sandbox's pnpm store with pnpm-release-management's consumer store

pnpm 12 checks TLS with the platform verifier, which on macOS refuses mitm-cache's per-build certificate (UnknownIssuer on every registry tarball); prm's mkPnpmConsumerStore replays the fetches over plain HTTP for pnpm 12, still checking each tarball's lockfile integrity. The starter's own store derivation goes

* ci(ci): run the release gate only on pushes to main

starter-verify F10 and F13 (Kiro ruling, cycle 35): drop workflow_dispatch so mutation runs on push to main only; the README no longer claims Cell workflows, Cell.provide, tenant-bound store ports or compile-time phase markers, and no longer tells readers to run pnpm mutation locally

* chore(repo): refuse to run the journeys when port 1337 is already taken

starter-verify F6 (Kiro ruling, cycle 35): a stale server on :1337 would answer the journeys instead of this run's pnpm dev. Proof: with a python http.server on 127.0.0.1:1337, bin/journeys prints the refusal and exits 1; with the port free, pnpm journeys passes 2 of 2

* test(repo): drop the plain-fetch tests from the journeys suite

starter-verify F9 (Kiro ruling, cycle 35): home.integration.test.ts fetched / without a browser, and request-timeout.integration.test.ts only tested the fetch fixture's own deadline. The strict-CSP browser journey already asserts / answers 200, and #62 adds the health journey. Nothing else imports site.fixture.ts, so it goes too. pnpm journeys: 1 of 1 passed; e2e typecheck and lint clean

* chore(repo): restore commitlint.config.ts as main has it

starter-verify F4 (Kiro ruling, cycle 35): commitlint.config.ts is a read-only evaluator surface, so #52 returns it byte-for-byte to main's version. No gate needs the edit: from a linked worktree, the sandboxed commit-msg hook still sees the staged files and refuses a fix(...) commit of this tooling-only diff (type-matches-diff-shape)

* ci(ci): stop running the journeys against deployed sites

starter-verify F11 (Kiro ruling, cycle 35): deployed-site journeys are not in the spec, so they come out: bin/journeys-deployed, the journeys:deployed script, the journeys and artifact-upload steps in previews.yml and release-gate.yml (the uploads only carried what those journeys wrote), the README line and the AGENTS.md clause. Deploy, preview comment and destroy are unchanged; the preview job is named for what it does now

* fix(repo): send every rpc call to the served path in one round trip

starter-verify F12 (Kiro ruling, cycle 35): RpcClient.layerProtocolHttp prepends its url to the protocol's empty request path, and joining '/api/rpc' with '' gives '/api/rpc/'. The Worker serves '/api/rpc' exactly, so every call took a 307 to the served path. The client now builds the protocol with makeProtocolHttp over a client whose requests are set to SITE_RPC_PATH. Probe (Chromium against pnpm dev, every /api/rpc* request and response on a home-page load): before, POST /api/rpc/ -> 307 -> POST /api/rpc -> 200 per call; after, POST /api/rpc -> 200. Site typecheck and lint clean; pnpm journeys green

* feat(repo): decide the worker's health in a pure workflow the site tests

Kiro ruling (cycle 35, F5 follow-up): the site needs a test of its own that survives the guestbook's removal. check-health.workflow.ts is a Workflow.make decision from the D1 probe outcome to Healthy or the DatabaseUnreachable refusal, which becomes the health procedure's typed error; the handler only runs the probe and calls it. __tests__/check-health.workflow.property.test.ts states the law: healthy exactly when the probe answered. Sabotage: flipping the decision fails it (shrunk to ProbeUnanswered). The site's test and mutation toolchain (vitest, the Stryker set, effect-cell-types, configs) moves here from the guestbook layer with the first workflow, and the release-gate planner now plans @endgame/site

* chore(repo): fail commitlint closed when git cannot read the index

Kiro ruling (cycle 35, F4 follow-up), declared per CONST-W3 in #52: commitlint.config.ts is an evaluator surface, and this restores #52's tightening of it that F4 reverted. stagedFiles no longer catches git's failure and returns no files; it lets the error through, so an unreadable index fails the commit instead of passing type-matches-diff-shape as no staged files. The sandbox proof sandbox-proofs/git-hooks.test.ts:135 depends on it; with main's version it fails with 'git failed silently'

* ci(ci): let the macos chromium download follow the chrome for testing redirect

Kiro ruling (cycle 35), declared per CONST-W3 in #52: ci.yml's macOS Playwright install step gains exactly --allow-host storage.googleapis.com. cdn.playwright.dev answers Chrome for Testing builds with a 307 to storage.googleapis.com/chrome-for-testing-public/..., which the sandbox refused, so the download failed with 403 (run 37566060960, job 112614085077) after the macOS dev shell started building

* build(repo): drop darwin from the flake and the toolchain

Ryan's standing rule via Kiro (cycle 52): no macOS anywhere. The flake builds x86_64-linux and aarch64-linux only; dprint pins only Linux archives; the comment-checker sandbox loses its sandbox-exec branch; AGENTS.md says CI is Linux only

* ci(ci): run the journeys on linux only

Ryan's standing rule via Kiro (cycle 52): no macOS legs. The journeys job runs on ubuntu-latest alone

* ci(ci): drop the macos chromium install step

Ryan's standing rule via Kiro (cycle 52): journeys run on Linux only, where Chromium comes from the dev shell

* build(repo): drop darwin from the sandbox patch and supported architectures

Ryan's standing rule via Kiro (cycle 52): the linked-worktree sandbox patch keeps only its bubblewrap hunks (the seatbelt profile hunks go), and pnpm resolves optional packages for linux only. The macOS Playwright install step, with the storage.googleapis.com allowance, left with the merge of #49

* chore(repo): drop the journeys busy-port guard

Conductor ruling, cycle 61, F4: inside the sandbox's network namespace the guard can never fire, and that isolation already keeps a stale host server from answering the journeys

* build(repo): show which tests ran in the turbo test logs

Conductor ruling, cycle 61, F6: the test task's outputLogs is new-only, so CI logs show each executed test

* ci(ci): install for the changeset check through the dev shell's bootstrap

Conductor ruling, cycle 61, F2: the plain install outside Nix cannot read the .sfs-deps tarballs (ERR_PNPM_TARBALL_READ_LOCAL_TARBALL). devshell: true makes prm's shared workflow run the starter's own bootstrap script, which installs through sandbox. Evaluator-surface edit at the conductor's direction (CONST-W3)

* build(repo): drop the root mutation script no workflow calls

Conductor ruling, cycle 61, F5: the release gate runs turbo run mutation per package directly; nothing calls the root pnpm mutation

* deps(deps): move @effect/tsgo to 0.50.0

Conductor ruling, cycle 62: the same bump pnpm-release-management#31 took. effect and every @effect/* stay at 4.0.1; none resolves lower. tsgo 0.50.0 reports nothing new: typecheck and lint are clean uncached. @systemfsoftware/oxlint-config-recommended 4.0.0 still pulls its own @effect/tsgo 0.45.0

* build(repo): take systemfsoftware main 8a4b543

Conductor ruling, cycle 62: systemfsoftware input moves from 497dd37 to main 8a4b543 (#659, #660, the macOS drops). pnpm-release-management still follows the rev systemfsoftware main pins (5eb4c5d). The published tarballs are byte-identical: the sandboxed bootstrap installs against the unchanged lockfile

* build(repo): name the changesets versioning strategy, move @effect/tsgo to 0.51.0

Conductor ruling, cycle 67: prm main (#9, ca932ae) now requires versioning.strategy. The starter versions each workspace package from .changeset intents, so it takes changesets. Followed prm README: "`changesets` versioning drives the changesets libraries per package: the assembled release plan decides each member's bump, workspace dependents move with it, and the consumed intents are removed". No other field is required under that strategy. @effect/tsgo 0.50.0 -> 0.51.0 (npm latest, 2026-10-07T09:37Z); effect stays 4.0.1. Uncached check:ci and journeys pass with no new diagnostics

* build(repo): give the dev shell release-tools from the pnpm-release-management input

Conductor ruling, cycle 71: since pnpm-release-management#11 (aa712d9), devshell: true runs nix develop --command sandbox -- changeset-management check with the release-tools in the caller's dev shell. The pinned input (5eb4c5d, followed from systemfsoftware main) already exports packages.<system>.release-tools, so the lock does not move

* build(repo): take the sandbox from pnpm-release-management unpatched

Conductor rulings, cycles 74 and 77: pnpm-release-management main 537d17c carries #14, #20 (the consumer store) and #24 (the linked-worktree change), so the starter's own copy of that change (nix/patches/sandbox-linked-worktree-git.patch) and its applyPatches wiring go. pnpm-release-management becomes a direct input on main, locked to 537d17c; systemfsoftware follows it, and its nixpkgs, comment-checker and importPnpmLock follow the starter's

* test(repo): drop every sandbox launcher from PATH in the hooks proof

starter-verify c73: withoutSandboxOnPath removed only the first directory that held sandbox, so with two launchers on PATH the hooks still found one and the refusal step failed falsely. It now drops every PATH entry that provides sandbox. Two launchers on PATH: the old helper fails the step, the new one passes; with the hooks' sandbox prefix stripped, the step fails

* build(repo): take the stryker packages from the stryker-js-effect flake

Conductor ruling, cycle 83: stryker-js-effect main f06f244 (#196) publishes its 17 members as tarballs with an index.json. The flake adds it as an input (locked to f06f244, following the starter's nixpkgs, comment-checker, importPnpmLock, pnpm-release-management and systemfsoftware), and .sfs-deps now holds both flakes' tarballs with one merged index.json. Every @systemfsoftware/stryker-* member is a file:.sfs-deps catalog entry with an override, the same mechanism as the systemfsoftware packages, plus one for the stryker-js-vm-runner npm alias that stryker-js 17.0.2 declares, so check:sfs-sources passes with no npm-sourced @systemfsoftware/* package

* revert(repo): take lake1/deploy's merge back off lake1/nix-sandbox

7857171 (lake1/nix-sandbox merged into lake1/deploy) was pushed to lake1/nix-sandbox by mistake during the cycle 83 merge-up. This restores the layer's tree to fb4f8a1 exactly; lake1/deploy reverts this revert so it keeps its own content
kiro-systemf Bot pushed a commit that referenced this pull request Oct 7, 2026
* ci(ci): move mutation to a release gate on main

check:ci no longer runs stryker. The release gate on push to main plans one
shard per workspace package that declares a mutation script, refuses an
empty set, and runs each shard at break 100 on the fleet with its
incremental report cached. The checker drops
prioritizePerformanceOverAccuracy, which stryker-js 15 removes. Operator
approval: Kiro, 2026-10-05 (GATE1)

* ci(ci): run the release gate on github-hosted runners

The plan and per-package mutation jobs move from the self-hosted fleet,
which admits only private repositories, to ubuntu-latest. Mutation stays
one parallel job per package.

Operator approval: Kiro, 2026-10-05 (GATE1)

* fix(ci): refuse a mutation shard whose mutate globs match no files

A package with a mutation script whose mutate globs match nothing
reports zero mutants and passes a break-100 threshold vacuously
(review finding #9). Each package now declares its globs once, as
stryker.mutate in package.json; its Stryker config reads them and the
release gate's shard planner expands them. The planner fails red at
plan time naming the package, its directory and its globs when they
match no file, including a package with no declared globs. Its Deno
tests are ordinary tests: a turbo root task, test:scripts, runs them
through the dev shell's deno, and both pnpm test and check:ci run it,
so a local check:ci covers the planner exactly as CI does

* ci(ci): pass the release gate with a notice when there are no decisions to mutate

A decision is a *.workflow.ts file. When no workspace package has one, the planner emits an empty shard list and a 'No decisions to mutate' notice, and the mutation job is skipped. When decisions exist and no package declares a mutation script, it still refuses the empty set, naming how many decisions went unmutated. Kiro ruling, 2026-10-06

* deps(deps): move to effect 4 stable at exact pins

Every catalog entry is an exact version: effect 4.0.1, the
systemfsoftware toolchain on its Effect 4 stable majors (recommended preset
4.0.0, stryker-js 15.0.1, vitest 2.0.0, gherkin-spec 7.0.1) and current
tooling. minimumReleaseAge 1440 is explicit, so the policy is strict;
the exclude list holds the effect name patterns and one exact entry per
resolved systemfsoftware package. oxlint-tsgolint stays 7.0.2001, the
exact peer the presets require. turbo 2.11 writes an agent guidance block
into AGENTS.md unless agentGuidance is false, so turbo.json opts out.
Grader package majors move here because they require Effect 4 stable
(CONST-W3 declared)

* build(repo): move lint, test and mutation settings to the root

oxlint.shared.ts, vitest.shared.ts and stryker.shared.ts hold the house
settings; each package config extends them and declares only its own
mutate set and aliases. Both lint overrides are gone: preset 4.0.0 already
accepts Gherkin step bodies and build-config imports. The root lint file is
named oxlint.shared.ts so lint-staged's nearest-config walk never treats
the repository root as a package. Turbo inputs track the shared files.
Operator approval: Kiro, 2026-10-05 (GATE1)

* build(repo): declare the effect/http unstable-api opt-in once at the root

Root tsconfig.base.json extends the bundler/dom preset and allows exactly
effect/http through the effect language service. effect 4.0.1 ships HTTP
only as unstable and global-fetch-in-effect bans the fetch alternative.
Operator approval: Kiro, 2026-10-05 (GATE1)

* feat(repo): serve the starter site from one worker

One Cloudflare Worker serves the TanStack Start site, defined with Alchemy and run locally by pnpm dev through alchemy dev. The e2e journeys run against that local app

* ci(ci): run the e2e journeys against pnpm dev on linux and macos

bin/journeys starts the app with pnpm dev, runs the journeys against it and stops it with one SIGINT to its process group, so alchemy's local sidecar is not orphaned. The dev port is strict, so a stale server fails the run instead of moving it to another port

* feat(repo): serve an effect httpapi and its openapi document from the site worker

The site Worker routes /api/ to an Effect HttpApi: GET /api/health answers {"status":"ok"}, and /api/openapi.json serves the OpenAPI 3.1 document HttpApiBuilder generates. Every other path still goes to TanStack Start under the strict CSP. The root unstable-API opt-in gains exactly effect/http-api, where Effect 4.0.1 ships HttpApi (Kiro ruling, 2026-10-06). A journey reads the API description and calls the health check against pnpm dev

* feat(repo): give the site worker one d1 database, emulated locally under pnpm dev

alchemy.run.ts declares a D1 database and binds it to the Worker as DB. alchemy dev creates and serves it locally with no cloud account, and each cloud stage gets its own. The Worker reads the binding from cloudflare:workers, typed through Alchemy's InferEnv of the Site, so a binding renamed on one side fails the typecheck. The Health procedure now answers ok only when the database answers SELECT 1, and the home page shows unreachable when it does not. Queries go through the D1 binding itself, with no effect/sql or @effect/sql-d1

* feat(repo): preview every pull request and deploy production after the release gate

In an adopter's copy, never the template, a same-repo pull request deploys as its pr-<N> stage, gets its URL in one PR comment, runs the e2e journeys against it and is destroyed when it closes. Main deploys production once every mutation shard passes, to SITE_DOMAIN when that repository variable is set. The deployed journeys run through pnpm journeys:deployed, whose sandbox reaches only the site and gets no Cloudflare credentials

* feat(repo): call the site worker through effect rpc instead of httpapi

Ryan ruling via Kiro 2026-10-07: one RpcGroup served by RpcServer over HTTP at /api/rpc, a typed RpcClient in the page; a Health procedure replaces /api/health and the OpenAPI document goes. The home page shows the Worker's health and a browser journey reads it. The unstable-API opt-in lists effect/rpc instead of effect/http-api

* docs(repo): name the token rights the d1 deploy needs

* feat(repo): add a guestbook example feature over rpc and d1, removable in one step

A pure Workflow.make decision trims and refuses a guestbook entry with typed errors; two RPC procedures sign and list entries through a small Effect service over the D1 binding; the page calls them through the site's typed RpcClient and shows the typed refusal. Its laws run under vitest, its journeys in a real browser against pnpm dev. Everything lives under the two guestbook folders; the README names the four registration points and how to undo them

* refactor(repo): delete the hello seed package

packages/starter and every trace of it leave in one commit: the README
sections that describe it, the trusted-publisher note in .changeset, and
the catalog entries no remaining package uses (rimraf, tsdown).

npm publishing goes with it. The release job keeps capture, release-note
assertion, tagging and GitHub Releases and drops build, OIDC and pnpm
publish. A version in a manifest with no matching <name>@v<version> tag is
owed; no script queries registry.npmjs.org.
Operator approval: Kiro, 2026-10-05 (release.yml)

* feat(repo): enforce a strict nonce csp with trusted types

The Worker draws a fresh nonce per request, hands it to TanStack Start for its scripts and sends a strict Content-Security-Policy with Trusted Types. A browser journey in Chromium loads the home page under that policy with no violations, and removing the Trusted Types directive fails it

* build(repo): run all dependency code in the sandbox, systemfsoftware packages from nix

Every script that runs dependency code (install, build, test, dev, the journeys and the git hooks) goes through prm's sandbox launcher, systemfsoftware packages come from the systemfsoftware flake as tarballs, and the pnpm store is built from per-tarball fetches. check:sfs-sources stays red on the two Stryker packages, which still resolve from npm until systemfsoftware/stryker-js-effect ships them as flake outputs

* feat(repo): deploy the site from one command

pnpm run deploy deploys the Worker with Alchemy to the adopter's own Cloudflare account through bin/cloud, the one place Cloudflare credentials enter the sandbox; cloud stages keep their state in Cloudflare. A production domain is adopter configuration: SITE_DOMAIN, read by alchemy.run.ts on the prod stage. The template holds no credentials

* ci(ci): deploy production after a passing or skipped mutation job

With no decisions to mutate the release gate skips the mutation job, which used to skip the deploy that needs it. Production now deploys when the plan passed and the mutation job passed or was skipped, and never when either failed or the run was cancelled

* refactor(repo): name the site's procedure health

* test(repo): split the guestbook journeys into sign, see the entry and empty refused

Each journey opens its own browser context, so its own cookie jar; seeing the entry reopens the guestbook in a second context after signing

* refactor(repo): name the guestbook procedures sign and list

* ci(ci): pin the shared release tooling to main, as main does

* build(repo): take systemfsoftware from main and pnpm-release-management from its lock

systemfsoftware main 497dd37 fetches each workspace tarball as its own fixed-output derivation, so the tarballs match across linux and darwin; the pre-squash #606 commit hashed pnpm's whole store, which differs on darwin. pnpm-release-management now follows systemfsoftware's lock (5eb4c5d). The sandbox proofs take iplConfigHook and the whole nix tree, as prm's own flake does; the lockfile records the new tarballs' integrity

* build(repo): build the sandbox's pnpm store with pnpm-release-management's consumer store

pnpm 12 checks TLS with the platform verifier, which on macOS refuses mitm-cache's per-build certificate (UnknownIssuer on every registry tarball); prm's mkPnpmConsumerStore replays the fetches over plain HTTP for pnpm 12, still checking each tarball's lockfile integrity. The starter's own store derivation goes

* ci(ci): run the release gate only on pushes to main

starter-verify F10 and F13 (Kiro ruling, cycle 35): drop workflow_dispatch so mutation runs on push to main only; the README no longer claims Cell workflows, Cell.provide, tenant-bound store ports or compile-time phase markers, and no longer tells readers to run pnpm mutation locally

* chore(repo): refuse to run the journeys when port 1337 is already taken

starter-verify F6 (Kiro ruling, cycle 35): a stale server on :1337 would answer the journeys instead of this run's pnpm dev. Proof: with a python http.server on 127.0.0.1:1337, bin/journeys prints the refusal and exits 1; with the port free, pnpm journeys passes 2 of 2

* test(repo): drop the plain-fetch tests from the journeys suite

starter-verify F9 (Kiro ruling, cycle 35): home.integration.test.ts fetched / without a browser, and request-timeout.integration.test.ts only tested the fetch fixture's own deadline. The strict-CSP browser journey already asserts / answers 200, and #62 adds the health journey. Nothing else imports site.fixture.ts, so it goes too. pnpm journeys: 1 of 1 passed; e2e typecheck and lint clean

* chore(repo): restore commitlint.config.ts as main has it

starter-verify F4 (Kiro ruling, cycle 35): commitlint.config.ts is a read-only evaluator surface, so #52 returns it byte-for-byte to main's version. No gate needs the edit: from a linked worktree, the sandboxed commit-msg hook still sees the staged files and refuses a fix(...) commit of this tooling-only diff (type-matches-diff-shape)

* ci(ci): stop running the journeys against deployed sites

starter-verify F11 (Kiro ruling, cycle 35): deployed-site journeys are not in the spec, so they come out: bin/journeys-deployed, the journeys:deployed script, the journeys and artifact-upload steps in previews.yml and release-gate.yml (the uploads only carried what those journeys wrote), the README line and the AGENTS.md clause. Deploy, preview comment and destroy are unchanged; the preview job is named for what it does now

* fix(repo): send every rpc call to the served path in one round trip

starter-verify F12 (Kiro ruling, cycle 35): RpcClient.layerProtocolHttp prepends its url to the protocol's empty request path, and joining '/api/rpc' with '' gives '/api/rpc/'. The Worker serves '/api/rpc' exactly, so every call took a 307 to the served path. The client now builds the protocol with makeProtocolHttp over a client whose requests are set to SITE_RPC_PATH. Probe (Chromium against pnpm dev, every /api/rpc* request and response on a home-page load): before, POST /api/rpc/ -> 307 -> POST /api/rpc -> 200 per call; after, POST /api/rpc -> 200. Site typecheck and lint clean; pnpm journeys green

* chore(repo): fail the site test run when it finds no test files

starter-verify F5 (Kiro ruling, cycle 35): the site has tests, so an empty run must fail. Without --passWithNoTests, pnpm --filter @endgame/site test runs the 5 guestbook properties (exit 0), and vitest run over a directory with no test files exits 1 (No test files found)

* docs(repo): say what removing the guestbook leaves in a deployed d1

starter-verify F8 (Kiro ruling, cycle 35): removal leaves the guestbook_entries table and its 0001_create_guestbook_entries.sql row in __alchemy_migrations (alchemy's default migrations table) in a deployed D1; the README says so and gives the two statements that drop them. Both names as found in the local D1 pnpm dev created

* feat(repo): decide the worker's health in a pure workflow the site tests

Kiro ruling (cycle 35, F5 follow-up): the site needs a test of its own that survives the guestbook's removal. check-health.workflow.ts is a Workflow.make decision from the D1 probe outcome to Healthy or the DatabaseUnreachable refusal, which becomes the health procedure's typed error; the handler only runs the probe and calls it. __tests__/check-health.workflow.property.test.ts states the law: healthy exactly when the probe answered. Sabotage: flipping the decision fails it (shrunk to ProbeUnanswered). The site's test and mutation toolchain (vitest, the Stryker set, effect-cell-types, configs) moves here from the guestbook layer with the first workflow, and the release-gate planner now plans @endgame/site

* chore(repo): fail commitlint closed when git cannot read the index

Kiro ruling (cycle 35, F4 follow-up), declared per CONST-W3 in #52: commitlint.config.ts is an evaluator surface, and this restores #52's tightening of it that F4 reverted. stagedFiles no longer catches git's failure and returns no files; it lets the error through, so an unreadable index fails the commit instead of passing type-matches-diff-shape as no staged files. The sandbox proof sandbox-proofs/git-hooks.test.ts:135 depends on it; with main's version it fails with 'git failed silently'

* ci(ci): let the macos chromium download follow the chrome for testing redirect

Kiro ruling (cycle 35), declared per CONST-W3 in #52: ci.yml's macOS Playwright install step gains exactly --allow-host storage.googleapis.com. cdn.playwright.dev answers Chrome for Testing builds with a 307 to storage.googleapis.com/chrome-for-testing-public/..., which the sandbox refused, so the download failed with 403 (run 37566060960, job 112614085077) after the macOS dev shell started building

* build(repo): drop darwin from the flake and the toolchain

Ryan's standing rule via Kiro (cycle 52): no macOS anywhere. The flake builds x86_64-linux and aarch64-linux only; dprint pins only Linux archives; the comment-checker sandbox loses its sandbox-exec branch; AGENTS.md says CI is Linux only

* ci(ci): run the journeys on linux only

Ryan's standing rule via Kiro (cycle 52): no macOS legs. The journeys job runs on ubuntu-latest alone

* ci(ci): drop the macos chromium install step

Ryan's standing rule via Kiro (cycle 52): journeys run on Linux only, where Chromium comes from the dev shell

* build(repo): drop darwin from the sandbox patch and supported architectures

Ryan's standing rule via Kiro (cycle 52): the linked-worktree sandbox patch keeps only its bubblewrap hunks (the seatbelt profile hunks go), and pnpm resolves optional packages for linux only. The macOS Playwright install step, with the storage.googleapis.com allowance, left with the merge of #49

* chore(repo): drop the journeys busy-port guard

Conductor ruling, cycle 61, F4: inside the sandbox's network namespace the guard can never fire, and that isolation already keeps a stale host server from answering the journeys

* build(repo): show which tests ran in the turbo test logs

Conductor ruling, cycle 61, F6: the test task's outputLogs is new-only, so CI logs show each executed test

* ci(ci): install for the changeset check through the dev shell's bootstrap

Conductor ruling, cycle 61, F2: the plain install outside Nix cannot read the .sfs-deps tarballs (ERR_PNPM_TARBALL_READ_LOCAL_TARBALL). devshell: true makes prm's shared workflow run the starter's own bootstrap script, which installs through sandbox. Evaluator-surface edit at the conductor's direction (CONST-W3)

* build(repo): drop the root mutation script no workflow calls

Conductor ruling, cycle 61, F5: the release gate runs turbo run mutation per package directly; nothing calls the root pnpm mutation

* docs(repo): restore the guestbook section and its removal steps

Conductor ruling, cycle 61, F1: merge 57a661c took d1's README and dropped the guestbook paragraph, the removal list and the D1 note. Restored from 8f8de78, updated to the six steps that remove the feature today

* deps(deps): move @effect/tsgo to 0.50.0

Conductor ruling, cycle 62: the same bump pnpm-release-management#31 took. effect and every @effect/* stay at 4.0.1; none resolves lower. tsgo 0.50.0 reports nothing new: typecheck and lint are clean uncached. @systemfsoftware/oxlint-config-recommended 4.0.0 still pulls its own @effect/tsgo 0.45.0

* build(repo): take systemfsoftware main 8a4b543

Conductor ruling, cycle 62: systemfsoftware input moves from 497dd37 to main 8a4b543 (#659, #660, the macOS drops). pnpm-release-management still follows the rev systemfsoftware main pins (5eb4c5d). The published tarballs are byte-identical: the sandboxed bootstrap installs against the unchanged lockfile

* build(repo): name the changesets versioning strategy, move @effect/tsgo to 0.51.0

Conductor ruling, cycle 67: prm main (#9, ca932ae) now requires versioning.strategy. The starter versions each workspace package from .changeset intents, so it takes changesets. Followed prm README: "`changesets` versioning drives the changesets libraries per package: the assembled release plan decides each member's bump, workspace dependents move with it, and the consumed intents are removed". No other field is required under that strategy. @effect/tsgo 0.50.0 -> 0.51.0 (npm latest, 2026-10-07T09:37Z); effect stays 4.0.1. Uncached check:ci and journeys pass with no new diagnostics

* build(repo): give the dev shell release-tools from the pnpm-release-management input

Conductor ruling, cycle 71: since pnpm-release-management#11 (aa712d9), devshell: true runs nix develop --command sandbox -- changeset-management check with the release-tools in the caller's dev shell. The pinned input (5eb4c5d, followed from systemfsoftware main) already exports packages.<system>.release-tools, so the lock does not move

* build(repo): take the sandbox from pnpm-release-management unpatched

Conductor rulings, cycles 74 and 77: pnpm-release-management main 537d17c carries #14, #20 (the consumer store) and #24 (the linked-worktree change), so the starter's own copy of that change (nix/patches/sandbox-linked-worktree-git.patch) and its applyPatches wiring go. pnpm-release-management becomes a direct input on main, locked to 537d17c; systemfsoftware follows it, and its nixpkgs, comment-checker and importPnpmLock follow the starter's

* test(repo): drop every sandbox launcher from PATH in the hooks proof

starter-verify c73: withoutSandboxOnPath removed only the first directory that held sandbox, so with two launchers on PATH the hooks still found one and the refusal step failed falsely. It now drops every PATH entry that provides sandbox. Two launchers on PATH: the old helper fails the step, the new one passes; with the hooks' sandbox prefix stripped, the step fails

* build(repo): take the stryker packages from the stryker-js-effect flake

Conductor ruling, cycle 83: stryker-js-effect main f06f244 (#196) publishes its 17 members as tarballs with an index.json. The flake adds it as an input (locked to f06f244, following the starter's nixpkgs, comment-checker, importPnpmLock, pnpm-release-management and systemfsoftware), and .sfs-deps now holds both flakes' tarballs with one merged index.json. Every @systemfsoftware/stryker-* member is a file:.sfs-deps catalog entry with an override, the same mechanism as the systemfsoftware packages, plus one for the stryker-js-vm-runner npm alias that stryker-js 17.0.2 declares, so check:sfs-sources passes with no npm-sourced @systemfsoftware/* package

* revert(repo): take lake1/deploy's merge back off lake1/nix-sandbox

7857171 (lake1/nix-sandbox merged into lake1/deploy) was pushed to lake1/nix-sandbox by mistake during the cycle 83 merge-up. This restores the layer's tree to fb4f8a1 exactly; lake1/deploy reverts this revert so it keeps its own content

* chore(repo): name both flakes in the sfs-sources message

Conductor ruling, cycle 87: the stryker packages come from the stryker-js-effect flake since #52, so the message names both flakes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant