Skip to content

fix(scripts): give every throwaway-repo git child an environment of its own (#16644 Tier B) - #19168

Merged
os-try-charles merged 4 commits into
mainfrom
claude/issue-16644-git-env-isolation-tier-b
Sep 19, 2026
Merged

os-try-charles merged 4 commits into
mainfrom
claude/issue-16644-git-env-isolation-tier-b

Conversation

@os-try-charles

@os-try-charles os-try-charles commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #16644

Clause-②: no

What this changes

Every git child these files spawn against a throwaway repository now carries an
environment of its own — gitFreeEnv() from scripts/git-env.mjs, the blanket GIT_*
strip #16624 landed and #16753 applied in Tier A. The judgement is made per spawn
point
, not per file: a child that operates on the repository its cwd and arguments
name gets the strip; a child that talks to a remote keeps the ambient environment and is
marked as doing so.

⚠️ This is prevention, not a repaired outage. No failure has been measured in any of
these files — the card is observation-class and says so. The mechanism was measured on
#16624: 8,190 paths staged as deleted in a shared index, and core.bare = true written
into the .git/config every linked worktree of that clone reads, from a self-test that
printed ticks throughout. What follows is that shape, reproduced against a disposable
stand-in and then closed.

Scope, and why it is wider than the card's Tier B

  • Tier B, 13 files under scripts/** — the card's enumeration, re-measured on
    origin/main @ 5d0ee8f60 as still bare.
  • packages/spec/scripts/build-schemas-check-mode.test.ts and
    packages/spec/scripts/sharded-artifacts.test.ts
    — added on the triage ruling
    (comment 5578727562, acceptance point 4): 「packages/spec 两个文件收敛到
    gitFreeEnv(),LEAKED_GIT_ENV 一并删除。⛔ 不要保留"两种都行"的过渡态」. Their
    hand-maintained ten-name allowlist of GIT_* location variables is gone; the reason it
    goes rather than gains an entry is the card's own: an allowlist has to be kept level
    with git's list, and the key it misses is the key that bites.
  • ⛔ Not touched: Tier A (scripts/git-merge-regen.mjs,
    scripts/check-regen-pending.mjs) — already done by Tier A of the #16624 class: the merge=os-regen driver and the pre-commit/pre-push gate build throwaway git repos with no environment of their own — the two files git itself invokes #16753, which triage split out
    of this card as priority:p1; and Tier C, which contains scripts/pm/** — the
    domain:skills lane.
  • ⛔ No repo-wide gate here, per triage acceptance point 5: it would land red and get
    weakened. It belongs after Tier C, on its own card.

Per spawn point: local-only or network-touching

gitFreeEnv() is applied ONLY to the local-only column. The boundary is triage's,
fixed verbatim from the card: this container really does carry GIT_CONFIG_COUNT with
GIT_CONFIG_KEY_0=credential.interactive, GIT_CONFIG_KEY_1/2=url.https://github.com/.insteadOf
and GIT_SSL_CAINFO=/root/.ccr/ca-bundle.crt (read off the box while writing this), so a
network child stripped of them loses its transport.

file local-only spawn points, now stripped left ambient, and why
scripts/ablation-dist-preflight.mjs status --porcelain -z; show HEAD:PATH; the self-test's git() over a mkdtemp corpus —
scripts/check-adr-0087-registration.mjs the git(args, cwd) helper (every argv form); cat-file --batch — (the fetch --unshallow texts are remedy prose, not spawns)
scripts/check-bash32-floor.mjs listPopulation's ls-files -z; fixtureRepo's init -q and add -A; the index-vs-disk control's ls-files —
scripts/check-changeset-no-major.mjs the one git() helper: init / config / add / commit / diff / show / rev-parse / merge-base, and the #4690 leg's fetch whose remote is a local mkdtemp PATH — no https/ssh child exists in this file
scripts/check-empty-changeset.mjs the one git() helper, same census, two local-path fetch legs —
scripts/check-engine-split-ratio.mjs the main git helper; the self-test g helper (init/config/add/log + two clone from a file:// URL under its own mkdtemp); the dated commit; the --cwd re-entry child —
scripts/check-nul-bytes.mjs lsFiles; repoRoot; the self-test's init, config, add -A -f, ls-files, add -A —
scripts/check-skill-frame-freshness.mjs rev-parse / init / config / remote add / update-ref / commit, through a git() helper that strips by default ⛔ fetch --quiet --no-tags origin main — NETWORK-TOUCHING. Marked ⛔ AMBIENT ENVIRONMENT ON PURPOSE, reached through an explicit network: true. Stripped, this gate would degrade to its offline rung on every run: a warning where a verdict belongs
scripts/check-type-check-coverage.mjs check-ignore --stdin -z; readIgnoredPaths' ls-files --others --ignored; the self-test's g — (tsc children are not git)
scripts/docs-audit/check-drift-comment.mjs GIT_ENV's base, so the fixture git() helper and the mapper child both inherit the strip — the identity and GIT_CONFIG_GLOBAL/SYSTEM pins are DELIBERATE and re-applied on top of it
scripts/objectui-changeset-digest.mjs the git(cwd, args) helper; the clone from a file:// mkdtemp source; rev-parse HEAD; merge-base --is-ancestor; and the six bash bump-objectui.sh fixture drivers, whose env BASE is now stripped because that script spawns git one frame down two bash capability probes carrying BASH_ENV — they spawn no git at all, so they are not spawn points for this card
scripts/objectui-range.mjs the git(cwd, args) helper; the end-to-end node re-entry child —
scripts/collect-release-notes.sh one unset of every ambient GIT_* name, before any child: all four report sections read checkouts by path, and the self-test's init/add/commit/three clones/one fetch are all under its own mktemp root with a file:// remote — the per-command GIT_AUTHOR_DATE=… git commit prefixes in the fixture loop are applied per invocation, after that line, and are deliberately unaffected
packages/spec/scripts/build-schemas-check-mode.test.ts, …/sharded-artifacts.test.ts HERMETIC_ENV is now built from gitFreeEnv(); every fixture git and every generator run inside one operates on an fs.mkdtemp directory — GIT_CONFIG_GLOBAL / GIT_CONFIG_SYSTEM / GIT_CONFIG_NOSYSTEM stay set on top: they are the file's own "read no config" pins, not inherited leakage

验收备注

Triage's six acceptance points (comment 5578727562), and what was measured against each.

1. 先红后绿, every batch. A one-time instrument built a disposable stand-in
repository (git init + a tracked file + a commit), pointed the hook-exported location
variables at it, ran each file's temp-repo path, and compared a fingerprint of the
stand-in's config, index (ls-files), refs (for-each-ref) and HEAD before and
after. ⭐ The stand-in is disposable on purpose: pointing those variables at this checkout
would not test the incident, it would be the incident, for every agent on the box — the
same discipline scripts/git-merge-regen.mjs states for its own probe.

Two leak shapes were run, because they are not interchangeable and scripts/git-env.mjs
records why: with GIT_WORK_TREE also set, git resolves the work tree to the stand-in and
add -A re-adds its own files, so the index comes back unchanged and a case can pass
proving nothing. GIT_DIR alone is the incident's shape.

  • Firing control, per probe: before any verdict, a plain git rev-parse --absolute-git-dir in an unrelated directory had to answer with the stand-in's git dir.
    It did, in all 52 probes. A control that does not fire measures nothing.
  • RED, the UNFIXED files (restored one at a time from the merge base 5d0ee8f60,
    each proven on disk by comparing git hash-object against that blob, then restored from
    HEAD and proven restored by an empty git diff HEAD): 12 of 13 wrote the
    stand-in
    under GIT_DIR alone — config in 10, index in 11, refs in 10, HEAD in 4.
    ⭐ Three of them (ablation-dist-preflight, check-adr-0087-registration,
    docs-audit/check-drift-comment) exited 0 while doing it — [finding] a gate self-test's git init/git add -A inherited ambient GIT_* under pre-commit — staged 8,190 deletions in the real index and wrote core.bare = true into the SHARED .git/config, breaking the primary checkout for every agent on the box #16624's signature
    exactly: a green self-test writing another repository. The 13th,
    check-nul-bytes.mjs, did not write the stand-in; it exited 1, because the leaked
    init created no repository in the directory it was handed and the fixture could not be
    built. Red in a different key, and stated as such rather than counted as a write.
  • GREEN, on this branch: all 13 leave the stand-in byte-identical under BOTH leak
    shapes (26 probes), and all 26 child runs exit 0 — the isolation is complete enough
    that the self-tests still pass with a leaked git environment in the process.
  • Every probe also fingerprinted the real checkout (status --porcelain + the shared
    config) before and after. It never moved, in any of the 52 probes, and the whole-tree
    git status --porcelain after the red leg was empty.

2. Negative control. With those variables absent — ordinary CI and hand runs — every
changed file behaves as today: 19 invocations (each gate's bare call and its
--self-test, both batteries, never --self-test alone), all exit 0, real batteries
observed in the logs (check-nul-bytes 75 assertions, check-bash32-floor 177 cases,
check-type-check-coverage 55+97+56+28+19+18 cases, objectui-range all checks,
collect-release-notes all cases). The two packages/spec files: 2 files / 114 tests
passed
, 487s.

3. Per spawn point, local-only vs network-touching. The table above. One
network-touching child in the whole surface, and it keeps the ambient environment.

4. packages/spec converged, allowlist deleted. Both files now build HERMETIC_ENV
from gitFreeEnv(); the allowlist constant is gone from both and is not re-spelled in
either file, so a census of the retired shape does not match the paragraph that explains
it. ⛔ No fourth spelling was introduced. Equivalence is structural rather than argued: the
ten names the allowlist removed are a strict subset of "every GIT_-prefixed key", so the
new fixture environment is a subset of the old one, and the 114 tests above pass on it.

5. No repo-wide gate. None added.

6. Closing count, with the card's own classifier — and it is NOT zero. Reported in
the report and below: the classifier's spawn probe was run in the argv-array form
('git' plus a separate init/add/ls-files probe), never as a command string,
because triage's own first measurement was a false negative for exactly that reason and
「对照不发火的零什么都没测到」.

Acceptance notes

⚠️ One finding, filed rather than fixed here:
packages/create-objectstack/src/template-consistency.test.ts carries a THIRD copy of the
retired GIT_* allowlist
(same ten names, same for … delete env[key] shape). Triage's
census of that shape on 2026-09-08 found two carriers and named both; this one is not on
its list. It is left out of this PR deliberately — converging it would add a package, and
therefore a verification surface, that neither triage nor the dispatch declared — and it
is reported for the Tier C lap so the "one spelling, not two" ruling can actually close.
Until it lands, the allowlist shape survives in one file.

Noted, not filed: nothing else.

Local verification, and what is declared to CI

  • dispatch-gates --commands re-derived in this worktree against the real changed set
    (15 paths, merge base 5d0ee8f60, three-dot) → 82 commands; run, with results in the
    report. pnpm check:pm-dispatch-gates was detached and waited on with tail --pid, per
    its recorded 430–450s prescription.
  • ⛔ Declared to CI, not run locally: the 50 artifact-roster families, the 11
    wide-population families, the 5 workflow-valued families and the 5 path-scheduled CI
    jobs dispatch-gates names as outside its runnable list, and the repo-wide pnpm lint.
  • skip-changeset: nothing published moves. packages/spec's files[] does not list
    scripts/, and repo-root scripts/** is inside no package.

Generated by Claude Code


PM addendum — the declared surface, corrected (domain:devx 执行席, 2026-09-19T04:48Z)

⚠️ This body undercounts its own surface. The API reads 17 changed files; the body says 16 in one place and "15 paths" in the verification section. The author declined to PATCH the body under its write budget and put the reading in the commit message instead, so the PM records it here rather than leaving a reviewer to reconcile it.

The two files beyond the originally dispatched 13 scripts/** + 2 packages/spec fixtures, both forced rather than chosen:

  1. packages/spec/vitest.repo-tests.json (+1 line) — verified by the PM's own ablation at 2026-09-19T04:15:38Z: with the line removed check-cross-package-test-inputs exits 1 naming scripts/sharded-artifacts.test.ts; with it, 0. The blob was restored to its pristine hash and git diff HEAD was clean.
  2. scripts/git-env.d.mts (43 lines) — the sanctioned shape, not a new invention. PM control, read at 2026-09-19T04:47:44Z: packages/spec/scripts/build-schemas-check-mode.test.ts imports git-env.mjs at :57 and check-regen-pending.mjs at :58 — adjacent lines, same file, same TS program, same lane — and only the second had a sibling .d.mts. Nine such mirrors existed before this PR; there are now ten, and check-declaration-mirrors discovers its corpus (both batteries exit 0 on this head).

⛔ The PM has kept the mirror rather than reverting it: the two remedies that avoid a new file are a @ts-expect-error suppression and duplicating the retired allowlist back into the fixtures, and both are refused — the first trades a red for a suppression, the second reverses triage's 「收敛到 gitFreeEnv(),退役 allowlist」 ruling.


Generated by Claude Code

…ts own

Git exports GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE into every child it runs
and those outrank `cwd`, so a `git init` / `add -A` / `ls-files` aimed at a temp
directory lands on the repository those variables name. Measured on #16624:
8,190 paths staged as deleted in a shared index and `core.bare = true` written
into the `.git/config` every linked worktree reads, from a self-test that
printed ticks throughout.

Applies the landed `gitFreeEnv()` strip (scripts/git-env.mjs, #16624, applied in
Tier A by #16753) to the Tier B spawn points, per spawn point rather than per
file, and converges the two `packages/spec` fixtures onto the same spelling by
retiring their hand-maintained GIT_* allowlist.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
…set-digest too

The `execFileSync('bash', [bump-objectui.sh, ...])` end-to-end leg still spread
`process.env`, so under a leaked GIT_DIR the driver's own `git rev-parse
<sha>^{commit}` resolved against the leaked repository and the self-test died.
Measured: with this spawn left ambient the `--self-test` exited 1 under the leak
probe while every other file exited 0.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
@os-try-charles os-try-charles added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 19, 2026 — with Claude
@github-actions

github-actions Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/spec/vitest.repo-tests.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/vitest.repo-tests.json) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4b21c8d1fbc3d20ba965723db17e51401748ee38 → packageMentionDocs.

Converging its fixture environment onto `gitFreeEnv()` makes the file import
`scripts/git-env.mjs`, so it now reads outside its package. `check:cross-package
-test-inputs` said so (exit 1, naming the file): an escaping test left in the
`local` project runs under a task whose hash never moves with what it reads, so
it can go red on main while every PR reports green. Verified after: the gate
prints OK, and `vitest list` puts the file in `repo` and no longer in `local`.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
CI red on 6b398c3: `packages/spec` `pnpm run typecheck` exited 2 with TS7016
at both converted fixtures -- "Could not find a declaration file for module
'../../../scripts/git-env.mjs'". Reproduced locally against
`check:scripts-typecheck`, the lane that owns it: `tsconfig.scripts.json`
includes `scripts/**/*`, inherits `strict` from the root config and sets no
`allowJs`, so an untyped `.mjs` import from `packages/spec/scripts/` is TS7016.

The mechanical difference from the live precedent: `scripts/js-comment-mask.mjs`
and `scripts/check-regen-pending.mjs` each ship a hand-written `.d.mts` sibling
(#5475, #10398) -- nine of them existed before this commit -- and
`scripts/git-env.mjs` did not. That is also why the pre-existing
`check-regen-pending.mjs` import in the very same test file type-checks.

So this copies that shape and nothing else: one sibling declaration, PARTIAL on
purpose per `invoked-as.d.mts`, declaring only the export TypeScript consumes.
`check:declaration-mirrors` discovers the corpus rather than listing it and now
reports "OK: 10 hand-written declaration(s) agree with their modules on name,
kind and required arity" -- `gitFreeEnv`'s `Function.length` is 0 and the
declaration's required arity is 0.

⚠️ This is one file beyond the declared surface, and it is declared as such: it
is the mandatory accompaniment of the import triage ordered, and the two
remedies that avoid a new file are the two the dispatch refuses.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
@os-try-charles
os-try-charles marked this pull request as ready for review September 19, 2026 04:53
@os-try-charles
os-try-charles added this pull request to the merge queue Sep 19, 2026
Merged via the queue into main with commit c229223 Sep 19, 2026
45 checks passed
@os-try-charles
os-try-charles deleted the claude/issue-16644-git-env-isolation-tier-b branch September 19, 2026 05:24
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…onto gitFreeEnv() (objectstack-ai#19391)

Fixes objectstack-ai#19171

Clause-②: no

## What this does

The hand-maintained ten-name `GIT_*` location allowlist survived in one
more file after PR objectstack-ai#19168 converged the two `packages/spec` carriers.
This retires it the same way, so the repo carries **one** spelling of
the strip and no "either is fine" transitional state — the objectstack-ai#16644
ruling's 「不要保留「两种都行」的过渡态」.

In `packages/create-objectstack/src/template-consistency.test.ts`,
`REPO_READ_ENV` becomes `gitFreeEnv()`. The ten names it replaced are
byte-identical to the allowlist objectstack-ai#19168 deleted from both spec carriers —
the same shape, not a variant — and the constant is not re-spelled
anywhere in the file, so a census of the retired shape does not match
the paragraph that explains it.

### The header prose at `:42-54` is kept, deliberately

That paragraph records why this carrier stops short of the fixture
harnesses' `/dev/null` config pins: closing global config would also
discard `safe.directory`, which is what lets git read a checkout owned
by another user inside a container, and it would trade a passing read
for `detected dubious ownership`.

`gitFreeEnv()` does not reintroduce that risk, and the reason is
mechanical rather than a judgement call: it **deletes** keys instead of
pinning any of them to `/dev/null`, so it never sets
`GIT_CONFIG_GLOBAL`. Global and system config stay open,
`safe.directory` with them. The concern is preserved, and more strictly
— deletion moves git toward its own defaults rather than substituting an
empty config file.

The widening is free here for the boundary the module's own header
draws: `gitFreeEnv()` is for a **local** child and never for one that
talks to a remote. Both children in this file are local reads against
`cwd` (`ls-files` and `git grep`), so the transport settings the blanket
strip also removes are nothing either one needs. Measured on this
container: the ambient environment carries 11 `GIT_*` keys, **none** of
them among the ten the allowlist named, and the `GIT_CONFIG_*` triple
carries only `credential.interactive` and two `url.insteadOf` remote
rewrites. There is also landed precedent for a local read of the
**real** checkout taking this environment —
`scripts/check-type-check-coverage.mjs` spells it `env: gitFreeEnv(), //
LOCAL-ONLY (objectstack-ai#16644)`.

## The two companion files are FORCED, not chosen

The import is a relative ES-module specifier that escapes the package,
and `check-cross-package-test-inputs` treats an undeclared escaping
import as a red gate by design. This is not an argument — it was
measured on the conversion commit before the declarations existed:

```text
FAIL: cross-package test inputs are not declared consistently.

  - create-objectstack names path(s) no declared glob covers, so a change to them would not
    re-run its tests:
      scripts/git-env.mjs   (named in packages/create-objectstack/src/template-consistency.test.ts)
    Widen the package's globs to cover them.
```

So `scripts/cross-package-test-inputs.mjs` and `turbo.json` gain
`scripts/git-env.mjs`. Same shape PR objectstack-ai#19168's own addendum recorded for
its two extra files — both forced rather than chosen. The alternative —
re-spelling the blanket strip inline to stay inside one file — is
refused: it would be a **fourth** spelling of the retired shape, which
is what the ruling forbids.

Only the `.mjs` is declared, not its `.d.mts` sibling: vitest resolves
and loads the module at run time, which is what makes it a live input to
this package's `test` verdict, while the declaration is type-only and
the generic `typecheck` turbo task takes `$TURBO_DEFAULT$` with no
repo-root additions at all. See the acceptance note below.

## Does the test still fail for the reason it was written to catch?

Yes — measured, not assumed. A conversion that compiles, runs green and
no longer asserts the property is the failure mode this card is about,
so the deliverable is the ablation rather than the diff.

The battery builds a **decoy** repository under `mktemp -d` (never the
real checkout) and proves a firing control before any verdict:

```text
absolute-git-dir  leak=[/tmp/os19171-decoy-WNXIGS/.git]
absolute-git-dir  real=[/home/user/objectstack/.git/worktrees/objectstack-issue-19171]
ls-files '*SKILL.md'  leak=0   real=15
```

Under a leaked `GIT_DIR` the exact command this test runs answers for
the decoy and reports **0** SKILL.md files — at **exit 0**, silently.
That is the objectstack-ai#9068 exposure class the file exists to close. A second
probe confirms the leak survives the pnpm/exec spawn chain, so legs 2
and 3 are not void.

| Leg | Strip | Leak | Exit | Tests |
|:---|:---|:---|:---|:---|
| L1 negative control | present | absent | 0 | 37 passed |
| L2 guard | present | **present** | 0 | 37 passed |
| L3 ablation | **removed** | **present** | **1** | **1 failed**, 36
passed |
| L4 ablation control | **removed** | absent | 0 | 37 passed |

L3's red is the vacuous-green guard itself:

```text
FAIL  src/template-consistency.test.ts > skills catalog boundary > finds the curated skills/ catalog (sanity)
AssertionError: expected 0 to be greater than 0
```

L4 is the leg that makes the red mean something: with the strip removed
but no leak present, all 37 still pass — so the red in L3 comes from the
leak, not from the mutation. The mutation was landed through
`scripts/ablation-replace.mjs` (anchor 1 to 0, blob `36ab445bd902` to
`6b125ae11ffb`), and the restore is proven on disk rather than by an
exit code: blob equals HEAD and `git diff HEAD` is empty.

## Verification

Run at `0e2e1065b0`, the commit this PR lands.

- `pnpm --filter create-objectstack typecheck` — **exit 0**. This was
the open question: the module sits outside the package's `rootDir:
"src"`, and TS6059 was the failure mode. It resolves to the declaration
instead, and that is a reading rather than an inference — `tsc --noEmit
--listFiles` puts `scripts/git-env.d.mts` in the program at entry 178 of
308, with the test file itself at 179.
- **Reverse verification that the green is real**: the declaration is
*effective*, not silently `any`. Mutating the call to `gitFreeEnv` (the
missing parentheses that the `.d.mts` header names as the costliest
mistake here) turns the same typecheck red with `TS2322: Type '(base?:
ProcessEnv | undefined) => ProcessEnv' is not assignable to type
'ProcessEnv'`, then restores to blob equal to HEAD.
- `pnpm --filter create-objectstack test` — **16 files, 203 tests, all
passed**.
- `pnpm --filter create-objectstack build` — green; the dependency
closure is empty by construction (this package has no workspace
dependencies), so that leg is an empty run.
- `pnpm check:cross-package-test-inputs` — 255 self-test cases pass,
then **OK**.
- Derived gate family via `scripts/pm/dispatch-gates.mjs`: **68 derived,
65 green, 0 unrun**. Three are NOT MEASURED, each exiting 3 on its own
prerequisite (`check:dual-build-cjs-loads`, `check:lean-entry-closure`,
`check:type-check-debt` all need a full-repo build, which is CI's).
Re-derived after `git fetch origin main`: still 68, no new families.
- `pnpm lint` (`eslint . --no-inline-config`, repo-wide, not narrowed) —
**exit 0**.

No changeset: measured rather than assumed. This package's `files[]` is
`dist`, `README.md`, `CHANGELOG.md`; after a build, `gitFreeEnv`,
`REPO_READ_ENV` and `LEAKED_GIT_ENV` have **zero** hits across those
paths while the positive controls `syncObjectStackDeps`,
`SKILLS_INSTALL_COMMAND` and `TEMPLATES` all hit. The changed test file
is not a build entry point, and the other two files are repo-root
tooling and config.

## Acceptance notes

Out of scope, noted and deliberately not filed:

- `scripts/git-env.d.mts`'s header names only the two `packages/spec`
fixtures as its TypeScript consumers. A third now exists, so that
sentence is stale. It is prose, not a gate — `check:declaration-mirrors`
discovers its own corpus and is green — so nothing enforces it and
nothing breaks. Flagged by the dispatching seat as explicitly outside
this fence.
- `@objectstack/vitest-filter-preflight#test` declares
`scripts/js-comment-mask.mjs` without the `.d.mts` sibling that thirteen
other turbo entries pair with it, while its
`test/config-wiring-sweep.test.ts` does import the module from
TypeScript. On inspection this is consistent rather than wrong — a
`test` task is vitest, which never type-checks, so the declaration is
genuinely irrelevant to that task's hash — and it is the same reasoning
applied here for `git-env`. Recorded because it reads as an
inconsistency until you work that out; no successor is blocked by it.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01QCdUBjM47SxioST9z5Zwdf)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants