diff --git a/.claude/settings.json b/.claude/settings.json
index 94c7e30e..dfbc3029 100644
--- a/.claude/settings.json
+++ b/.claude/settings.json
@@ -1,7 +1,4 @@
{
- "// ": "Managed by socket-wheelhouse; edit the template, then cascade.",
- "// auth": "No apiKeyHelper. Claude Code spawns that runner with neither env nor cwd, so it could never read AI_BALANCER_ENABLED, and a helper that returns nothing renders as 'apiKeyHelper failed: did not return a value' on every launch. The balancer route instead exports ANTHROPIC_API_KEY into CLAUDE_ENV_FILE from the ai-balancer-proxy-start SessionStart hook, which DOES receive the env and so can gate on the flag. Nothing persists in settings, so the claude.ai login is the default and needs no undo.",
- "// env": "The fleet no-phone-home knobs, one copy per delivery surface. FLEET_ENV (.github/actions/fleet/setup/fleet-env.json) is the source; the shell-rc bridge and the CI workflow env already derive from it, and this block is the third surface — a Claude Code session and every tool it spawns. Kept in lockstep by agent/settings-env-matches-fleet-env.mts, so a knob added there fails this file until it is added here too.",
"env": {
"AI_BALANCER_ENABLED": "1",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
@@ -12,67 +9,6 @@
"OTEL_SDK_DISABLED": "true",
"SFW_TELEMETRY_DISABLED": "true"
},
- "hooks": {
- "PostToolUse": [
- {
- "hooks": [
- {
- "type": "command",
- "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/fleet/index.cjs PostToolUse"
- }
- ]
- }
- ],
- "PreToolUse": [
- {
- "matcher": ".*",
- "hooks": [
- {
- "type": "command",
- "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/fleet/index.cjs PreToolUse"
- }
- ]
- }
- ],
- "SessionStart": [
- {
- "hooks": [
- {
- "type": "command",
- "command": "node \"$CLAUDE_PROJECT_DIR\"/scripts/repo/bootstrap/fetch-session.mts"
- }
- ]
- },
- {
- "hooks": [
- {
- "type": "command",
- "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/fleet/index.cjs SessionStart"
- }
- ]
- }
- ],
- "Stop": [
- {
- "hooks": [
- {
- "type": "command",
- "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/fleet/index.cjs Stop"
- }
- ]
- }
- ],
- "UserPromptSubmit": [
- {
- "hooks": [
- {
- "type": "command",
- "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/fleet/index.cjs UserPromptSubmit"
- }
- ]
- }
- ]
- },
"permissions": {
"allow": [
"Bash(gh release create:*)",
@@ -88,11 +24,5 @@
"Bash(git push -f:*)",
"Bash(git push --force-with-lease:*)"
]
- },
- "// statusLine": "Fail-soft on purpose. scripts/fleet is FETCHED from the release bundle, never tracked, so the script is absent until a fetch has run. Measured without the -f test: the statusline exited 1 on module-not-found, once per render. The test makes an absent script render nothing instead.",
- "statusLine": {
- "type": "command",
- "command": "bin=\"$CLAUDE_PROJECT_DIR/.claude/hooks/fleet/_dist/statusline/statusline-render-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/')\" && [ -x \"$bin\" ] && exec \"$bin\" || script=\"$CLAUDE_PROJECT_DIR/scripts/fleet/spend-statusline.mts\" && [ -f \"$script\" ] && node \"$script\" || true"
- },
- "// ": "Repository-owned Claude settings belong below this marker."
+ }
}
diff --git a/.config/fleet/.prettierignore b/.config/fleet/.prettierignore
deleted file mode 100644
index e80a039a..00000000
--- a/.config/fleet/.prettierignore
+++ /dev/null
@@ -1,133 +0,0 @@
-**/build/**
-!**/scripts/fleet/build/**/
-!**/scripts/fleet/build/**/*.mts
-!**/scripts/repo/build/**/
-!**/scripts/repo/build/**/*.mts
-**/build/**/build/**
-**/build/**/*.d.mts
-
-# Format-ignore (but track) — files we keep byte-identical with their upstream /
-# vendored / generated source. oxfmt reads this file via
-# `--ignore-path .config/fleet/.prettierignore`; the lint runner threads the flag
-# so the convention works from any working directory.
-#
-# EVERY pattern here is `**/`-anchored. oxfmt builds the matcher with
-# `Gitignore::new(path)` (oxc apps/oxfmt/src/cli/resolve.rs), which roots it at
-# THIS file's directory (.config/fleet/) — so a bare slashed pattern like
-# `upstream/**` anchors to `.config/fleet/upstream/**` and silently matches nothing.
-# A leading `**/` matches at any depth. Enforced by
-# scripts/fleet/check/prettierignore-globs-are-anchored.mts.
-#
-# `.claude/` is treated like node_modules — never formatted, never linted, no
-# matter whether the files are git-tracked. Hooks, skills, settings, and the
-# acorn AST helper (`_shared/ast/`, a thin npm-backed wrapper over the
-# `@ultrathink/acorn.rs.wasm` wasm parser) are all opaque to the formatter under
-# this single exclusion.
-**/.claude/**
-
-# `.agents/skills/` is the GENERATED cross-tool skill mirror
-# (gen/agents-skills-mirror.mts flattens .claude/skills/{fleet,repo}// to
-# .agents/skills/-/ for Codex + OpenCode). Generated output whose
-# source (.claude/, above) is itself unformatted-by-design, so the byte-identical
-# mirror is ignored too. agents-skills-mirror-is-current keeps it in sync.
-**/.agents/**
-
-# Tooling mirrors are formatted at their canonical template source.
-# Test ownership does not imply distribution. The formatter reads distributed
-# test paths from the generated fleet ignore block in oxlintrc.json.
-**/.config/fleet/**
-**/.github/actions/fleet/**
-**/.git-hooks/fleet/**
-**/assets/fleet/**
-**/docs/fleet/**
-**/scripts/fleet/**
-
-# The rolldown-inlined dep-0 fetcher — a build artifact (from template/bootstrap/
-# src/* via scripts/repo/gen/bootstrap.mts), not hand-written source; oxfmt would
-# fight the bundler output. The template/bootstrap/src/* modules ARE formatted.
-# The .d.mts sibling is the generator's rolldown-plugin-dts declaration emit —
-# same contract.
-**/bootstrap/fleet.mjs
-**/bootstrap/fleet.d.mts
-# The SessionStart fetch-kernel's declaration emit, from the same generator.
-
-# The rolldown-bundled fleet oxlint plugin (scripts/fleet/build-oxlint-bundle.mts
-# output) — generated + gitignored; oxfmt would fight the bundler output. The
-# `.config/fleet/**` glob above does not reach it (the whole-tree walk's matcher skips
-# the dotdir `.config`), so it needs an explicit line — twin of isNeverGated() in
-# scripts/fleet/_shared/format-scope.mts and the oxfmtrc.json ignorePatterns.
-# NB: this matcher is rooted at THIS file's dir (.config/fleet/), so the pattern
-# is anchored to the BASENAME — a `**/.config/fleet/oxlint-plugin.generated.mjs` form is a
-# silent no-op here (the file's path relative to the matcher root is just
-# `oxlint-plugin.generated.mjs`). The `.gitignore` twin keeps the repo-root-anchored
-# `**/.config/fleet/oxlint-plugin.generated.mjs` form because THAT matcher roots at the
-# repo root.
-**/oxlint-plugin.generated.mjs
-
-# The ownership registry, generated by emit-ownership.mts — twin of
-# isNeverGated() in scripts/fleet/_shared/format-scope.mts. Its JSON output
-# differs from oxfmt's (sorted keys, no trailing spaces), so a plain format
-# run dirties it from a clean tree every time. The generator owns the bytes.
-**/commit-cascade/manifest/fleet-files.json
-
-# The wheelhouse dogfood oxlint config, generated by
-# scripts/repo/gen/dogfood-oxlint-config.mts, which serializes canonical
-# 2-space JSON and holds its own staleness check. Same split as the registry
-# above: isNeverGated() already skips it on a scoped run, so only oxfmt's own
-# --all walk reached it, and the two disagreed the moment the generator emitted
-# a long array. The generator owns the bytes.
-**/.config/repo/oxlintrc.dogfood.json
-
-# The repo-tier external-tools pin file, written by
-# scripts/fleet/update/external-tools.mts on every soak-plan apply. Same split
-# again: the updater serializes it, so formatting it from a clean tree only
-# dirties what the next apply rewrites.
-**/.config/repo/external-tools.json
-
-# The per-repo config JSON Schema, emitted from the TypeBox source by
-# scripts/fleet/socket-wheelhouse-emit-schema.mts. Same split as its two
-# siblings above: the explicit-path formatter already skips it, so leaving it
-# in the --all walk let the two disagree the moment a description grew long
-# enough to rewrap. The emitter owns the bytes.
-**/.config/repo/socket-wheelhouse-schema.json
-
-# Generated build/test output — machine-written trees whose generators own the
-# bytes; formatting them is churn against the next build. These are the
-# GENERATED_GLOBS twins (scripts/fleet/constants/generated-globs.mts is the
-# single source; scripts/fleet/check/generated-globs-are-consistent.mts asserts
-# this file covers every entry).
-**/dist/**
-**/out/**
-**/target/**
-**/test/fixtures/**
-
-# Vendored / upstream trees — kept byte-identical with their source of truth. Per
-# CLAUDE.md "untracked-by-default for vendored / build-copied trees": someone
-# else's source, not ours; the formatter would rewrite it into our local style.
-# Fixture trees are test data, including conformance corpora and tiered fleet
-# fixtures. Their harness or owning sync controls their byte representation.
-**/fixtures/**
-**/upstream/**
-
-# gh-aw generated workflows. `gh aw compile` turns a `.md` agentic workflow
-# into a hardened `.lock.yml`; that artifact is tool-owned and must stay
-# byte-identical to the compiler output (the .md is the source of truth).
-**/.github/workflows/*.lock.yml
-
-# Everything from the top of this file through the end sentinel below is
-# fleet-canonical: cascade and bundle placement rewrite that whole span from
-# the wheelhouse source on every refresh — see
-# scripts/fleet/_shared/fleet-canonical-splice.mts. Member-generated content,
-# like the lockstep-mirrors block scripts/fleet/lockstep/emit-mirror-globs.mts
-# appends, must live BELOW the end sentinel; placement preserves that tail
-# byte-for-byte.
-
-**/*.generated.cjs
-**/*.generated.cts
-**/*.generated.js
-**/*.generated.json
-**/*.generated.mjs
-**/*.generated.mts
-**/*.generated.ts
-**/template/generated/**
-#fleet-canonical-end
diff --git a/.config/fleet/oxlintrc.json b/.config/fleet/oxlintrc.json
deleted file mode 100644
index 516b0ba6..00000000
--- a/.config/fleet/oxlintrc.json
+++ /dev/null
@@ -1,373 +0,0 @@
-{
- "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/5306f24d9e82ae36ad9c3c964f33075bc589c799/npm/oxlint/configuration_schema.json",
- "plugins": ["typescript", "unicorn", "import"],
- "jsPlugins": ["./oxlint-plugin.generated.mjs"],
- "categories": {
- "correctness": "error",
- "suspicious": "error"
- },
- "rules": {
- "eslint/complexity": ["error", 15],
- "eslint/curly": "error",
- "eslint/no-await-in-loop": "off",
- "eslint/no-console": "off",
- "eslint/no-control-regex": "off",
- "eslint/no-empty": [
- "error",
- {
- "allowEmptyCatch": true
- }
- ],
- "eslint/no-new": "error",
- "eslint/no-proto": "error",
- "eslint/no-shadow": "error",
- "eslint/no-underscore-dangle": "off",
- "eslint/no-unmodified-loop-condition": "off",
- "eslint/no-unused-vars": [
- "error",
- {
- "args": "all",
- "argsIgnorePattern": "^_",
- "varsIgnorePattern": "^$",
- "caughtErrors": "all",
- "caughtErrorsIgnorePattern": "^_",
- "destructuredArrayIgnorePattern": "^_",
- "ignoreRestSiblings": false
- }
- ],
- "eslint/no-useless-catch": "off",
- "eslint/no-var": "error",
- "eslint/prefer-const": "error",
- "eslint/prefer-regex-literals": [
- "error",
- {
- "disallowRedundantWrapping": true
- }
- ],
- "eslint/preserve-caught-error": "off",
- "eslint/sort-imports": "off",
- "import/no-cycle": "off",
- "import/no-named-as-default": "off",
- "import/no-named-as-default-member": "off",
- "import/no-self-import": "error",
- "import/no-unassigned-import": "off",
- "socket/backtick-commands-in-comments": "error",
- "socket/bag-param-optionality-naming": "error",
- "socket/const-enum-shape": "error",
- "socket/export-top-level-functions": "error",
- "socket/exported-name-has-domain-word": "error",
- "socket/git-directories-are-hierarchical": "error",
- "socket/guard-contract": "error",
- "socket/import-block-is-hoisted": "error",
- "socket/inclusive-language": "error",
- "socket/lint-disable-precedes-code": "error",
- "socket/match-assertions-normalize-paths": "error",
- "socket/max-comment-block-lines": "error",
- "socket/max-file-lines": "error",
- "socket/mcp-tool-names-are-conventional": "error",
- "socket/no-agent-brand-assumption": "error",
- "socket/no-ai-slop-prose": "error",
- "socket/no-bare-crypto-named-usage": "error",
- "socket/no-bare-spawn-childproc-access": "error",
- "socket/no-boolean-trap-param": "error",
- "socket/no-cached-for-on-iterable": "error",
- "socket/no-comment-glob-star-slash": "error",
- "socket/no-console-prefer-logger": "error",
- "socket/no-default-export": "error",
- "socket/no-deprecation": "error",
- "socket/no-dynamic-import-in-snapshot-hook": "error",
- "socket/no-dynamic-import-outside-bundle": "error",
- "socket/no-error-message-assertions": "error",
- "socket/no-eslint-biome-config-ref": "error",
- "socket/no-fetch-prefer-http-request": "error",
- "socket/no-file-scope-oxlint-disable": "error",
- "socket/no-fileoverview-prefer-file": "error",
- "socket/no-force-delete": "error",
- "socket/no-handbuilt-file-url": "error",
- "socket/no-inline-defer-async": "error",
- "socket/no-inline-logger": "error",
- "socket/no-literal-control-char": "error",
- "socket/no-logger-glyph-prefix": "error",
- "socket/no-logger-newline-literal": "error",
- "socket/no-malformed-bypass-marker": "error",
- "socket/no-map-async-callback": "error",
- "socket/no-minified-bundler-output": "error",
- "socket/no-module-eval-side-effects": "error",
- "socket/no-namespace-import": "error",
- "socket/no-npx-dlx": "error",
- "socket/no-optional-param-before-options-bag": ["warn"],
- "socket/no-optional-positional-trap": "error",
- "socket/no-options-param-mutation": "error",
- "socket/no-package-manager-auto-update-reenable": "error",
- "socket/no-parenthetical-aside": ["error"],
- "socket/no-placeholders": "error",
- "socket/no-platform-specific-import": "error",
- "socket/no-private-path-in-source": "error",
- "socket/no-process-chdir": "error",
- "socket/no-process-cwd-in-scripts-hooks": "error",
- "socket/no-promise-race": "error",
- "socket/no-promise-race-in-loop": "error",
- "socket/no-prose-jargon": "error",
- "socket/no-redundant-spread-fallback": "error",
- "socket/no-required-in-options-bag": ["error"],
- "socket/no-runtime-features-below-engine-floor": "error",
- "socket/no-snapshot-hostile-builtin": "error",
- "socket/no-source-content-tests": "error",
- "socket/no-source-sniffing": "error",
- "socket/no-spawn-stream-double-consume": "error",
- "socket/no-spawnsync-code-property": "error",
- "socket/no-src-import-in-test-expect": "error",
- "socket/no-status-emoji": "error",
- "socket/no-structured-clone-prefer-json": "error",
- "socket/no-sync-rm-in-test-lifecycle": "error",
- "socket/no-top-level-await": "error",
- "socket/no-truncated-lint-disable-reason": "error",
- "socket/no-truthy-object-guard": "error",
- "socket/no-underscore-identifier": "error",
- "socket/no-use-strict-in-esm": "error",
- "socket/no-vendored-alias-in-test": "error",
- "socket/no-vitest-empty-test": "error",
- "socket/no-vitest-focused-tests": "error",
- "socket/no-vitest-identical-title": "error",
- "socket/no-vitest-skipped-tests": "error",
- "socket/no-vitest-standalone-expect": [
- "error",
- {
- "additionalTestBlockFunctions": ["cmdit"]
- }
- ],
- "socket/no-which-for-local-bin": "error",
- "socket/optional-explicit-undefined": "error",
- "socket/options-null-proto": "error",
- "socket/options-param-naming": "error",
- "socket/paths-are-normalized-before-match-at-edit": "error",
- "socket/personal-path-placeholders": "error",
- "socket/prefer-all-settled": "error",
- "socket/prefer-array-from": "error",
- "socket/prefer-async-spawn": "error",
- "socket/prefer-cached-for-loop": "error",
- "socket/prefer-char-code-comparison": "error",
- "socket/prefer-crlf-safe-split": "error",
- "socket/prefer-ellipsis-char": "error",
- "socket/prefer-env-as-boolean": "error",
- "socket/prefer-error-message-helper": "error",
- "socket/prefer-exec-path-over-node": "error",
- "socket/prefer-exists-sync": "error",
- "socket/prefer-find-repo-root": "error",
- "socket/prefer-find-up-package-json": "error",
- "socket/prefer-function-declaration": "error",
- "socket/prefer-lib-versions-over-semver": "error",
- "socket/prefer-mock-import": "error",
- "socket/prefer-modern-syntax": "error",
- "socket/prefer-node-builtin-imports": "error",
- "socket/prefer-non-capturing-group": "error",
- "socket/prefer-object-array-destructure": "error",
- "socket/prefer-optional-chain": "error",
- "socket/prefer-pure-call-form": "error",
- "socket/prefer-refined-record": "error",
- "socket/prefer-replace-function": "error",
- "socket/prefer-repo-root-dot-cache": "error",
- "socket/prefer-safe-delete": "error",
- "socket/prefer-separate-type-import": "error",
- "socket/prefer-setup-phrasing": "error",
- "socket/prefer-shell-win32": "error",
- "socket/prefer-socket-lib-compare-str": "error",
- "socket/prefer-socket-lib-env": "error",
- "socket/prefer-socket-lib-error-message": "error",
- "socket/prefer-socket-lib-json-format": "error",
- "socket/prefer-socket-lib-normalize-path": "error",
- "socket/prefer-socket-lib-predicates": "error",
- "socket/prefer-spawn-over-execsync": "error",
- "socket/prefer-stable-self-import": "error",
- "socket/prefer-static-type-import": "error",
- "socket/prefer-typebox-schema": "error",
- "socket/prefer-undefined-over-null": "error",
- "socket/prefer-windows-test-helpers": "error",
- "socket/repository-paths-are-contained": "error",
- "socket/require-async-iife-entry": "error",
- "socket/require-regex-comment": "error",
- "socket/require-vitest-globals-import": "error",
- "socket/require-void-run-hook": "error",
- "socket/returned-object-null-proto": "error",
- "socket/socket-api-token-env": "error",
- "socket/sort-array-literals": "error",
- "socket/sort-boolean-chains": "error",
- "socket/sort-equality-disjunctions": "error",
- "socket/sort-named-imports": "error",
- "socket/sort-object-literal-properties": "error",
- "socket/sort-regex-alternations": "error",
- "socket/sort-set-args": "error",
- "socket/sort-source-methods": "error",
- "socket/terse-lint-disable-reason": "error",
- "socket/use-fleet-canonical-api-token-getter": "error",
- "typescript/array-type": [
- "error",
- {
- "default": "array-simple"
- }
- ],
- "typescript/consistent-type-assertions": [
- "error",
- {
- "assertionStyle": "as"
- }
- ],
- "typescript/consistent-type-imports": "error",
- "typescript/no-duplicate-enum-values": "error",
- "typescript/no-duplicate-type-constituents": [
- "error",
- {
- "ignoreUnions": true
- }
- ],
- "typescript/no-explicit-any": "error",
- "typescript/no-extra-non-null-assertion": "error",
- "typescript/no-extraneous-class": "off",
- "typescript/no-misused-new": "error",
- "typescript/no-non-null-asserted-optional-chain": "off",
- "typescript/no-redundant-type-constituents": "off",
- "typescript/no-this-alias": [
- "error",
- {
- "allowDestructuring": true
- }
- ],
- "typescript/no-unnecessary-type-assertion": "off",
- "typescript/no-useless-empty-export": "error",
- "typescript/no-wrapper-object-types": "error",
- "typescript/prefer-as-const": "error",
- "typescript/triple-slash-reference": "error",
- "unicorn/consistent-function-scoping": "off",
- "unicorn/no-array-for-each": "off",
- "unicorn/no-array-reverse": "error",
- "unicorn/no-array-sort": "error",
- "unicorn/no-empty-file": "off",
- "unicorn/no-null": "off",
- "unicorn/no-useless-fallback-in-spread": "off",
- "unicorn/numeric-separators-style": "error",
- "unicorn/prefer-node-protocol": "error",
- "unicorn/prefer-spread": "off"
- },
- "overrides": [
- {
- "files": [
- "**/scripts/**",
- "**/test/**",
- "**/tests/**",
- "**/.config/**",
- "**/.git-hooks/**",
- "**/.github/**",
- "**/.claude/hooks/**",
- "**/.claude/skills/**",
- "**/.claude/workflows/**"
- ],
- "rules": {
- "socket/export-top-level-functions": "off",
- "socket/inclusive-language": "off",
- "socket/no-default-export": "off",
- "socket/no-dynamic-import-outside-bundle": "off",
- "socket/no-npx-dlx": "off",
- "socket/no-placeholders": "off",
- "socket/no-status-emoji": "off",
- "socket/prefer-function-declaration": "off",
- "socket/sort-source-methods": "off"
- }
- },
- {
- "files": [
- "**/template/base/universal/.claude/**",
- "**/template/base/universal/.config/fleet/**",
- "**/template/base/universal/.git-hooks/**",
- "**/template/base/universal/scripts/fleet/**",
- "**/template/base/universal/test/fleet/**"
- ],
- "rules": {
- "socket/max-file-lines": "off"
- }
- },
- {
- "files": ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
- "rules": {
- "eslint/no-unused-vars": "off"
- }
- },
- {
- "files": [
- "**/scripts/fleet/build-hook-bundle.mts",
- "**/scripts/fleet/build-hook-snapshot.mts",
- "**/scripts/fleet/build-snapshot-launcher.mts",
- "**/scripts/fleet/fetch-fleet-pack.mts",
- "**/scripts/fleet/gen/hook-dispatch.mts",
- "**/scripts/fleet/gen/hook-validators.mts",
- "**/scripts/fleet/lib/stable-alias.mts",
- "**/scripts/fleet/lockstep/emit-mirror-globs.mts",
- "**/scripts/fleet/sync-oxlint-rules.mts",
- "**/scripts/fleet/update.mts",
- "**/scripts/fleet/update/fleet-pins.mts",
- "**/scripts/repo/dogfood.mts",
- "**/scripts/repo/commit-cascade/fixers/mirror-mode.mts"
- ],
- "rules": {
- "socket/prefer-mirror-lock-write": "error"
- }
- }
- ],
- "ignorePatterns": [
- "**/.agents/**",
- "**/.cache/**",
- "**/.opencode-tmp/**",
- "**/.claude/**",
- "**/coverage/**",
- "**/coverage-isolated/**",
- "**/dist/**",
- "**/node_modules/**",
- "**/patches/**",
- "**/test/fixtures/**",
- "**/test/packages/**",
- "**/test/repo/**",
- "**/tests/fixtures/**",
- "**/wasm_exec.js",
- "**/.config/fleet/oxlint-plugin.generated.mjs",
- "**/*.d.ts",
- "**/*.d.ts.map",
- "**/*.tsbuildinfo",
- "#fleet-canonical-begin (managed by socket-wheelhouse sync)",
- "**/.claude/agents/fleet/**",
- "**/.claude/commands/fleet/**",
- "**/.claude/hooks/fleet/**",
- "**/.claude/rules/fleet/**",
- "**/.claude/skills/fleet/**",
- "**/.config/fleet/**",
- "**/.config/fleet/oxlint-plugin/**",
- "**/.config/repo/rolldown/**",
- "**/.git-hooks/**",
- "**/.github/actions/fleet/**",
- "**/.pnpm-store/**",
- "**/bootstrap/**",
- "**/docs/fleet/agents.md/**",
- "**/scripts/fleet/**",
- "**/scripts/repo/bootstrap/**",
- "**/test/fleet/_shared/**",
- "**/test/fleet/scripts/**",
- "**/wasm_exec.js",
- "**/.config/repo/vitest.config.mts",
- "**/.config/repo/vitest.settings.mts",
- "**/.mcp.json",
- "**/test/fleet/e2e/comment-voice.test.mts",
- "**/test/fleet/integration/comment-voice.test.mts",
- "**/test/fleet/nock-loopback-passthrough.test.mts",
- "**/test/fleet/unit/ci/gates/run.test.mts",
- "**/test/fleet/unit/comment-voice.test.mts",
- "**/test/fleet/unit/credentials/otp/bindings.test.mts",
- "**/test/fleet/unit/fix/plan.test.mts",
- "**/test/fleet/unit/fix/run.test.mts",
- "**/test/fleet/unit/lockstep/emit-mirror-globs.test.mts",
- "**/test/fleet/unit/registry/apple/ci/keychain.test.mts",
- "**/test/fleet/unit/registry/cargo/placeholder.test.mts",
- "**/test/fleet/unit/registry/npm/placeholder.test.mts",
- "**/test/fleet/unit/setup/lib/pnpm-path.test.mts",
- "#fleet-canonical-end",
- "",
- ""
- ]
-}
diff --git a/.config/fleet/tsconfig.check.json b/.config/fleet/tsconfig.check.json
deleted file mode 100644
index 62c0749e..00000000
--- a/.config/fleet/tsconfig.check.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "//": "The repo half comes last so a member's own compilerOptions win. Path aliases live there: without it the type gate resolved none of a member's src/external/* mappings, because the root tsconfig is a sibling of this chain rather than an ancestor.",
- "extends": ["./tsconfig.check.base.json", "../repo/tsconfig.check.json"],
- "compilerOptions": {
- "rootDir": "../.."
- }
-}
diff --git a/.config/repo/external-tools.json b/.config/repo/external-tools.json
deleted file mode 100644
index 9ea117a3..00000000
--- a/.config/repo/external-tools.json
+++ /dev/null
@@ -1,51 +0,0 @@
-{
- "tools": {
- "bun": {
- "description": "Bun — the runtime this scanner plugs into via bunfig [install.security].scanner; the version the test suite and CI run against.",
- "notes": [
- "Release bun-v1.4.2, published 2026-09-05 — the exact runtime pin used by tests and CI.",
- "Integrity values are the upstream release's own SHASUMS256.txt entries converted to sha256 SRI.",
- "CI parity: .github/workflows/ci-gates.yml resolves Bun through the shared external-tool manager."
- ],
- "platforms": {
- "darwin-arm64": {
- "asset": "bun-darwin-aarch64.zip",
- "integrity": "sha512-uhAxGmAhtxO7Le+Jr34WCtXRuA4wwOkAThkYKr/t79uBxP7aP7yyJSBmg2oMAaiXmXTlckzkJJGMOkcIRZofEA=="
- },
- "darwin-x64": {
- "asset": "bun-darwin-x64.zip",
- "integrity": "sha512-4+hvbfmFztAqMzjhWHJubMdTIk7cx9OK2ApcarbMHq1FL6GxiWenCMEm8834mg53n0/cE/Ggj/dbmWMsABdVUw=="
- },
- "linux-arm64": {
- "asset": "bun-linux-aarch64.zip",
- "integrity": "sha512-eT1ChCtUpunmqaTwHslVzvS/3jCWzLVo/OTTzukW+dRpmJaRWoZqEdO8HsS3J0h27FoUeaJK1PwqTzbIPSXNRQ=="
- },
- "linux-arm64-musl": {
- "asset": "bun-linux-aarch64-musl.zip",
- "integrity": "sha512-qp6U7ooz94Pb667ZSwZS/DS0IU2jn9ORv8wEoMnMyWWIg2CSMPPN30FYxhnKpV0cWfGCAblXJF/K0kdcgmBi6Q=="
- },
- "linux-x64": {
- "asset": "bun-linux-x64.zip",
- "integrity": "sha512-aJ4/M5sTwlIYm305Q3Vflb2zHk1Ku016qJpfj+8CqtoJo3jqmUpzB2ZIt11UbTGbJigof0nIec/WLk5Kq589hA=="
- },
- "linux-x64-musl": {
- "asset": "bun-linux-x64-musl.zip",
- "integrity": "sha512-sQwSY7Vsv4+yQq0HJ7yS+E1EWg7BiJJQnafhMrMVD7heDfmBUBqrrqEVVedY5swxqsyHiRXMcoE/uC1HMyLS0g=="
- },
- "win32-arm64": {
- "asset": "bun-windows-aarch64.zip",
- "integrity": "sha512-MraeB20K74TTqAFhmPJi5RK2IS2eahsGOOp9A4BBJvsigi7vDVXD5S1u82keugmXIbnmFIIcPlZ+dmeeS6G+Cw=="
- },
- "win32-x64": {
- "asset": "bun-windows-x64.zip",
- "integrity": "sha512-RueeSnqQXoa1lptEWbvBBAkmj+anWb7htTj8VMS57gk6Om445Ac5egHEDUt1fyIT/qTVRKZkhbOQ/28anMDCEw=="
- }
- },
- "origin": "gh-asset",
- "repository": "github:oven-sh/bun",
- "tag": "bun-v1.4.2",
- "version": "1.4.2",
- "versionDate": "2026-09-05"
- }
- }
-}
diff --git a/.config/repo/lockstep.json b/.config/repo/lockstep.json
deleted file mode 100644
index 31eaaf40..00000000
--- a/.config/repo/lockstep.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "$schema": "../fleet/lockstep.schema.json",
- "area": "bun-security-scanner",
- "description": "Lock-step manifest. bun-security-scanner is a single npm package with no submodule upstreams or sibling language ports \u2014 empty rows is the expected state.",
- "rows": []
-}
diff --git a/.config/repo/rolldown.config.mts b/.config/repo/rolldown.config.mts
index 19cabad5..d422cdcf 100644
--- a/.config/repo/rolldown.config.mts
+++ b/.config/repo/rolldown.config.mts
@@ -13,7 +13,7 @@ import path from 'node:path'
import process from 'node:process'
import { createLibStubPlugin } from './rolldown/lib-stub.mts'
-import { REPO_ROOT } from '../../scripts/fleet/paths.mts'
+import { REPO_ROOT } from '../../scripts/repo/runtime.mts'
import type { OutputOptions, Plugin, RolldownOptions } from 'rolldown'
diff --git a/.config/repo/root-files.json b/.config/repo/root-files.json
deleted file mode 100644
index 6f3f8646..00000000
--- a/.config/repo/root-files.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "allow": {
- "bun.lock": "Bun's lockfile for the repo's own dependency graph; `bun test` and the scanner-under-test install flow resolve from it, and Bun writes it only at the package root.",
- "bunfig.toml": "Bun requires its config at the package root; wires `[install.security] scanner = \"./src/index.mts\"` so bun install exercises the scanner under test, per README setup.",
- "DISCLOSURE": "npm's dual-use content policy reads the disclosure from the package root, and the fleet gate scripts/fleet/check/dual-use-declarations-are-complete.mts requires it there plus in the package.json files allowlist for this dual-use scanner."
- }
-}
diff --git a/.config/repo/socket-wheelhouse.json b/.config/repo/socket-wheelhouse.json
deleted file mode 100644
index 60fc216a..00000000
--- a/.config/repo/socket-wheelhouse.json
+++ /dev/null
@@ -1,174 +0,0 @@
-{
- "ai": {
- "localAssist": true
- },
- "$schema": "./socket-wheelhouse-schema.json",
- "schemaVersion": 1,
- "repoName": "bun-security-scanner",
- "repo": {
- "type": "solo"
- },
- "build": {
- "from": "npm-registry",
- "type": "js"
- },
- "cover": {
- "runner": "bun",
- "thresholds": {
- "functions": 100,
- "lines": 100
- }
- },
- "contentPolicy": {
- "class": "dual-use",
- "packages": ["."]
- },
- "vitest": {
- "lanes": {
- "fast": ["test/*.test.mts", "test/modes/**/*.test.mts"],
- "mid": ["test/repo/**/*.test.mts"]
- },
- "nodeTestExclude": [
- "test/*.test.mts",
- "test/modes/**",
- "test/repo/integration/gitignore.test.mts",
- "test/repo/unit/advisories.test.mts",
- "test/repo/unit/build.test.mts",
- "test/repo/unit/clean.test.mts",
- "test/repo/unit/fetch-session.test.mts",
- "test/repo/unit/network-preload.test.mts",
- "test/repo/unit/prepare.test.mts",
- "test/repo/unit/rolldown.config.test.mts",
- "test/repo/unit/scanner-factory.test.mts"
- ]
- },
- "sharedModuleTests": {
- "grandfathered": [
- "action-port-map",
- "actions-runtime",
- "active-run-marker",
- "agent-offload",
- "ai-infra",
- "backoff",
- "bot-directives",
- "cargo-workspaces",
- "cascade-mirror-scope",
- "cascaded-mirrors",
- "changelog-path",
- "check-steps",
- "check-steps-hooks",
- "check-steps-paths",
- "check-steps-release",
- "chrome-cdp",
- "claude-model",
- "claude-usage",
- "claude-usage-breakdowns",
- "client-limits",
- "codex-model",
- "codex-usage",
- "comment-voice-store",
- "dispatch-scan",
- "duration-budgets",
- "errors",
- "external-tools",
- "fireconnect-claude",
- "fireconnect-config",
- "fireworks-usage",
- "fixer-lock",
- "fixture-names",
- "fleet-canonical-splice",
- "fleet-membership",
- "fleet-ports",
- "fleet-source-present",
- "format-scope",
- "gauge-ramp",
- "ghcr-package",
- "git-exec",
- "git-lock-retry",
- "git-mutex",
- "git-porcelain",
- "github-raw-url",
- "github-token",
- "github-tracked-surface",
- "gitmodules",
- "gitmodules-pin-contract",
- "go-workspaces",
- "guard-block-shape",
- "hook-wiring",
- "human-gate",
- "is-main-module",
- "launcher-variants",
- "lifecycle-scripts",
- "lint-runners",
- "managed-ruleset-identity",
- "member-release-probe",
- "mirror-lock",
- "model-catalog",
- "model-choices",
- "model-list-cache",
- "model-target-ids",
- "model-targets",
- "model-training-policy",
- "never-walked-entries",
- "odai",
- "offload-spend",
- "offload-spend-read",
- "open-url",
- "outbound-surfaces",
- "pack-fetcher",
- "pack-files",
- "pack-inspect",
- "pack-ref",
- "pack-structure",
- "path-to-repo",
- "picker",
- "pinned-ref",
- "pnpm-lockfile",
- "poll-with-decay",
- "pr-body-law",
- "process-lifecycle",
- "prose-em-dash",
- "provider-apis",
- "provider-availability",
- "provider-credentials",
- "provider-models",
- "quiescence",
- "release-channels",
- "release-gap-recovery",
- "release-subject",
- "release-version-source",
- "repo-checks",
- "repo-filter",
- "repo-setup",
- "repo-visibility",
- "report-url",
- "review-comment-law",
- "run-main",
- "rust-tool-pins",
- "scope-flags",
- "security-posture-law",
- "security-posture-probe",
- "snapshot-hostile-builtins",
- "socket-state",
- "spawn-env-scan",
- "spend-projection",
- "spend-report-charts",
- "spend-report-html",
- "spend-report-path",
- "spend-report-theme",
- "spend-statusline-render",
- "statusline-launcher",
- "statusline-render",
- "swift-targets",
- "synthetic-quota",
- "tar-executable",
- "template-payload-scope",
- "terminal-link",
- "test-collection",
- "test-isolation-law",
- "tracked-globs",
- "unix-path",
- "untrack-offenders"
- ]
- }
-}
diff --git a/.config/repo/tsconfig.check.json b/.config/repo/tsconfig.check.json
deleted file mode 100644
index bbe79667..00000000
--- a/.config/repo/tsconfig.check.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "//": "Member-owned half of the type-check config. .config/fleet/tsconfig.check.json extends this LAST, so anything declared here wins over the fleet defaults. Its main job is compilerOptions.paths: a member's package aliases resolve for `pnpm run type` only when they are declared here, because the root tsconfig.json is a sibling of the check chain rather than an ancestor. Paths are relative to THIS file, so a src/ wrapper reads ../../src/external/. Seeded empty and extended unconditionally, so leave the file in place even with nothing in it.",
- "compilerOptions": {
- "types": ["node", "vitest", "bun-types"]
- }
-}
diff --git a/.config/repo/vitest.config.mts b/.config/repo/vitest.config.mts
deleted file mode 100644
index b7137d50..00000000
--- a/.config/repo/vitest.config.mts
+++ /dev/null
@@ -1,592 +0,0 @@
-/**
- * @file Vitest configuration. Isolation: the fleet default is `isolate: true` —
- * each test file gets a fresh module registry + globals, so cross-file
- * leakage (process.env, path-rewire overrides, vi.mock state, nock
- * interceptors) is impossible. Correctness by default. A repo that wants the
- * faster shared-worker mode for a known-safe subset opts those files OUT by
- * listing globs in the `vitest.nonIsolated` array of the settings file,
- * `.config/repo/socket-wheelhouse.json`. When set, those globs run in a
- * second, non-isolated project and the default isolated project excludes
- * them. No globs → everything isolated.
- */
-import { existsSync } from 'node:fs'
-import os from 'node:os'
-import path from 'node:path'
-import process from 'node:process'
-
-import { envAsBoolean } from '@socketsecurity/lib-stable/env/boolean'
-import { isCI } from '@socketsecurity/lib-stable/env/ci'
-import { defineConfig } from 'vitest/config'
-
-import { GENERATED_GLOBS } from '../../scripts/fleet/constants/generated-globs.mts'
-import { resolveGeneratedTestExcludes } from '../../scripts/fleet/test-runner/discovery.mts'
-import { resolveCoverageConfig } from '../../.config/fleet/vitest.coverage.fleet.config.mts'
-import {
- discoverSharedTestFiles,
- readBunTestFiles,
- readConformanceExcludeGlobs,
- readNonIsolatedGlobs,
- readVitestLanes,
- readVitestSettings,
- repoNodeTestExcludeGlobs,
- stringArray,
-} from './vitest.settings.mts'
-
-// Pin TZ here, at config-eval time in the MAIN process, so every worker
-// inherits TZ=UTC from its spawn env. This has to happen before a worker
-// starts: under the default `threads` pool a worker cannot change its own
-// timezone afterwards — V8 caches it at the first Date op — so a `test.env.TZ`
-// entry or a setupFiles assignment LOOKS right and does nothing, because both
-// run inside the worker. Measured: `main after set TZ=UTC` reads day 19 while
-// `worker after self-set TZ=UTC` still reads day 18.
-//
-// Why it matters beyond flakiness: an unpinned TZ makes a date-formatting bug
-// invisible to whoever wrote it and reproducible only for readers in another
-// offset. socket-cli's analytics report labeled every row by LOCAL day while
-// the API buckets by UTC day, so the same command emitted different dates
-// west of UTC; its per-package config pinned TZ and hid the bug from the
-// fleet runner. `??=` so an operator probing another zone can still override.
-process.env['TZ'] ??= 'UTC'
-
-// Coverage is on when the COVERAGE env is set (cover.mts) or the `--coverage`
-// flag is passed. Match the FLAG, not any argv containing the substring
-// "coverage" — a nested test run whose file-path args happen to include
-// "coverage" must not silently turn coverage on and clean the shared
-// coverage/.tmp (see test.mts resolveVitestEnv).
-const isCoverageEnabled =
- envAsBoolean(process.env['COVERAGE']) ||
- process.argv.some(arg => arg.startsWith('--coverage'))
-
-// Ceiling on the contention multiplier. A starved spawn is queued, not hung, so
-// it deserves more time — but a genuinely STUCK test must still fail in
-// bounded time instead of hanging the run behind a growing budget.
-const BUDGET_LOAD_CAP = 4
-
-// Below this fraction of the core count the box counts as quiet and the base
-// budget stands unchanged. Half the cores busy is normal for a test run.
-const BUDGET_QUIET_LOAD_RATIO = 0.5
-
-/**
- * How much to stretch a test budget for the machine's current contention.
- * `1` on a quiet box, rising toward {@link BUDGET_LOAD_CAP} as load climbs.
- *
- * A background build — a parallel `cargo build` saturating every core — starves
- * a spawn-per-case suite: the child is queued behind the compiler, so a fixed
- * ceiling turns machine load into a red suite with no code change. Observed:
- * one hook spec went from 1 failure to 5, 6, then 7 across three consecutive
- * runs as an unrelated `rustc` ramped to 779% CPU.
- *
- * Both readings are injectable so the arithmetic is testable without depending
- * on the load of whatever machine runs the suite.
- */
-export function resolveBudgetLoadFactor(
- options?:
- | {
- cores?: number | undefined
- loadAvg?: number | undefined
- workers?: number | undefined
- }
- | undefined,
-): number {
- const opts = { __proto__: null, ...options } as {
- cores?: number | undefined
- loadAvg?: number | undefined
- workers?: number | undefined
- }
- const cores = Math.max(1, opts.cores ?? os.availableParallelism())
- // loadavg() is [0, 0, 0] on win32, which yields the base budget — correct,
- // since there is no signal to scale by.
- const observed = Math.max(0, opts.loadAvg ?? os.loadavg()[0] ?? 0)
- // The reading is taken as the config loads, BEFORE the run creates its own
- // contention, so a quiet box reads quiet and the whole suite then runs at
- // maxWorkers. Treat the run's own parallelism as a floor on load: a spawn in
- // worker 7 competes with six siblings whatever the box looked like a second
- // ago. Without this floor a full-suite run measured 74s against a 60s budget
- // on a box that read 5.26 at startup.
- const workers = Math.max(0, opts.workers ?? resolveMaxWorkers())
- const effective = Math.max(observed, workers)
- const ratio = effective / (cores * BUDGET_QUIET_LOAD_RATIO)
- return Math.min(BUDGET_LOAD_CAP, Math.max(1, ratio))
-}
-
-/**
- * The per-test budget: the CI/coverage base ladder, stretched by current
- * machine contention. Used for both `testTimeout` and `hookTimeout` so a
- * starved `beforeAll` fixture gets the same headroom as the tests it feeds.
- */
-export function resolveTestBudgetMs(
- options?:
- | {
- cores?: number | undefined
- loadAvg?: number | undefined
- workers?: number | undefined
- }
- | undefined,
-): number {
- const ci = isCI()
- const base =
- ci && isCoverageEnabled
- ? 120_000
- : ci
- ? 60_000
- : isCoverageEnabled
- ? 30_000
- : 10_000
- return Math.round(base * resolveBudgetLoadFactor(options))
-}
-
-// Local ceiling on the core-scaled worker count. Past this the suite stops
-// getting faster and only costs memory: measured on a 14-core box, the
-// integration tier ran 147s at 16 workers, 129s at 24 and 126s at 32 — a 2%
-// gain for a third more Node heaps.
-const LOCAL_MAX_WORKERS_CAP = 24
-const LOCAL_MAX_WORKERS_FLOOR = 8
-// Workers per core. Deliberately oversubscribed: a fleet test mostly WAITS on a
-// spawned script rather than computing, so a blocked worker leaves a core idle
-// and a sibling can use it. Under-subscribing measurably hurt — the same tier
-// took 170s at 6 workers and 169s at 10, slower than the 16-worker default it
-// was meant to improve on.
-const LOCAL_WORKERS_PER_CORE = 2
-
-/**
- * Worker cap for the pool before any per-repo override, which
- * {@link capMaxWorkers} floors this against.
- */
-export function resolveFallbackMaxWorkers(
- options?:
- | { cores?: number | undefined; isCI?: boolean | undefined }
- | undefined,
-): number {
- const opts = { __proto__: null, ...options } as {
- cores?: number | undefined
- isCI?: boolean | undefined
- }
- if (opts.isCI ?? isCI()) {
- return 4
- }
- // Coverage instrumentation is memory-hungry, so it keeps the flat, lower cap
- // rather than scaling up into a heap-exhaustion failure.
- if (isCoverageEnabled) {
- return 8
- }
- const cores = Math.max(1, opts.cores ?? os.availableParallelism())
- return Math.min(
- LOCAL_MAX_WORKERS_CAP,
- Math.max(LOCAL_MAX_WORKERS_FLOOR, cores * LOCAL_WORKERS_PER_CORE),
- )
-}
-export function resolveConfiguredMaxWorkers(): number | undefined {
- const configured = readVitestSettings().maxWorkers
- return typeof configured === 'number' && configured > 0
- ? configured
- : undefined
-}
-export function capMaxWorkers(
- configuredMaxWorkers: number | undefined,
- fallbackMaxWorkers: number,
-): number {
- return configuredMaxWorkers === undefined
- ? fallbackMaxWorkers
- : Math.min(configuredMaxWorkers, fallbackMaxWorkers)
-}
-export function resolveMaxWorkers(): number {
- return capMaxWorkers(
- resolveConfiguredMaxWorkers(),
- resolveFallbackMaxWorkers(),
- )
-}
-/**
- * Fast-fail bail count. A coverage run MUST execute the FULL suite to measure
- * it, so bail is INERT under coverage: bailing on the
- * first failure aborts ~half the suite and its subprocess coverage, collapsing
- * the aggregate to a phantom partial (#79: CI read 36% vs the true ~73% because
- * one failing test bailed the run after 249 of 1224 files). Plain CI test jobs
- * no coverage, keep fast-fail bail=1; local (no CI) runs the whole suite.
- * Pure so the resolution is unit-testable without a real CI/coverage env.
- */
-export function resolveBail(config: {
- readonly isCI: boolean
- readonly isCoverage: boolean
-}): number {
- const cfg = { __proto__: null, ...config }
- return !cfg.isCoverage && cfg.isCI ? 1 : 0
-}
-/**
- * Resolve-alias merge. This config is CASCADED — a member repo that edited it
- * directly lost the edit on the next cascade: socket-webext's
- * `@socketsecurity/sdk` → browser-build alias was wiped exactly that way. The
- * settings file's `vitest.alias` map is the repo-owned surface that survives.
- * Two maps merge per key, the second winning, which is what lets a caller layer
- * a fleet default under a repo's own entries. Dot-relative replacements — `./`
- * or `../` — resolve against `root`, the repo root at config-load time, because
- * vite substitutes alias replacements verbatim: left relative, the result would
- * resolve against each importer instead of the repo root. Bare package names
- * and absolute paths pass through untouched.
- */
-export function mergeVitestAlias(
- fleet: unknown,
- repo: unknown,
- root: string = process.cwd(),
-): Record {
- const entries = (tier: unknown): Array<[string, string]> =>
- tier !== null && typeof tier === 'object' && !Array.isArray(tier)
- ? Object.entries(tier).filter(
- (e): e is [string, string] => typeof e[1] === 'string',
- )
- : []
- return Object.fromEntries(
- [...entries(fleet), ...entries(repo)].map(([find, replacement]) => [
- find,
- replacement.startsWith('./') || replacement.startsWith('../')
- ? path.resolve(root, replacement)
- : replacement,
- ]),
- )
-}
-export function resolveVitestAlias(): Record {
- return mergeVitestAlias(undefined, readVitestSettings().alias)
-}
-export function resolvePool(): 'forks' | 'threads' {
- const chosen = readVitestSettings().pool
- return chosen === 'forks' || chosen === 'threads' ? chosen : 'threads'
-}
-// Vite's own server-side resolve conditions, mirrored as a literal because
-// `vite` is a transitive dep here, not a direct one — importing
-// `defaultServerConditions` from it would not typecheck, and `vitest/config`
-// does not re-export it. Verified against the installed vite 8.1.5, whose
-// DEFAULT_SERVER_CONDITIONS is exactly this list. drift-watch: re-verify on a
-// vite major bump.
-const VITE_DEFAULT_SERVER_CONDITIONS = [
- 'module',
- 'node',
- 'development|production',
-] as const
-
-/**
- * Extra resolve conditions, from `vitest.conditions`. Vite REPLACES its default
- * condition list rather than appending to it, so the repo's entries are listed
- * FIRST and vite's own server defaults are appended — dropping them would break
- * plain node resolution for every dependency.
- */
-export function resolveVitestConditions(): string[] {
- const configured = stringArray(readVitestSettings().conditions)
- return configured.length > 0
- ? [...configured, ...VITE_DEFAULT_SERVER_CONDITIONS]
- : []
-}
-const nonIsolatedGlobs = readNonIsolatedGlobs()
-const repoResolveAlias = resolveVitestAlias()
-const repoResolveConditions = resolveVitestConditions()
-
-// Lane resolution. The runner sets FLEET_LANE (bare `pnpm test` → 'fast'); the
-// filter also applies under coverage. An unset lane traverses every lane.
-const vitestLanes = readVitestLanes()
-const activeLane = process.env['FLEET_LANE']
-// A lane's dir globs → test-file include patterns (`--lane mid|slow` runs ONLY
-// that lane; a trailing `/**` becomes `/**/*.test.{…}`).
-export function laneToTestGlobs(globs: string[]): string[] {
- return globs.map(g =>
- /\.[cm]?[jt]s(?:\}|$)/u.test(g)
- ? g
- : `${g.replace(/\/\*+$/, '')}/**/*.test.{js,ts,mjs,mts,cjs}`,
- )
-}
-const ALL_TEST_GLOBS = ['**/test/**/*.test.{js,ts,mjs,mts,cjs}']
-
-/**
- * Resolve one speed lane without dropping unclassified tests.
- *
- * Slow takes precedence over mid. Fast owns every test outside mid and slow,
- * so a new test stays in the development loop until measurement moves it.
- */
-export function resolveLaneSelection(
- lanes: ReturnType,
- lane: string | undefined,
-): { exclude: string[]; include: string[] } {
- const mid = lanes.mid ?? []
- const slow = lanes.slow ?? []
- if (lane === 'fast') {
- return { exclude: [...mid, ...slow], include: [...ALL_TEST_GLOBS] }
- }
- if (lane === 'mid') {
- return {
- exclude: [...slow],
- include: laneToTestGlobs(mid),
- }
- }
- if (lane === 'slow') {
- return {
- exclude: [],
- include: laneToTestGlobs(slow),
- }
- }
- return { exclude: [], include: [...ALL_TEST_GLOBS] }
-}
-
-const laneSelection = resolveLaneSelection(vitestLanes, activeLane)
-// The conformance tier's dir globs, and whether THIS run is the explicit
-// conformance run. Set by scripts/repo/test-conformance.mts, never by hand.
-const conformanceGlobs = readConformanceExcludeGlobs()
-const conformanceTier = process.env['FLEET_TEST_CONFORMANCE'] === '1'
-
-/**
- * Vitest-runnable property/fuzz harnesses (`*.fuzz.test.mts`). The Tier-2
- * vitiate form (`*.fuzz.mts`) is already outside the include glob and runs on
- * its own runner, so it needs no entry here.
- */
-export const FUZZ_GLOBS: readonly string[] = [
- '**/test/**/*.fuzz.test.{js,ts,mjs,mts,cjs}',
-]
-export const ORDINARY_TEST_EXCLUDES: readonly string[] = ['**/test/e2e/**']
-// Whether THIS run is the explicit fuzz tier. Set by the weekly fuzz workflow,
-// never by hand.
-//
-// Fuzzing is not a per-commit gate. A randomized harness is budgeted by
-// wall-clock, so a red run depends on machine load and cannot be replayed; a
-// deterministic one replays but costs far more than the signal is worth per
-// commit (the seeded parity harness alone ran 5-20s). Both belong to a
-// scheduled run that can afford the full budget and file what it finds.
-const fuzzTier = process.env['FLEET_TEST_FUZZ'] === '1'
-
-const config = defineConfig({
- // Repo-owned resolution from the settings file's `vitest.alias` +
- // `vitest.conditions` — see mergeVitestAlias and resolveVitestConditions.
- // Spread conditionally so a repo declaring neither keeps vite's own
- // resolution untouched.
- ...(Object.keys(repoResolveAlias).length || repoResolveConditions.length
- ? {
- resolve: {
- ...(Object.keys(repoResolveAlias).length
- ? { alias: repoResolveAlias }
- : {}),
- ...(repoResolveConditions.length
- ? { conditions: repoResolveConditions }
- : {}),
- },
- }
- : {}),
- test: {
- // Reuse content-keyed transforms while each isolated file keeps fresh state.
- fsModuleCache: true,
- deps: {
- interopDefault: false,
- },
- server: {
- deps: {
- // Treat @socketsecurity/lib-stable as external — bypass vite's
- // transform pipeline so Node resolves it natively (CJS default
- // condition). Without this, vite's `development` condition resolves
- // lib-stable via its `source` exports field (TypeScript source), and
- // the TS source files reference `./external/semver` sub-paths that are
- // not listed in the lib-stable exports map, producing an unhandled
- // EnvironmentPluginContainer.resolveId error that kills the test run.
- external: [/node_modules\/@socketsecurity\/lib-stable/],
- },
- },
- globals: false,
- environment: 'node',
- // Test setup lives under test/scripts/{fleet,repo}/setup.mts — fleet-canonical
- // setup, nock fail-closed, env scrubbing, in fleet/, repo-specific setup in
- // repo/. Both are optional: vitest skips a setupFile that doesn't exist via
- // the existsSync filter so scaffolding-only repos don't error.
- setupFiles: [
- 'test/fleet/scripts/setup.mts',
- 'test/repo/scripts/setup.mts',
- ].filter(p => existsSync(p)),
- // Slow takes precedence over mid; fast owns every remaining test.
- // `**/`-anchored so a
- // monorepo's nested `packages//test/**` trees are discovered from this
- // one root config — a bare `test/**/*.test...` only anchors at the repo
- // root, silently missing every sub-package's tests (each scoped `vitest run`
- // returns "No test files found" and a full run "passes" having executed
- // zero of them).
- include: fuzzTier
- ? [...FUZZ_GLOBS]
- : conformanceTier
- ? laneToTestGlobs(conformanceGlobs)
- : laneSelection.include,
- // Vitest treats `test/**` as `**/test/**`, so without an explicit
- // exclude it picks up every nested `test/` directory in the repo
- // — including the `.git-hooks/test/`, the oxlint plugin's per-rule
- // `.config/fleet/oxlint-plugin/fleet//test/` suites,
- // and `scripts/**/test/` suites that run under `node --test`, not
- // vitest. Those tests use `import { test } from 'node:test'` and
- // produce zero vitest suites, which vitest reports as failures.
- // List the known node:test homes here so vitest skips them cleanly
- // (their own `node --test` runners pick them up separately).
- exclude: [
- '**/node_modules/**',
- ...(conformanceTier ? [] : ORDINARY_TEST_EXCLUDES),
- // The conformance tier is opt-in via `pnpm run test:conformance`. Every
- // other lane drops it: these wrappers each spawn a FULL external corpus
- // (Test262 is ~92k scenarios per implementation), which is minutes to
- // hours, not a unit suite. Lifted only for the explicit conformance run,
- // where the include above targets exactly these globs.
- ...(conformanceTier ? [] : conformanceGlobs),
- // Property/fuzz harnesses run ONLY in the fuzz tier, never in the
- // ordinary suite or a pre-commit lane. Lifted only for the explicit fuzz
- // run, where the include above targets exactly these globs.
- ...(fuzzTier ? [] : FUZZ_GLOBS),
- // Generated/vendored trees (dist, build, upstream, test/fixtures, …) —
- // shared with lint + format from one source (constants/generated-globs.mts)
- // so the ignore surfaces can't drift. vite's default loader can't
- // transform many of these (a module-graph walk into a vendored tree or a
- // wasm blob fails "ESM integration proposal for Wasm"), so discovery AND
- // `vitest related` must skip them; scripts/fleet/test.mts filters the same
- // set from the staged pre-commit run.
- ...GENERATED_GLOBS,
- '**/.{idea,git,cache,output,temp}/**',
- '**/.claude/**',
- '.git-hooks/**',
- '.config/fleet/oxlint-plugin/**',
- 'scripts/**/test/**',
- // `template/**` holds CANONICAL non-test sources (the cascaded LIVE
- // copies are what the suite runs); live test/repo is the sole test
- // authoring home, so template is excluded unconditionally.
- 'template/**',
- // `test/isolated/**` is the isolated SUITE's turf — its own forks / longer
- // -timeout config (`vitest.config.isolated.mts`), run as a separate suite
- // by cover.mts. Exclude it from this shared suite ONLY when the repo ships
- // that config, so a repo without the isolated suite still runs any
- // `test/isolated` files here instead of silently dropping them. This is the
- // isolated DIRECTORY tier — distinct from the `isolate:` state-isolation
- // split (the `nonIsolated` projects) further down.
- ...(existsSync('.config/repo/vitest.config.isolated.mts')
- ? ['test/isolated/**']
- : []),
- // Repo-tunable node:test homes (e.g. `tools/**/test/**`) from the
- // settings file's `vitest.nodeTestExclude`. The same key feeds
- // prefer-vitest-guard's allowlist so the two never drift.
- ...repoNodeTestExcludeGlobs(),
- ...readBunTestFiles(),
- ...laneSelection.exclude,
- ],
- // Some repos in the fleet (scaffolding-only, hook-only, etc.) ship
- // this config but don't yet have a `test/` directory — vitest's
- // default behavior would fail "no tests found" there. Repos that
- // do have tests still error on actual test failures; this flag
- // only affects the empty-suite case.
- // Zero discovered files is normal for a scoped run, but it is a FAILURE
- // for the conformance tier: that run exists to execute those globs, so
- // discovering none means the tier is misconfigured and a silent pass would
- // report the heavy suites green without running one of them.
- //
- // The fuzz tier is deliberately NOT held to that: a repo can fuzz only at
- // Tier 2 (`*.fuzz.mts`, its own runner), which discovers nothing here and
- // is not a misconfiguration. Whether a repo fuzzes at all is
- // fuzz-tiers-are-covered's question, and which legs fire is the workflow's.
- passWithNoTests: !conformanceTier,
- // Reporters left unset so vitest applies its own default:
- // `[isAgent() ? 'minimal' : 'default', ...(GITHUB_ACTIONS ? ['github-actions'] : [])]`
- // (vitest/src/defaults.ts). That yields the token-lean `minimal` reporter
- // inside an AI coding agent (std-env `isAgent`: CLAUDECODE/CURSOR_/…),
- // `default` for humans, and the `github-actions` annotations reporter in CI.
- // Hard-coding `reporters: ['default']` would override that default and
- // defeat all three. https://vitest.dev/guide/reporters
- pool: resolvePool(),
- // Vitest 4 removed `poolOptions`; the per-pool worker knobs are now
- // top-level. `maxThreads`/`maxForks` → `maxWorkers`; `singleThread`/
- // `singleFork` → `fileParallelism: false` (forces maxWorkers to 1);
- // `minThreads` and `useAtomics` were dropped with no replacement.
- // Worker count tuned to physical CPUs: GH Actions ubuntu-latest has
- // 4 cores, dev laptops typically 8-16. `isCI()` (rewire-aware
- // presence check on `CI`) is truthy even for CI="" or CI=0, matching
- // the fleet convention that any CI value means CI.
- //
- // Isolation: true by default (correctness — no cross-file state leak). A
- // repo lists safe-to-share globs in the `vitest.nonIsolated` array of
- // .config/repo/socket-wheelhouse.json; when set, this default project
- // EXCLUDES them (the second project runs them non-isolated). When unset,
- // every file is isolated.
- isolate: true,
- // Keep coverage file-parallel. Worker setup removes the already-consumed
- // COVERAGE flag before test code runs, so a nested Vitest child cannot turn
- // coverage back on and clean the outer run's shared .tmp reports. Ordinary
- // Node children still inherit NODE_V8_COVERAGE for subprocess merging.
- // Local coverage caps at 8 workers because this spawn-heavy suite saturates
- // there; 16 workers add filesystem/process contention. Ordinary local tests
- // retain 16 workers, while CI matches its 4 available cores.
- maxWorkers: resolveMaxWorkers(),
- // Coverage runs with V8 instrumentation that spawned children inherit, so
- // spawn-heavy tests, hook integration specs launch a node child per case
- // legitimately exceed 10s there. CI gets a 60s budget unconditionally:
- // 2-core runners × parallel workers starve spawn-per-case suites
- // (RuleTester spawns one oxlint child per case) — the 10s/30s ceilings
- // killed lint-rule suites mid-queue on every OS while the same files pass
- // locally. CI *with* coverage is strictly heavier than either alone
- // (instrumentation + 4-core contention + thousands of instrumented child
- // spawns in one run), so it gets the longest budget — the plain-CI 60s
- // still timed out the spawn-per-case hook specs (npm-2fa-needs-pty-guard,
- // single-lander-guard) under peak release-cover contention, losing their
- // coverage and failing the gate while all four metrics were above
- // threshold. Complete the ladder rather than shave the threshold.
- testTimeout: resolveTestBudgetMs(),
- hookTimeout: resolveTestBudgetMs(),
- bail: resolveBail({
- isCI: isCI(),
- isCoverage: isCoverageEnabled,
- }),
- // Coverage shape comes from the fleet base merged with the repo-owned
- // `coverage` section of .config/repo/socket-wheelhouse.json (include
- // replace, exclude add/remove) — one canonical exclude list instead of a
- // drifted copy here.
- coverage: {
- enabled: isCoverageEnabled,
- // Ordinary tests do not need the workspace scan for coverage aliases.
- ...(isCoverageEnabled ? resolveCoverageConfig() : {}),
- },
- },
-})
-
-// The fast lane excludes every build-test home through its mid/slow lane
-// filters. Avoid the repository-wide authored-build scan during the ten-second
-// fast loop; mid, slow, and full runs retain the complete exception handling.
-if (config.test && activeLane !== 'fast') {
- config.test.exclude = resolveGeneratedTestExcludes({
- repoRoot: process.cwd(),
- include: config.test.include ?? [],
- exclude: config.test.exclude ?? [],
- })
-}
-
-// Construct complete project options explicitly: Vite's extends merge concatenates
-// include arrays, which otherwise makes the shared project rerun isolated files.
-if (nonIsolatedGlobs.length && config.test) {
- const baseTest = config.test
- const include = baseTest.include ?? []
- const exclude = baseTest.exclude ?? []
- const sharedFiles = discoverSharedTestFiles(nonIsolatedGlobs, {
- include,
- exclude,
- })
-
- config.test = {
- ...baseTest,
- projects: [
- {
- ...config,
- extends: false,
- test: {
- ...baseTest,
- name: 'isolated',
- isolate: true,
- exclude: [...exclude, ...nonIsolatedGlobs],
- },
- },
- {
- ...config,
- extends: false,
- test: {
- ...baseTest,
- name: 'non-isolated',
- isolate: false,
- include: sharedFiles.length
- ? sharedFiles
- : ['__fleet_no_shared_tests__'],
- },
- },
- ],
- }
-}
-
-export default config
diff --git a/.config/repo/vitest.settings.mts b/.config/repo/vitest.settings.mts
deleted file mode 100644
index fab61ab4..00000000
--- a/.config/repo/vitest.settings.mts
+++ /dev/null
@@ -1,202 +0,0 @@
-/**
- * @file The repo-tunable vitest settings surface — the `vitest` section of
- * the canonical per-repo settings file
- * (.config/repo/socket-wheelhouse.json), split from vitest.config.mts along
- * its natural boundary: everything here READS settings, everything there
- * RESOLVES runtime config from them. One file, one parse; each key's
- * contract is on its field below, and docs/fleet/agents.md/test-layout.md
- * carries the tier rationale.
- * Exported readers are ordered alphabetically, which `socket/sort-source-
- * methods` enforces in every consuming member. Neither copy of this file is
- * linted in the wheelhouse itself, so the order is a downstream contract.
- */
-
-import { existsSync, globSync, lstatSync, readFileSync } from 'node:fs'
-import path from 'node:path'
-
-import { normalizePath } from '@socketsecurity/lib-stable/paths/normalize'
-
-import {
- parseBunTestFiles,
- readBunTestFiles as readConfiguredBunTestFiles,
-} from '../../scripts/fleet/test-runner/bun-tests.mts'
-
-export interface VitestRepoConfig {
- // Exact repo-relative test files run by Bun and excluded from Vitest.
- bunTests?: string[] | undefined
- // Module resolve aliases for the test transform, e.g.
- // `{ "@socketsecurity/sdk": "./dist/index.browser.js" }`. A key is a LITERAL
- // specifier, never a glob, so a monorepo lists one entry per package; see
- // mergeVitestAlias for the dot-relative semantics.
- alias?: Record | undefined
- // Extra `resolve.conditions`. The route for a monorepo whose `exports` map
- // carries a `source` condition: without it vitest resolves a workspace
- // package to its built `dist`, the instrumented `src` never runs, and the
- // repo reports 0% coverage.
- conditions?: string[] | undefined
- conformanceExclude?: string[] | undefined
- lanes?: VitestLanes | undefined
- // Worker cap for the pool, floored against the CI/coverage-aware fallback.
- maxWorkers?: number | undefined
- // Globs safe to run in the faster non-isolated pool.
- nonIsolated?: string[] | undefined
- // node:test homes excluded from vitest discovery, e.g. `tools/**/test/**` for
- // a `node --test` tool corpus. prefer-vitest-guard reads the SAME key so its
- // allowlist and this exclude never drift.
- nodeTestExclude?: string[] | undefined
- pool?: 'forks' | 'threads' | undefined
-}
-
-/**
- * Test LANES — a SPEED category, orthogonal to test TYPE (unit/integration/e2e)
- * — from the `vitest.lanes` section of the canonical per-repo settings file
- * (.config/repo/socket-wheelhouse.json; see paths.mts's resolver order for the
- * fallbacks). `slow` = heavy suites (subprocess-per-case, e.g. hook integration
- * specs); `mid` = isolated in-process suites (env-mutating / vi.mock /
- * fs-heavy); `fast` = pure in-process. `slow` selects its declared patterns.
- * `mid` selects its declared patterns except tests selected by `slow`.
- * `fast` selects every remaining test, regardless of its declared patterns.
- * The runner's
- * `--lane ` flag (scripts/fleet/test.mts) selects one, and bare
- * `pnpm test` defaults to `fast` for a quick local loop. The lane filter is
- * active under coverage too. An unset FLEET_LANE traverses every lane; the
- * coverage runner selects each lane in turn and merges their reports.
- */
-export interface VitestLanes {
- fast?: string[] | undefined
- mid?: string[] | undefined
- slow?: string[] | undefined
-}
-
-// The settings file, canonical location first and the repo-root dotfile as the
-// one fallback a member may ship.
-export const SETTINGS_FILES = [
- '.config/repo/socket-wheelhouse.json',
- '.socket-wheelhouse.json',
-] as const
-
-/**
- * Resolve the shared project while retaining Node's glob semantics. Most lane
- * exclusions are exact test files: checking those in a Set avoids repeatedly
- * matching hundreds of literal patterns during glob traversal. Only ordinary
- * relative file paths qualify; escapes, glob syntax, unusual names, missing
- * paths and directories stay with Node's original exclusion implementation.
- */
-export function discoverSharedTestFiles(
- patterns: string[],
- options: { include: string[]; exclude: string[]; cwd?: string | undefined },
-): string[] {
- const exactFiles = new Set()
- const globExcludes: string[] = []
- for (const pattern of options.exclude) {
- const normalizedPattern = normalizePath(pattern)
- // Positive literal grammar: no escape, extglob, brace or bracket syntax;
- // segments cannot be dot/dotdot. Unrecognized forms use native glob.
- const literal =
- /^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*[A-Za-z0-9_-][A-Za-z0-9_.-]*\.test\.(?:js|ts|mjs|mts|cjs)$/u.test(
- normalizedPattern,
- )
- let regularFile = false
- if (literal) {
- try {
- regularFile = lstatSync(
- path.resolve(options.cwd ?? '.', normalizedPattern),
- ).isFile()
- } catch {
- // Missing or unreadable candidates retain native exclusion behavior.
- }
- }
- if (regularFile) {
- exactFiles.add(normalizedPattern)
- } else {
- globExcludes.push(normalizedPattern)
- }
- }
- return [
- ...globSync(patterns, { cwd: options.cwd, exclude: globExcludes }),
- ].filter(
- file =>
- !exactFiles.has(file.split(path.sep).join('/')) &&
- options.include.some(pattern => path.matchesGlob(file, pattern)),
- )
-}
-
-/**
- * The CONFORMANCE tier — heavy external-suite wrappers (a full Test262 corpus
- * per implementation, upstream conformance harnesses) named by
- * `vitest.conformanceExclude` in the settings file.
- *
- * `scripts/repo/test-conformance.mts` runs this tier explicitly with
- * FLEET_TEST_CONFORMANCE=1; every other run must EXCLUDE it. Both halves live
- * here so neither can sit unwired: an env var the runner sets and nothing
- * reads, or a setting that names the tier while no lane excludes it, would —
- * so `pnpm run cover` spawned a ~92k-scenario corpus per BUILT implementation,
- * three at once. Against the 60s unit budget that reads as a hung run rather
- * than the multi-hour sweep it actually is.
- */
-export function readConformanceExcludeGlobs(): string[] {
- return stringArray(readVitestSettings().conformanceExclude)
-}
-
-export function readNonIsolatedGlobs(): string[] {
- return stringArray(readVitestSettings().nonIsolated)
-}
-
-export function readVitestLanes(): VitestLanes {
- const lanes = readVitestSettings().lanes
- return lanes !== null && typeof lanes === 'object' && !Array.isArray(lanes)
- ? {
- ...(Array.isArray(lanes.fast) ? { fast: stringArray(lanes.fast) } : {}),
- mid: stringArray(lanes.mid),
- slow: stringArray(lanes.slow),
- }
- : {}
-}
-
-export function readBunTestFiles(repoRoot?: string | undefined): string[] {
- return repoRoot === undefined
- ? parseBunTestFiles(readVitestSettings().bunTests)
- : readConfiguredBunTestFiles(repoRoot)
-}
-
-/**
- * The `vitest` section of the settings file. The ONE settings-file parse in
- * this config: every resolver reads its key off this, so a torn or absent
- * file degrades to fleet defaults in exactly one place instead of six.
- */
-export function readVitestSettings(): VitestRepoConfig {
- for (let i = 0, { length } = SETTINGS_FILES; i < length; i += 1) {
- const file = SETTINGS_FILES[i]!
- if (!existsSync(file)) {
- continue
- }
- try {
- const parsed: unknown = JSON.parse(readFileSync(file, 'utf8'))
- const section =
- parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
- ? (parsed as { vitest?: VitestRepoConfig | undefined }).vitest
- : undefined
- return section !== null &&
- typeof section === 'object' &&
- !Array.isArray(section)
- ? section
- : {}
- } catch {
- return {}
- }
- }
- return {}
-}
-
-export function repoNodeTestExcludeGlobs(): string[] {
- return stringArray(readVitestSettings().nodeTestExclude)
-}
-
-/**
- * A settings value read as a string array; anything else reads as empty.
- */
-export function stringArray(value: unknown): string[] {
- return Array.isArray(value)
- ? value.filter((g): g is string => typeof g === 'string')
- : []
-}
diff --git a/.git-hooks/_shared/canonical/bundle.mts b/.git-hooks/_shared/canonical/bundle.mts
deleted file mode 100644
index 8ee826df..00000000
--- a/.git-hooks/_shared/canonical/bundle.mts
+++ /dev/null
@@ -1,123 +0,0 @@
-import { canonicalOriginAllowed } from './source.mts'
-import crypto from 'node:crypto'
-import { readFileSync, statSync } from 'node:fs'
-import path from 'node:path'
-import process from 'node:process'
-import { spawnSync } from '@socketsecurity/lib-stable/process/spawn/child'
-import { canonicalPathIsSafe } from './git.mts'
-import type { CanonicalIndexEntry } from './git.mts'
-
-function record(value: unknown): Record | undefined {
- return value !== null && typeof value === 'object' && !Array.isArray(value)
- ? Object.fromEntries(Object.entries(value))
- : undefined
-}
-
-function unscopedTarget(
- manifest: Record,
- file: string,
-): boolean {
- for (const field of [
- 'capabilityScopedFiles',
- 'conditionalScopedFiles',
- 'shapeScopedFiles',
- ]) {
- const groups = manifest[field]
- if (groups === undefined) {
- continue
- }
- if (!Array.isArray(groups)) {
- return false
- }
- for (const value of groups) {
- const files = record(value)?.['files']
- if (!Array.isArray(files) || files.includes(file)) {
- return false
- }
- }
- }
- return true
-}
-
-export function canonicalBundleBytesMatch(
- ref: string,
- file: string,
- entry: CanonicalIndexEntry,
- manifest: unknown,
-): boolean {
- const parsed = record(manifest)
- const files = record(parsed?.['files'])
- const expected = files?.[file]
- return (
- /^fleet-pack-[0-9a-f]{40}$/u.test(ref) &&
- parsed?.['templateSha'] === ref.slice('fleet-pack-'.length) &&
- unscopedTarget(parsed, file) &&
- canonicalPathIsSafe(file) &&
- entry.mode === '100644' &&
- typeof expected === 'string' &&
- /^[0-9a-f]{64}$/u.test(expected) &&
- crypto.hash('sha256', entry.content, 'hex') === expected
- )
-}
-
-const manifests = new Map()
-function fetchManifest(member: string, ref: string): unknown {
- const key = `${member}:${ref}`
- if (!manifests.has(key)) {
- const result = spawnSync(
- process.execPath,
- [path.join(member, 'scripts/fleet/pack/canonical-proof.mts'), ref],
- {
- cwd: member,
- encoding: 'utf8',
- stdio: ['ignore', 'pipe', 'pipe'],
- timeout: 30_000,
- maxBuffer: 4_194_304,
- },
- )
- if (result.status !== 0) {
- return undefined
- }
- const lines = result.stdout.trim().split(/\r?\n/u)
- const manifest: unknown = JSON.parse(lines.at(-1) ?? '')
- if (record(manifest)?.['templateSha'] !== ref.slice('fleet-pack-'.length)) {
- return undefined
- }
- manifests.set(key, manifest)
- }
- return manifests.get(key)
-}
-
-export function canonicalBundleCopyMatches(
- member: string,
- file: string,
- entry: CanonicalIndexEntry,
- options: {
- fetchManifest?: ((member: string, ref: string) => unknown) | undefined
- } = {},
-): boolean {
- if (!canonicalOriginAllowed(member)) {
- return false
- }
- try {
- const appliedPath = path.join(
- member,
- '.cache/fleet/socket-wheelhouse/bundle-applied',
- )
- if (statSync(appliedPath).size > 128) {
- return false
- }
- const ref = readFileSync(appliedPath, 'utf8').trim()
- if (!/^fleet-pack-[0-9a-f]{40}$/u.test(ref)) {
- return false
- }
- return canonicalBundleBytesMatch(
- ref,
- file,
- entry,
- (options.fetchManifest ?? fetchManifest)(member, ref),
- )
- } catch {
- return false
- }
-}
diff --git a/.git-hooks/_shared/canonical/fork-scan.mts b/.git-hooks/_shared/canonical/fork-scan.mts
deleted file mode 100644
index 938497af..00000000
--- a/.git-hooks/_shared/canonical/fork-scan.mts
+++ /dev/null
@@ -1,260 +0,0 @@
-import { spawnSync } from '@socketsecurity/lib-stable/process/spawn/child'
-import { existsSync, lstatSync, readdirSync, readFileSync } from 'node:fs'
-import path from 'node:path'
-import { readCanonicalIndexEntry, readCanonicalTreeEntry } from './git.mts'
-import { canonicalMemberCopyMatches } from './proof.mts'
-import type { CanonicalIndexEntry } from './git.mts'
-
-import {
- fleetCanonicalEntries,
- isOperatorLocalPath,
- isPerRepoMarkerPath,
-} from '../../../.claude/hooks/fleet/_shared/fleet-fork.mts'
-import {
- findFleetRegions,
- textHasFleetBlockMarkers,
-} from '../../../.claude/hooks/fleet/_shared/fleet-markers.mts'
-
-// Each child names one capability or member, never an arbitrary generated
-// bucket. Generated universal files map directly to the destination tree.
-const TEMPLATE_ROOTS: readonly string[] = [
- path.join('base', 'conditional'),
- 'overrides',
- path.join('generated', 'conditional'),
-]
-
-/**
- * Candidate template sources for a live repo-relative path.
- */
-export function templateTwinPaths(repoRoot: string, file: string): string[] {
- const candidates = [
- path.join(repoRoot, 'template', 'base', 'universal', file),
- ]
- const generatedUniversal = path.join(
- repoRoot,
- 'template',
- 'generated',
- 'universal',
- )
- if (existsSync(generatedUniversal)) {
- candidates.push(path.join(generatedUniversal, file))
- }
- for (let i = 0, { length } = TEMPLATE_ROOTS; i < length; i += 1) {
- const root = path.join(repoRoot, 'template', TEMPLATE_ROOTS[i]!)
- let names: string[]
- try {
- names = readdirSync(root)
- } catch {
- continue
- }
- for (let j = 0, { length: namesLength } = names; j < namesLength; j += 1) {
- candidates.push(path.join(root, names[j]!, file))
- }
- }
- return candidates
-}
-
-/**
- * Whether the live content is byte-identical to one of its template twins.
- *
- * Then it is cascade OUTPUT, not a fork: a fork is a live copy that DIVERGED
- * from canonical. The cascade lands its own mirrors outside this hook chain,
- * but when it loses the index lock to a parallel session it leaves them staged,
- * and only the operator can land them. Refusing that commit leaves no reachable
- * fix - the tool-call guard forbids writing the mirror by hand, and the cascade
- * cannot retry while the lock is held - so the operator's only remaining route
- * is skipping every hook, which is strictly worse than this exemption.
- */
-export function matchesTemplateTwin(
- repoRoot: string,
- file: string,
- content: string | Uint8Array,
- mode = '100644',
-): boolean {
- const candidates = templateTwinPaths(repoRoot, file)
- for (let i = 0, { length } = candidates; i < length; i += 1) {
- try {
- const stat = lstatSync(candidates[i]!)
- const candidateMode = stat.mode & 0o111 ? '100755' : '100644'
- if (!stat.isFile() || candidateMode !== mode) {
- continue
- }
- if (
- readFileSync(candidates[i]!).equals(
- typeof content === 'string' ? Buffer.from(content) : content,
- )
- ) {
- return true
- }
- } catch {
- continue
- }
- }
- return false
-}
-
-function readForkGit(repoRoot: string, args: string[]): string {
- const result = spawnSync('git', ['--literal-pathspecs', ...args], {
- cwd: repoRoot,
- encoding: 'utf8',
- stdio: ['ignore', 'pipe', 'pipe'],
- timeout: 5000,
- })
- return result.status === 0 ? (result.stdout ?? '') : ''
-}
-
-function mergeParentIds(repoRoot: string): string[] {
- const mergeHeadPath = readForkGit(repoRoot, [
- 'rev-parse',
- '--git-path',
- 'MERGE_HEAD',
- ]).trim()
- const parents = readFileSync(path.resolve(repoRoot, mergeHeadPath), 'utf8')
- .trim()
- .split(/\r?\n/)
- // Each parent is a full SHA-1 (40 hex digits) or SHA-256 (64 hex digits).
- if (
- !parents.every(parent => /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(parent))
- ) {
- return []
- }
- return parents
-}
-
-export function matchesMergeParentIndex(
- repoRoot: string,
- file: string,
-): boolean {
- try {
- const parents = mergeParentIds(repoRoot)
- if (parents.length === 0) {
- return false
- }
- const staged = readForkGit(repoRoot, [
- 'ls-files',
- '--stage',
- '-z',
- '--',
- file,
- ])
- // Match one stage-zero entry: mode, object ID, stage, then a tab.
- const entry = /^(\d+) ([a-f0-9]+) 0\t/.exec(staged)
- if (!entry || staged !== `${entry[0]}${file}\0`) {
- return false
- }
- const expected = `${entry[1]} blob ${entry[2]}\t${file}\0`
- for (const parent of parents) {
- if (
- readForkGit(repoRoot, ['cat-file', '-t', parent]).trim() !== 'commit'
- ) {
- return false
- }
- if (
- readForkGit(repoRoot, ['ls-tree', '-z', parent, '--', file]) ===
- expected
- ) {
- return true
- }
- }
- } catch {
- return false
- }
- return false
-}
-
-export interface CanonicalForkFinding {
- file: string
-}
-
-function isInsideTemplateRelative(file: string): boolean {
- return file === 'template' || file.startsWith('template/')
-}
-
-/**
- * Every staged path (repo-relative, POSIX-normalized, add/change/modify
- * only — a caller filters deletions out via `--diff-filter=ACM`) that is
- * fleet-canonical and differs from its canonical source or merge parent.
- */
-export function scanCanonicalForkPaths(
- stagedFiles: readonly string[],
- repoRoot: string,
-): CanonicalForkFinding[] {
- const entries = fleetCanonicalEntries(repoRoot)
- if (entries.length === 0) {
- return []
- }
- const findings: CanonicalForkFinding[] = []
- for (let i = 0, { length } = stagedFiles; i < length; i += 1) {
- const file = stagedFiles[i]!
- if (isInsideTemplateRelative(file)) {
- continue
- }
- if (isPerRepoMarkerPath(file) || isOperatorLocalPath(file)) {
- continue
- }
- let isCanonical = false
- for (
- let j = 0, { length: entriesLength } = entries;
- j < entriesLength;
- j += 1
- ) {
- const entry = entries[j]!
- // Glob entries are best-effort excluded here too — same conservative
- // call `isCanonicalRelativePath` makes, so a bad pattern can never
- // over-block a commit.
- if (entry.includes('*')) {
- continue
- }
- if (file === entry || file.startsWith(`${entry}/`)) {
- isCanonical = true
- break
- }
- }
- if (!isCanonical) {
- continue
- }
- const entry = readCanonicalIndexEntry(repoRoot, file)
- if (entry && stagedCanonicalFileIsAllowed(repoRoot, file, entry)) {
- continue
- }
- findings.push({ file })
- }
- return findings
-}
-
-function fleetRegionBodies(content: Uint8Array): string[] {
- const text = Buffer.from(content).toString('utf8')
- if (!textHasFleetBlockMarkers(text)) {
- return []
- }
- const lines = text.split(/(?<=\n)/u)
- return findFleetRegions(lines).map(region =>
- lines.slice(region.start, region.end + 1).join(''),
- )
-}
-
-function stagedCanonicalFileIsAllowed(
- repoRoot: string,
- file: string,
- entry: CanonicalIndexEntry,
-): boolean {
- if (matchesMergeParentIndex(repoRoot, file)) {
- return true
- }
- const baseline = readCanonicalTreeEntry(repoRoot, 'HEAD', file)
- if (baseline && baseline.mode === entry.mode) {
- const before = fleetRegionBodies(baseline.content)
- const after = fleetRegionBodies(entry.content)
- if (
- before.length > 0 &&
- before.length === after.length &&
- before.every((body, index) => body === after[index])
- ) {
- return true
- }
- }
- return (
- matchesTemplateTwin(repoRoot, file, entry.content, entry.mode) ||
- canonicalMemberCopyMatches(repoRoot, file, entry)
- )
-}
diff --git a/.git-hooks/_shared/canonical/git.mts b/.git-hooks/_shared/canonical/git.mts
deleted file mode 100644
index e9396dda..00000000
--- a/.git-hooks/_shared/canonical/git.mts
+++ /dev/null
@@ -1,164 +0,0 @@
-/**
- * @file Read immutable Git blobs and the caller's active stage-zero index.
- */
-import { spawnSync } from '@socketsecurity/lib-stable/process/spawn/child'
-import process from 'node:process'
-import { normalizePath } from '@socketsecurity/lib-stable/paths/normalize'
-import { GIT_CONTEXT_VARS } from '../git-context-vars.mts'
-
-export interface CanonicalGitOptions {
- input?: Uint8Array | undefined
- preserveIndex?: boolean | undefined
-}
-export interface CanonicalGitResult {
- status: number
- stdout: Uint8Array
-}
-export type CanonicalGitRead = (
- root: string,
- args: readonly string[],
- options?: CanonicalGitOptions | undefined,
-) => CanonicalGitResult
-export interface CanonicalIndexEntry {
- mode: string
- oid: string
- content: Uint8Array
-}
-
-export function readCanonicalGit(
- root: string,
- args: readonly string[],
- options: CanonicalGitOptions = {},
-): CanonicalGitResult {
- const env = { ...process.env }
- for (let i = 0, { length } = GIT_CONTEXT_VARS; i < length; i += 1) {
- const name = GIT_CONTEXT_VARS[i]!
- if (name !== 'GIT_INDEX_FILE' || !options.preserveIndex) {
- delete env[name]
- }
- }
- const result = spawnSync(
- 'git',
- ['--no-replace-objects', '--literal-pathspecs', ...args],
- {
- cwd: root,
- encoding: 'buffer',
- env,
- input: options.input,
- stdio: ['pipe', 'pipe', 'pipe'],
- timeout: 5000,
- maxBuffer: 4_194_304,
- },
- )
- return {
- status: result.status ?? 1,
- stdout: Buffer.isBuffer(result.stdout)
- ? result.stdout
- : Buffer.from(result.stdout ?? ''),
- }
-}
-
-export function canonicalGitText(
- root: string,
- args: readonly string[],
- readGit: CanonicalGitRead = readCanonicalGit,
-): string | undefined {
- const result = readGit(root, args)
- return result.status === 0
- ? Buffer.from(result.stdout).toString('utf8')
- : undefined
-}
-
-export function canonicalPathIsSafe(file: string): boolean {
- const normalized = normalizePath(file)
- return (
- file === normalized &&
- file.length > 0 &&
- file.length < 1024 &&
- !normalized.startsWith('/') &&
- !normalized.includes('\\') &&
- !/[\x00-\x1f\x7f:]/u.test(file) &&
- normalizePath(file)
- .split('/')
- .every(
- part =>
- part !== '' &&
- part !== '.' &&
- part !== '..' &&
- part.toLowerCase() !== '.git',
- )
- )
-}
-
-function readBlobEntry(
- root: string,
- oid: string,
- mode: string,
- readGit: CanonicalGitRead,
-): CanonicalIndexEntry | undefined {
- if (!['100644', '100755'].includes(mode)) {
- return undefined
- }
- const result = readGit(root, ['cat-file', 'blob', oid])
- return result.status === 0 ? { mode, oid, content: result.stdout } : undefined
-}
-
-export function readCanonicalIndexEntry(
- root: string,
- file: string,
- readGit: CanonicalGitRead = readCanonicalGit,
-): CanonicalIndexEntry | undefined {
- if (!canonicalPathIsSafe(file)) {
- return undefined
- }
- const result = readGit(root, ['ls-files', '--stage', '-z', '--', file], {
- preserveIndex: true,
- })
- if (result.status !== 0) {
- return undefined
- }
- // A single stage-zero regular blob, with its exact NUL-terminated path.
- const match =
- /^(100644|100755) ((?:[a-f0-9]{40}|[a-f0-9]{64})) 0\t([^\0]+)\0$/u.exec(
- Buffer.from(result.stdout).toString('utf8'),
- )
- return match?.[3] === file
- ? readBlobEntry(root, match[2]!, match[1]!, readGit)
- : undefined
-}
-
-export function readCanonicalTreeEntry(
- root: string,
- ref: string,
- file: string,
- readGit: CanonicalGitRead = readCanonicalGit,
-): CanonicalIndexEntry | undefined {
- if (!canonicalPathIsSafe(file)) {
- return undefined
- }
- const text = canonicalGitText(
- root,
- ['ls-tree', '-z', ref, '--', file],
- readGit,
- )
- if (text === undefined) {
- return undefined
- }
- // A single regular tree blob, with its exact NUL-terminated path.
- const match =
- /^(100644|100755) blob ((?:[a-f0-9]{40}|[a-f0-9]{64}))\t([^\0]+)\0$/u.exec(
- text,
- )
- return match?.[3] === file
- ? readBlobEntry(root, match[2]!, match[1]!, readGit)
- : undefined
-}
-
-export function canonicalEntriesEqual(
- left: CanonicalIndexEntry,
- right: CanonicalIndexEntry,
-): boolean {
- return (
- left.mode === right.mode && Buffer.from(left.content).equals(right.content)
- )
-}
diff --git a/.git-hooks/_shared/canonical/patch.mts b/.git-hooks/_shared/canonical/patch.mts
deleted file mode 100644
index ef3bf7ad..00000000
--- a/.git-hooks/_shared/canonical/patch.mts
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * @file Select complete context-preserving hunks from an immutable Git patch.
- */
-export function canonicalHunksAreValid(value: unknown): value is number[] {
- return (
- Array.isArray(value) &&
- value.length > 0 &&
- value.length <= 128 &&
- value.every(
- (index, position) =>
- Number.isSafeInteger(index) &&
- index >= 0 &&
- (position === 0 || index > value[position - 1]),
- )
- )
-}
-
-export function selectCanonicalPatch(
- text: string,
- source: string,
- selected?: readonly number[] | undefined,
-): Uint8Array | undefined {
- if (
- !text.startsWith(`diff --git a/${source} b/${source}\n`) ||
- text.split('\ndiff --git ').length !== 1
- ) {
- return undefined
- }
- if (
- /^(?:Binary files |GIT binary patch|copy |deleted file mode|new file mode|new mode|old mode|rename )/mu.test(
- text,
- )
- ) {
- return undefined
- }
- const sections = text.split(/(?=^@@ )/mu)
- const header = sections.shift()
- if (
- !header?.includes(`--- a/${source}\n+++ b/${source}\n`) ||
- sections.length === 0
- ) {
- return undefined
- }
- const indexes = selected ?? [...sections.keys()]
- if (
- !canonicalHunksAreValid(indexes) ||
- indexes.some(index => index >= sections.length)
- ) {
- return undefined
- }
- return Buffer.from(header + indexes.map(index => sections[index]).join(''))
-}
diff --git a/.git-hooks/_shared/canonical/proof.mts b/.git-hooks/_shared/canonical/proof.mts
deleted file mode 100644
index f01724b7..00000000
--- a/.git-hooks/_shared/canonical/proof.mts
+++ /dev/null
@@ -1,357 +0,0 @@
-import { canonicalBundleCopyMatches } from './bundle.mts'
-/**
- * @file Prove stage-zero bytes from signed producer trees or ordered template
- * patches.
- */
-import {
- chmodSync,
- mkdirSync,
- mkdtempSync,
- readFileSync,
- writeFileSync,
-} from 'node:fs'
-import os from 'node:os'
-import { normalizePath } from '@socketsecurity/lib-stable/paths/normalize'
-import { selectCanonicalPatch } from './patch.mts'
-import { applyCanonicalReplacements } from './replacements.mts'
-import path from 'node:path'
-import {
- canonicalEntriesEqual,
- canonicalGitText,
- readCanonicalGit,
- readCanonicalTreeEntry,
-} from './git.mts'
-import {
- canonicalEligibleSources,
- canonicalProducerAllowed,
- canonicalSourceAllowed,
- findCanonicalProducer,
-} from './source.mts'
-import { readCanonicalReceipt, validateCanonicalRecipe } from './receipt.mts'
-import type { CanonicalGitRead, CanonicalIndexEntry } from './git.mts'
-import type { CanonicalPatchRecipe, CanonicalPatchStep } from './receipt.mts'
-import { safeDeleteSync } from '@socketsecurity/lib-stable/fs/safe'
-export { readCanonicalIndexEntry } from './git.mts'
-export { validateCanonicalRecipe } from './receipt.mts'
-export type {
- CanonicalGitRead,
- CanonicalIndexEntry,
- CanonicalGitOptions,
- CanonicalGitResult,
-} from './git.mts'
-export type { CanonicalPatchRecipe, CanonicalPatchStep } from './receipt.mts'
-
-export interface CanonicalProofOptions {
- readGit?: CanonicalGitRead | undefined
- producerAllowed?: ((root: string) => boolean) | undefined
- sourceAllowed?:
- | ((
- member: string,
- producer: string,
- commit: string,
- source: string,
- target: string,
- ) => boolean)
- | undefined
-}
-
-function signedParent(
- root: string,
- commit: string,
- readGit: CanonicalGitRead,
-): string | undefined {
- if (
- canonicalGitText(
- root,
- ['log', '-1', '--format=%G?', commit],
- readGit,
- )?.trim() !== 'G'
- ) {
- return undefined
- }
- const parents = canonicalGitText(
- root,
- ['rev-list', '--parents', '-n', '1', commit],
- readGit,
- )
- ?.trim()
- .split(' ')
- return parents?.length === 2 && parents[0] === commit ? parents[1] : undefined
-}
-
-function stepSourceAllowed(
- member: string,
- recipe: CanonicalPatchRecipe,
- step: CanonicalPatchStep,
- config: CanonicalProofOptions,
-): boolean {
- const readGit = config.readGit ?? readCanonicalGit
- const allowed =
- config.sourceAllowed ??
- ((memberRoot, producerRoot, commit, source, target) =>
- canonicalSourceAllowed(
- memberRoot,
- producerRoot,
- commit,
- source,
- target,
- readGit,
- ))
- return allowed(
- member,
- recipe.producerRoot,
- step.commit,
- step.sourcePath,
- step.targetPath,
- )
-}
-
-function sourcePatch(
- member: string,
- recipe: CanonicalPatchRecipe,
- step: CanonicalPatchStep,
- config: CanonicalProofOptions,
-): Uint8Array | undefined {
- const readGit = config.readGit ?? readCanonicalGit
- const parent = signedParent(recipe.producerRoot, step.commit, readGit)
- if (!parent || !stepSourceAllowed(member, recipe, step, config)) {
- return undefined
- }
- const before = readCanonicalTreeEntry(
- recipe.producerRoot,
- parent,
- step.sourcePath,
- readGit,
- )
- const after = readCanonicalTreeEntry(
- recipe.producerRoot,
- step.commit,
- step.sourcePath,
- readGit,
- )
- if (!before || !after || before.mode !== after.mode) {
- return undefined
- }
- const patch = readGit(recipe.producerRoot, [
- 'diff',
- '--no-ext-diff',
- '--no-textconv',
- '--src-prefix=a/',
- '--dst-prefix=b/',
- parent,
- step.commit,
- '--',
- step.sourcePath,
- ])
- if (patch.status !== 0 || patch.stdout.length === 0) {
- return undefined
- }
- return selectCanonicalPatch(
- Buffer.from(patch.stdout).toString('utf8'),
- step.sourcePath,
- step.hunks,
- )
-}
-
-function applyStrictPatch(
- scratch: string,
- patch: Uint8Array,
- strip: number,
-): boolean {
- return (
- readCanonicalGit(
- scratch,
- ['apply', '--whitespace=nowarn', `-p${strip}`, '-'],
- { input: patch },
- ).status === 0
- )
-}
-
-function applyIndividualHunks(
- scratch: string,
- destination: string,
- patch: Uint8Array,
- strip: number,
-): boolean {
- const sections = Buffer.from(patch)
- .toString('utf8')
- .split(/(?=^@@ )/mu)
- const header = sections.shift()
- if (!header || sections.length === 0) {
- return false
- }
- for (let i = 0, { length } = sections; i < length; i += 1) {
- const hunk = Buffer.from(header + sections[i])
- if (applyStrictPatch(scratch, hunk, strip)) {
- continue
- }
- const replacement = applyCanonicalReplacements(
- readFileSync(destination),
- hunk,
- )
- if (!replacement) {
- return false
- }
- writeFileSync(destination, replacement)
- }
- return true
-}
-
-function applyProofSteps(
- member: string,
- file: string,
- recipe: CanonicalPatchRecipe,
- baseline: CanonicalIndexEntry,
- config: CanonicalProofOptions,
-): Uint8Array | undefined {
- const scratch = mkdtempSync(path.join(os.tmpdir(), 'fleet-canonical-proof-'))
- try {
- const destination = path.join(scratch, file)
- mkdirSync(path.dirname(destination), { recursive: true })
- writeFileSync(destination, baseline.content)
- chmodSync(destination, baseline.mode === '100755' ? 0o755 : 0o644)
- const seen = new Set()
- const steps = recipe.patches.filter(item => item.targetPath === file)
- for (
- let stepIndex = 0, { length } = steps;
- stepIndex < length;
- stepIndex += 1
- ) {
- const step = steps[stepIndex]!
- if (seen.has(step.commit)) {
- return undefined
- }
- seen.add(step.commit)
- const patch = sourcePatch(member, recipe, step, config)
- if (!patch) {
- return undefined
- }
- const strip =
- normalizePath(step.sourcePath).split('/').length -
- normalizePath(file).split('/').length +
- 1
- if (
- !applyStrictPatch(scratch, patch, strip) &&
- (step.application !== 'unique-replacement' ||
- !applyIndividualHunks(scratch, destination, patch, strip))
- ) {
- return undefined
- }
- }
- return readFileSync(destination)
- } finally {
- safeDeleteSync(scratch)
- }
-}
-
-export function canonicalRecipeMatches(
- member: string,
- file: string,
- entry: CanonicalIndexEntry,
- value: unknown,
- options: CanonicalProofOptions = {},
-): boolean {
- const recipe = validateCanonicalRecipe(value)
- if (!recipe || !recipe.patches.some(step => step.targetPath === file)) {
- return false
- }
- const readGit = options.readGit ?? readCanonicalGit
- if (
- !(options.producerAllowed ?? canonicalProducerAllowed)(recipe.producerRoot)
- ) {
- return false
- }
- if (
- canonicalGitText(
- member,
- ['rev-parse', '--verify', 'HEAD'],
- readGit,
- )?.trim() !== recipe.memberHead
- ) {
- return false
- }
- const baseline = readCanonicalTreeEntry(member, 'HEAD', file, readGit)
- if (!baseline || baseline.mode !== entry.mode) {
- return false
- }
- try {
- const result = applyProofSteps(member, file, recipe, baseline, options)
- return result !== undefined && Buffer.from(result).equals(entry.content)
- } catch {
- return false
- }
-}
-
-export function canonicalRecipeCopyMatches(
- member: string,
- file: string,
- entry: CanonicalIndexEntry,
- value: unknown,
- options: CanonicalProofOptions = {},
-): boolean {
- const recipe = validateCanonicalRecipe(value)
- const readGit = options.readGit ?? readCanonicalGit
- if (
- !recipe ||
- !(options.producerAllowed ?? canonicalProducerAllowed)(
- recipe.producerRoot,
- ) ||
- canonicalGitText(
- member,
- ['rev-parse', '--verify', 'HEAD'],
- readGit,
- )?.trim() !== recipe.memberHead
- ) {
- return false
- }
- return recipe.patches.some(step => {
- if (
- step.targetPath !== file ||
- !signedParent(recipe.producerRoot, step.commit, readGit) ||
- !stepSourceAllowed(member, recipe, step, options)
- ) {
- return false
- }
- const source = readCanonicalTreeEntry(
- recipe.producerRoot,
- step.commit,
- step.sourcePath,
- readGit,
- )
- return source !== undefined && canonicalEntriesEqual(entry, source)
- })
-}
-
-export function canonicalMemberCopyMatches(
- member: string,
- file: string,
- entry: CanonicalIndexEntry,
-): boolean {
- const producer = findCanonicalProducer(member)
- if (producer) {
- const head = canonicalGitText(producer, [
- 'rev-parse',
- '--verify',
- 'HEAD',
- ])?.trim()
- if (head && signedParent(producer, head, readCanonicalGit)) {
- for (const source of canonicalEligibleSources(member, file)) {
- if (!canonicalSourceAllowed(member, producer, head, source, file)) {
- continue
- }
- const committed = readCanonicalTreeEntry(producer, head, source)
- if (committed && canonicalEntriesEqual(entry, committed)) {
- return true
- }
- }
- }
- }
- if (canonicalBundleCopyMatches(member, file, entry)) {
- return true
- }
- const receipt = readCanonicalReceipt(member)
- return (
- canonicalRecipeCopyMatches(member, file, entry, receipt) ||
- canonicalRecipeMatches(member, file, entry, receipt)
- )
-}
diff --git a/.git-hooks/_shared/canonical/receipt.mts b/.git-hooks/_shared/canonical/receipt.mts
deleted file mode 100644
index 08438264..00000000
--- a/.git-hooks/_shared/canonical/receipt.mts
+++ /dev/null
@@ -1,114 +0,0 @@
-/**
- * @file Validate runtime-only recipes for reproducible signed template deltas.
- */
-import { readFileSync } from 'node:fs'
-import path from 'node:path'
-import { canonicalHunksAreValid } from './patch.mts'
-import {
- canonicalGitText,
- canonicalPathIsSafe,
- readCanonicalGit,
-} from './git.mts'
-import type { CanonicalGitRead } from './git.mts'
-
-export interface CanonicalPatchStep {
- commit: string
- sourcePath: string
- targetPath: string
- hunks?: number[] | undefined
- application?: 'unique-replacement' | undefined
-}
-export interface CanonicalPatchRecipe {
- version: 1
- memberHead: string
- producerRoot: string
- patches: CanonicalPatchStep[]
-}
-// Accept immutable SHA-1 or SHA-256 object IDs only.
-const SHA_PATTERN = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u
-function recordValue(value: unknown): Record | undefined {
- return value !== null && typeof value === 'object' && !Array.isArray(value)
- ? (value as Record)
- : undefined
-}
-function validStep(value: unknown): value is CanonicalPatchStep {
- const record = recordValue(value)
- if (
- record?.['application'] !== undefined &&
- record['application'] !== 'unique-replacement'
- ) {
- return false
- }
- if (
- record?.['hunks'] !== undefined &&
- !canonicalHunksAreValid(record['hunks'])
- ) {
- return false
- }
- return Boolean(
- record &&
- typeof record['commit'] === 'string' &&
- SHA_PATTERN.test(record['commit']) &&
- typeof record['sourcePath'] === 'string' &&
- canonicalPathIsSafe(record['sourcePath']) &&
- typeof record['targetPath'] === 'string' &&
- canonicalPathIsSafe(record['targetPath']) &&
- record['sourcePath'].endsWith(`/${record['targetPath']}`),
- )
-}
-export function validateCanonicalRecipe(
- value: unknown,
-): CanonicalPatchRecipe | undefined {
- const record = recordValue(value)
- if (
- !record ||
- record['version'] !== 1 ||
- typeof record['memberHead'] !== 'string' ||
- !SHA_PATTERN.test(record['memberHead'])
- ) {
- return undefined
- }
- if (
- typeof record['producerRoot'] !== 'string' ||
- !path.isAbsolute(record['producerRoot']) ||
- record['producerRoot'].includes('\0')
- ) {
- return undefined
- }
- if (
- !Array.isArray(record['patches']) ||
- record['patches'].length === 0 ||
- record['patches'].length > 64 ||
- !record['patches'].every(validStep)
- ) {
- return undefined
- }
- return record as unknown as CanonicalPatchRecipe
-}
-export function canonicalReceiptPath(
- root: string,
- readGit: CanonicalGitRead = readCanonicalGit,
-): string | undefined {
- const relative = canonicalGitText(
- root,
- ['rev-parse', '--git-path', 'fleet/canonical-patch-receipt.json'],
- readGit,
- )?.trim()
- return relative ? path.resolve(root, relative) : undefined
-}
-export function readCanonicalReceipt(
- root: string,
-): CanonicalPatchRecipe | undefined {
- const receipt = canonicalReceiptPath(root)
- if (!receipt) {
- return undefined
- }
- try {
- const bytes = readFileSync(receipt)
- return bytes.length <= 65_536
- ? validateCanonicalRecipe(JSON.parse(bytes.toString('utf8')))
- : undefined
- } catch {
- return undefined
- }
-}
diff --git a/.git-hooks/_shared/canonical/replacements.mts b/.git-hooks/_shared/canonical/replacements.mts
deleted file mode 100644
index 1608ebcd..00000000
--- a/.git-hooks/_shared/canonical/replacements.mts
+++ /dev/null
@@ -1,158 +0,0 @@
-/**
- * @file Apply uniquely located replacement runs from verified signed hunks.
- */
-import { selectCanonicalPatch } from './patch.mts'
-
-interface ReplacementRun {
- before: string
- after: string
-}
-interface HunkState {
- before: number
- after: number
- previous: string
- run: ReplacementRun
- runs: ReplacementRun[]
-}
-
-function finishRun(state: HunkState): boolean {
- const run = state.run
- if (!run.before && !run.after) {
- return true
- }
- if (!run.before) {
- return false
- }
- state.runs.push(run)
- state.run = { before: '', after: '' }
- return true
-}
-
-function consumeHunkLine(state: HunkState, line: string): boolean {
- const tag = line[0]
- if (tag === ' ') {
- state.before += 1
- state.after += 1
- state.previous = tag
- return finishRun(state)
- }
- if (tag === '-') {
- state.before += 1
- state.run.before += line.slice(1)
- } else if (tag === '+') {
- state.after += 1
- state.run.after += line.slice(1)
- } else if (
- line === '\\ No newline at end of file' ||
- line === '\\ No newline at end of file\n'
- ) {
- if (state.previous === '-') {
- state.run.before = state.run.before.slice(0, -1)
- } else if (state.previous === '+') {
- state.run.after = state.run.after.slice(0, -1)
- } else if (state.previous !== ' ') {
- return false
- }
- return true
- } else {
- return false
- }
- state.previous = tag
- return true
-}
-
-function hunkReplacements(section: string): ReplacementRun[] | undefined {
- // Preserve each line terminator, including a final unterminated line.
- const lines = section.match(/[^\n]*\n|[^\n]+$/gu) ?? []
- const header = lines.shift()
- // Capture old and new line counts from one complete unified hunk header.
- const match =
- header && /^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@[^\n]*\n$/u.exec(header)
- if (!match) {
- return undefined
- }
- const state: HunkState = {
- before: 0,
- after: 0,
- previous: '',
- run: { before: '', after: '' },
- runs: [],
- }
- if (!lines.every(line => consumeHunkLine(state, line)) || !finishRun(state)) {
- return undefined
- }
- if (
- state.before !== Number(match[1] ?? 1) ||
- state.after !== Number(match[2] ?? 1)
- ) {
- return undefined
- }
- return state.runs
-}
-
-function utf8Text(bytes: Uint8Array): string | undefined {
- const text = Buffer.from(bytes).toString('utf8')
- return !text.includes('\0') && Buffer.from(text).equals(bytes)
- ? text
- : undefined
-}
-
-function replaceUniqueRun(
- text: string,
- run: ReplacementRun,
-): string | undefined {
- const index = text.indexOf(run.before)
- if (
- index < 0 ||
- (index > 0 && text.charCodeAt(index - 1) !== 10) /* '\n' */ ||
- text.indexOf(run.before, index + 1) !== -1 ||
- (!run.before.endsWith('\n') && index + run.before.length !== text.length)
- ) {
- return undefined
- }
- return (
- text.slice(0, index) + run.after + text.slice(index + run.before.length)
- )
-}
-
-export function applyCanonicalReplacements(
- content: Uint8Array,
- patch: Uint8Array,
-): Uint8Array | undefined {
- let text = utf8Text(content)
- const delta = utf8Text(patch)
- if (text === undefined || delta === undefined) {
- return undefined
- }
- const first = delta.split(/\r?\n/, 1)[0]
- const source = first?.startsWith('diff --git a/')
- ? first.slice('diff --git a/'.length).split(' b/', 1)[0]
- : undefined
- if (!source || !selectCanonicalPatch(delta, source)) {
- return undefined
- }
- const sections = delta.split(/(?=^@@ )/mu).slice(1)
- let total = 0
- for (
- let sectionIndex = 0, { length } = sections;
- sectionIndex < length;
- sectionIndex += 1
- ) {
- const runs = hunkReplacements(sections[sectionIndex]!)
- if (!runs) {
- return undefined
- }
- for (
- let runIndex = 0, { length: runLength } = runs;
- runIndex < runLength;
- runIndex += 1
- ) {
- total += 1
- text = replaceUniqueRun(text, runs[runIndex]!)
- if (text === undefined) {
- return undefined
- }
- }
- }
- return total > 0 ? Buffer.from(text) : undefined
-}
diff --git a/.git-hooks/_shared/canonical/source.mts b/.git-hooks/_shared/canonical/source.mts
deleted file mode 100644
index f8a67f7d..00000000
--- a/.git-hooks/_shared/canonical/source.mts
+++ /dev/null
@@ -1,145 +0,0 @@
-/**
- * @file Authorize producer origins and member-specific committed template
- * sources.
- */
-import { findWheelhouseRoot } from '../../../.claude/hooks/fleet/_shared/wheelhouse-root.mts'
-import fleetRosterJson from '../../../.claude/skills/fleet/cascading-commits/lib/fleet-repos.json' with { type: 'json' }
-import { SOCKET_GITHUB_ORGS } from '../../../scripts/fleet/constants/socket-scopes.mts'
-import { isFleetPackProducerSlug } from '../../../scripts/fleet/member/fleet-membership.mts'
-import {
- canonicalGitText,
- canonicalPathIsSafe,
- readCanonicalGit,
- readCanonicalTreeEntry,
-} from './git.mts'
-import type { CanonicalGitRead } from './git.mts'
-
-export function canonicalMemberSlug(root: string): string | undefined {
- const remote = canonicalGitText(root, ['remote', 'get-url', 'origin'])?.trim()
- // Accept the three GitHub transports, retaining the organization segment.
- const match =
- remote &&
- /^(?:git@github\.com:|https:\/\/github\.com\/|ssh:\/\/git@github\.com\/)([^/]+)\/([^/]+?)(?:\.git)?\/?$/iu.exec(
- remote,
- )
- if (!match) {
- return undefined
- }
- const member = fleetRosterJson.repos.find(
- repo => repo.name.toLowerCase() === match[2]!.toLowerCase(),
- )
- if (!member) {
- return undefined
- }
- const owners =
- 'owner' in member && typeof member.owner === 'string'
- ? [member.owner]
- : SOCKET_GITHUB_ORGS
- return owners.some(owner => owner.toLowerCase() === match[1]!.toLowerCase())
- ? member.name
- : undefined
-}
-export function canonicalOriginAllowed(root: string): boolean {
- return canonicalMemberSlug(root) !== undefined
-}
-export function canonicalProducerAllowed(root: string): boolean {
- return isFleetPackProducerSlug(canonicalMemberSlug(root))
-}
-export function findCanonicalProducer(memberRoot: string): string | undefined {
- const root = findWheelhouseRoot({ startDir: memberRoot })
- return root && canonicalProducerAllowed(root) ? root : undefined
-}
-
-function memberConfig(
- member: string,
- readGit: CanonicalGitRead,
-): Record | undefined {
- const source = canonicalGitText(
- member,
- ['show', 'HEAD:.config/repo/socket-wheelhouse.json'],
- readGit,
- )
- try {
- const parsed: unknown =
- source === undefined ? undefined : JSON.parse(source)
- return parsed !== null &&
- typeof parsed === 'object' &&
- !Array.isArray(parsed)
- ? (parsed as Record)
- : undefined
- } catch {
- return undefined
- }
-}
-
-function objectValue(value: unknown): Record {
- return value !== null && typeof value === 'object' && !Array.isArray(value)
- ? (value as Record)
- : {}
-}
-
-function memberLayers(member: string, readGit: CanonicalGitRead): string[] {
- const slug = canonicalMemberSlug(member)
- if (!slug) {
- return []
- }
- const config = memberConfig(member, readGit)
- if (!config || config['repoName'] !== slug) {
- return []
- }
- const kind = objectValue(config['repo'])['type']
- const capabilities = Object.entries(objectValue(config['capabilities']))
- .filter(([, scopes]) => Array.isArray(scopes) && scopes.length > 0)
- .map(([name]) => name)
- const layers = [`template/overrides/${slug}`]
- const generated = ['template/generated/conditional/npm']
- const release = objectValue(config['release'])
- if (release['github'] !== false) {
- generated.push('template/generated/conditional/github-release')
- layers.push('template/base/conditional/github-release')
- }
- for (let i = 0, { length } = capabilities; i < length; i += 1) {
- const capability = capabilities[i]!
- if (/^[a-z][a-z0-9-]*$/u.test(capability)) {
- if (capability !== 'npm') {
- generated.push(`template/generated/conditional/${capability}`)
- }
- layers.push(`template/base/conditional/${capability}`)
- }
- }
- if (kind === 'mono' || kind === 'solo') {
- layers.push(`template/${kind}`)
- }
- layers.push('template/base/universal')
- return [...generated, 'template/generated/universal', ...layers]
-}
-
-export function canonicalSourceAllowed(
- member: string,
- producer: string,
- commit: string,
- source: string,
- target: string,
- readGit: CanonicalGitRead = readCanonicalGit,
-): boolean {
- if (!canonicalPathIsSafe(source) || !canonicalPathIsSafe(target)) {
- return false
- }
- for (const layer of memberLayers(member, readGit)) {
- const candidate = `${layer}/${target}`
- if (readCanonicalTreeEntry(producer, commit, candidate, readGit)) {
- return source === candidate
- }
- }
- return false
-}
-
-export function canonicalEligibleSources(
- member: string,
- file: string,
- readGit: CanonicalGitRead = readCanonicalGit,
-): string[] {
- return canonicalPathIsSafe(file)
- ? memberLayers(member, readGit).map(layer => `${layer}/${file}`)
- : []
-}
diff --git a/.git-hooks/_shared/check-output.mts b/.git-hooks/_shared/check-output.mts
deleted file mode 100644
index 48855d5c..00000000
--- a/.git-hooks/_shared/check-output.mts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { readFileSync } from 'node:fs'
-import process from 'node:process'
-import { stripVTControlCharacters } from 'node:util'
-
-import { isDebugNs } from '@socketsecurity/lib-stable/debug/namespace'
-import { debugNs } from '@socketsecurity/lib-stable/debug/output'
-
-import { isMainModule } from '../../scripts/fleet/process/is-main-module.mts'
-
-export const CHECK_DEBUG_NS = '*'
-
-export function debugCheck(...args: unknown[]): void {
- if (isDebugNs(CHECK_DEBUG_NS)) {
- debugNs(CHECK_DEBUG_NS, ...args)
- }
-}
-
-export function showCheckOutput(status: number | null, output: string): void {
- if (!output) {
- return
- }
- if (
- status !== 0 ||
- // Keep incomplete gates, Node warning classes, and tool warning labels.
- /(?:NOT a pass|Warning:|\bwarn(?:ing)?s?\b)/i.test(
- stripVTControlCharacters(output).replace(/\b0 warnings?\b/gi, ''),
- )
- ) {
- process.stderr.write(output)
- } else if (isDebugNs(CHECK_DEBUG_NS)) {
- debugCheck(output.trimEnd())
- }
-}
-
-export function showCheckResult(result: {
- status: number | null
- stdout: string | Buffer | null
- stderr: string | Buffer | null
-}): void {
- showCheckOutput(result.status, `${result.stdout ?? ''}${result.stderr ?? ''}`)
-}
-
-if (isMainModule(import.meta.url)) {
- showCheckOutput(
- Number(process.argv[2]),
- readFileSync(process.argv[3]!, 'utf8'),
- )
-}
diff --git a/.git-hooks/_shared/commit-format.mts b/.git-hooks/_shared/commit-format.mts
deleted file mode 100644
index d2633752..00000000
--- a/.git-hooks/_shared/commit-format.mts
+++ /dev/null
@@ -1,146 +0,0 @@
-// Conventional Commits 1.0 header validation, shared by both enforcement
-// surfaces:
-// - the commit-message-format-guard PreToolUse hook (.claude/hooks/), which
-// inspects `git commit -m` tool calls at Claude Bash time, and
-// - the commit-msg git-stage backstop (.git-hooks/), which inspects the
-// subject regardless of how the commit was made (subprocess / worktree /
-// CI / test harness) — the layer the tool-call guard never sees.
-// Canonical home: .git-hooks/_shared/; the .claude/hooks/ guard imports this
-// cross-tree (the shared thing is this code, per the fleet "DRY across the two
-// hook trees" rule). This module is side-effect-free — it reads no stdin, spawns
-// nothing, and calls process.exit nowhere — so the git-stage hook can import it
-// without triggering the guard's stdin-reading `main()`.
-//
-// Spec: https://www.conventionalcommits.org/en/v1.0.0/
-
-// Real Claude-specific integration: "the Claude tool-call guard" names the
-// .claude/hooks/ PreToolUse guard, which only exists for Claude Code, not
-// generic agent guidance.
-// oxlint-disable-next-line socket/no-agent-brand-assumption -- real
-export const ALLOWED_TYPES = [
- 'build',
- 'chore',
- 'ci',
- 'docs',
- 'feat',
- 'fix',
- 'perf',
- 'refactor',
- 'revert',
- 'style',
- 'test',
-] as const
-
-export const ALLOWED_TYPE_SET: ReadonlySet = new Set(ALLOWED_TYPES)
-
-// Header form: [(scope)][!]:
-// - type: lowercase letters
-// - optional (scope) in parens
-// - optional `!` breaking-change marker
-// - `: ` separator (colon + space)
-// - non-empty description
-export const HEADER_RE = /^([a-z]+)(\([^)]+\))?(!)?: (.+)$/
-
-// Subjects git itself writes for non-`-m` commits. These are never
-// Conventional Commits and must not be format-blocked at the git-stage twin
-// (the Claude tool-call guard never sees them — they carry no inline -m
-// message). `git merge` → `Merge branch …`/`Merge pull request …`;
-// `git revert` → `Revert "…"`; autosquash → `fixup! …`/`squash! …`/`amend! …`.
-const AUTO_SUBJECT_RE = /^(?:(?:amend|fixup|squash)! |Merge\b|Revert\b)/
-
-/**
- * True when the subject is one git auto-generates for a merge / revert /
- * autosquash commit. Those are exempt from Conventional Commits enforcement.
- */
-export function isAutoGeneratedSubject(subject: string): boolean {
- return AUTO_SUBJECT_RE.test(subject.trim())
-}
-
-/**
- * Result of validating a single message header.
- *
- * - Kind: 'ok' — header passes
- * - Kind: 'no-type' — first line has no `: ` prefix at all
- * - Kind: 'bad-type' — first line has a `: ` prefix but word isn't
- * lowercase / not in the type set
- * - Kind: 'uppercase-type' — type letters are present but include uppercase
- * - Kind: 'empty-description' — header has `: ` but description is
- * empty/whitespace
- */
-export type HeaderCheck =
- | { kind: 'ok' }
- | { kind: 'no-type'; line: string }
- | { kind: 'bad-type'; line: string; type: string }
- | { kind: 'uppercase-type'; line: string; type: string }
- | { kind: 'empty-description'; line: string; type: string }
-
-export function validateHeader(line: string): HeaderCheck {
- // Quick pre-check: does the line look like a Conventional header at all?
- // We accept any leading word-token before `: ` for diagnosis even if the
- // case is wrong; the strict HEADER_RE then refines.
- const looseMatch = /^([A-Za-z]+)(\([^)]+\))?(!)?:\s*(.*)$/.exec(line)
- if (!looseMatch) {
- return { kind: 'no-type', line }
- }
- const type = looseMatch[1]!
- const desc = looseMatch[4]!
- // Type must be all-lowercase.
- if (type !== type.toLowerCase()) {
- return { kind: 'uppercase-type', line, type }
- }
- // Type must be in the allowed set.
- if (!ALLOWED_TYPE_SET.has(type)) {
- return { kind: 'bad-type', line, type }
- }
- // Strict format check (catches "feat:description" without space, etc.).
- const strictMatch = HEADER_RE.exec(line)
- if (!strictMatch) {
- // The loose pattern matched but the strict one didn't — that means
- // either the `: ` separator is missing the space, or the description
- // is empty.
- if (!desc.trim()) {
- return { kind: 'empty-description', line, type }
- }
- return { kind: 'no-type', line }
- }
- const description = strictMatch[4]!
- if (!description.trim()) {
- return { kind: 'empty-description', line, type }
- }
- return { kind: 'ok' }
-}
-
-/**
- * Build a context-appropriate suggestion for an invalid header. We look at the
- * user's input and propose ONE example of a valid replacement based on what
- * they typed.
- */
-export function suggestReplacement(check: HeaderCheck): string {
- if (check.kind === 'ok') {
- return ''
- }
- const text = check.line.trim()
- // Lowercase variant: try to recover the intent.
- if (check.kind === 'uppercase-type') {
- return `${check.type.toLowerCase()}: ${text.slice(text.indexOf(':') + 1).trim()}`
- }
- if (check.kind === 'bad-type') {
- // Suggest 'feat' as a generic recoverable type, keep the rest.
- const rest =
- text.slice(text.indexOf(':') + 1).trim() || 'describe the change'
- return `feat: ${rest}`
- }
- if (check.kind === 'empty-description') {
- return `${check.type}: describe the change`
- }
- // no-type: try to fold whatever the user typed into a feat header.
- const words = text.split(/\s+/).filter(Boolean)
- const first = (words[0] ?? '').toLowerCase()
- // If the first word looks like a noun (e.g. "parser", "extension"), use it
- // as a scope and keep the rest as the description.
- if (words.length >= 2 && /^[a-z][a-z0-9-]*$/.test(first)) {
- const rest = words.slice(1).join(' ')
- return `feat(${first}): ${rest}`
- }
- return `feat: ${text || 'describe the change'}`
-}
diff --git a/.git-hooks/_shared/commit-subject.mts b/.git-hooks/_shared/commit-subject.mts
deleted file mode 100644
index ac64c0a1..00000000
--- a/.git-hooks/_shared/commit-subject.mts
+++ /dev/null
@@ -1,74 +0,0 @@
-// Placeholder commit-subject detection, shared by both enforcement surfaces:
-// - the no-placeholder-commit-subject-guard PreToolUse hook (.claude/hooks/),
-// which inspects `git commit -m` tool calls, and
-// - the commit-msg git-stage backstop (.git-hooks/), which inspects the
-// subject regardless of how the commit was made (subprocess / worktree /
-// CI / test harness).
-// Canonical home: .git-hooks/_shared/; the .claude/hooks/ guard imports this
-// cross-tree (the shared thing is this code, per the fleet "DRY across the two
-// hook trees" rule).
-
-// Subjects that say nothing about the change — the fingerprint of a
-// test-harness / replayed / sandbox commit (a batch of `initial` commits once
-// reached a fleet repo's main). Matched case-insensitively against the whole
-// trimmed subject, after stripping one trailing period.
-const PLACEHOLDER_SUBJECTS = new Set([
- '.',
- 'changes',
- 'commit',
- 'fix',
- 'fixes',
- 'fixup',
- 'init',
- 'initial',
- 'initial commit',
- 'temp',
- 'test',
- 'tmp',
- 'update',
- 'updates',
- 'wip',
-])
-
-/**
- * The subject line of a commit message: the first non-blank, non-comment line.
- */
-export function commitSubject(message: string): string {
- return (
- message
- .split(/\r?\n/)
- .find(l => l.trim() && !l.trimStart().startsWith('#'))
- ?.trim() ?? ''
- )
-}
-
-/**
- * Why a commit subject is unusable, or `ok`.
- *
- * `empty` and `placeholder` are separated because they have DIFFERENT causes
- * and different fixes. An empty subject is almost never someone typing nothing:
- * it is a `-F` path that did not resolve, a truncated command line, or an
- * editor closed without saving. Telling that author their subject looks like
- * `wip` sends them looking for a denylist entry that was never involved.
- */
-export type CommitSubjectVerdict = 'empty' | 'ok' | 'placeholder'
-
-/**
- * Classify a commit subject. Strips a single trailing period and lowercases
- * before matching the denylist.
- */
-export function commitSubjectVerdict(subject: string): CommitSubjectVerdict {
- const norm = subject.trim().replace(/\.$/, '').trim().toLowerCase()
- if (!norm) {
- return 'empty'
- }
- return PLACEHOLDER_SUBJECTS.has(norm) ? 'placeholder' : 'ok'
-}
-
-/**
- * True when a commit subject is unusable, either kind. Callers that need to
- * explain WHICH problem it is read {@link commitSubjectVerdict} instead.
- */
-export function isPlaceholderSubject(subject: string): boolean {
- return commitSubjectVerdict(subject) !== 'ok'
-}
diff --git a/.git-hooks/_shared/cross-repo-expressions.mts b/.git-hooks/_shared/cross-repo-expressions.mts
deleted file mode 100644
index ac7fb2b1..00000000
--- a/.git-hooks/_shared/cross-repo-expressions.mts
+++ /dev/null
@@ -1,186 +0,0 @@
-import { fileURLToPath } from 'node:url'
-
-import type { AcornNode } from '../../.claude/hooks/fleet/_shared/ast/core.mts'
-import { normalizePath } from '@socketsecurity/lib-stable/paths/normalize'
-import {
- repositoryPathApi,
- resolveRepositoryPath,
-} from './repo-containment.mts'
-
-export interface RepositoryExpressionContext {
- root: string
- file: string
- bindings: Map
-}
-
-export function repositoryAstNode(value: unknown): AcornNode | undefined {
- return value !== null &&
- typeof value === 'object' &&
- typeof (value as AcornNode).type === 'string'
- ? (value as AcornNode)
- : undefined
-}
-
-export function repositoryExpressionName(node: AcornNode | undefined): string {
- if (!node) {
- return ''
- }
- if (node.type === 'Identifier') {
- return String(node['name'])
- }
- if (node.type === 'MetaProperty') {
- return 'import.meta'
- }
- if (node.type === 'MemberExpression' && !node['computed']) {
- return `${repositoryExpressionName(repositoryAstNode(node['object']))}.${repositoryExpressionName(repositoryAstNode(node['property']))}`
- }
- return ''
-}
-
-export function isRepositoryPathExpression(node: AcornNode): boolean {
- const name = repositoryExpressionName(repositoryAstNode(node['callee']))
- return (
- (node.type === 'CallExpression' &&
- // Match path namespace methods and named imports of the same operations.
- /^(?:(?:path|posix|win32)(?:\.(?:posix|win32))?\.)?(?:dirname|fileURLToPath|join|resolve)$/u.test(
- name,
- )) ||
- (node.type === 'NewExpression' && name === 'URL')
- )
-}
-
-function evaluateRepositoryName(
- node: AcornNode,
- context: RepositoryExpressionContext,
- visiting: Set,
-): string | undefined {
- const name = repositoryExpressionName(node)
- if (name === '__dirname' || name === 'import.meta.dirname') {
- return repositoryPathApi(context.file).dirname(context.file)
- }
- if (name === '__filename' || name === 'import.meta.url') {
- return context.file
- }
- if (node.type !== 'Identifier') {
- return undefined
- }
- if (!context.bindings.has(name)) {
- return name === 'REPO_ROOT' ? context.root : undefined
- }
- if (visiting.has(name)) {
- return undefined
- }
- const next = new Set(visiting)
- next.add(name)
- return evaluateRepositoryValue(context.bindings.get(name), context, next)
-}
-
-function evaluateRepositoryTemplate(
- node: AcornNode,
- context: RepositoryExpressionContext,
- visiting: Set,
-): string | undefined {
- const expressions = node['expressions'] as AcornNode[]
- const quasis = node['quasis'] as Array<{ value: { cooked: string } }>
- let value = quasis[0]?.value.cooked ?? ''
- for (let index = 0; index < expressions.length; index += 1) {
- const part = evaluateRepositoryValue(expressions[index], context, visiting)
- if (part === undefined) {
- return undefined
- }
- value += part + (quasis[index + 1]?.value.cooked ?? '')
- }
- return value
-}
-
-function evaluateRepositoryUrl(args: string[]): string | undefined {
- if (args[0]?.startsWith('file:')) {
- try {
- return fileURLToPath(args[0])
- } catch {
- return undefined
- }
- }
- if (!args[0] || !args[1] || /^[a-z]+:/iu.test(args[1])) {
- return undefined
- }
- return resolveRepositoryPath(
- repositoryPathApi(args[1]).dirname(args[1]),
- args[0],
- )
-}
-
-function evaluateRepositoryCall(
- node: AcornNode,
- context: RepositoryExpressionContext,
- visiting: Set,
-): string | undefined {
- const callee = repositoryExpressionName(repositoryAstNode(node['callee']))
- if (node.type === 'CallExpression' && callee === 'process.cwd') {
- return context.root
- }
- if (!isRepositoryPathExpression(node)) {
- return undefined
- }
- const values = (node['arguments'] as AcornNode[]).map(argument =>
- evaluateRepositoryValue(argument, context, visiting),
- )
- if (values.some(value => value === undefined)) {
- return undefined
- }
- const args = values as string[]
- if (node.type === 'NewExpression') {
- return evaluateRepositoryUrl(args)
- }
- if (callee.endsWith('fileURLToPath')) {
- return args[0]?.startsWith('file:') ? evaluateRepositoryUrl(args) : args[0]
- }
- const api = repositoryPathApi(context.root)
- if (callee.endsWith('dirname')) {
- return args[0] === undefined ? undefined : api.dirname(args[0])
- }
- const normalized = args.map(argument => normalizePath(argument))
- return callee.endsWith('resolve')
- ? api.resolve(context.root, ...normalized)
- : api.join(...normalized)
-}
-
-function evaluateRepositoryValue(
- node: AcornNode | undefined,
- context: RepositoryExpressionContext,
- visiting: Set,
-): string | undefined {
- if (!node) {
- return undefined
- }
- if (node.type === 'Literal' && typeof node['value'] === 'string') {
- return node['value']
- }
- if (node.type === 'TemplateLiteral') {
- return evaluateRepositoryTemplate(node, context, visiting)
- }
- if (node.type === 'BinaryExpression' && node['operator'] === '+') {
- const left = evaluateRepositoryValue(
- repositoryAstNode(node['left']),
- context,
- visiting,
- )
- const right = evaluateRepositoryValue(
- repositoryAstNode(node['right']),
- context,
- visiting,
- )
- return left === undefined || right === undefined ? undefined : left + right
- }
- return (
- evaluateRepositoryName(node, context, visiting) ??
- evaluateRepositoryCall(node, context, visiting)
- )
-}
-
-export function evaluateRepositoryExpression(
- node: AcornNode | undefined,
- context: RepositoryExpressionContext,
-): string | undefined {
- return evaluateRepositoryValue(node, context, new Set())
-}
diff --git a/.git-hooks/_shared/cross-repo.mts b/.git-hooks/_shared/cross-repo.mts
deleted file mode 100644
index 6c298b25..00000000
--- a/.git-hooks/_shared/cross-repo.mts
+++ /dev/null
@@ -1,303 +0,0 @@
-import { repositorySourceRoot } from './repository-source-root.mts'
-import path from 'node:path'
-import { fileURLToPath } from 'node:url'
-
-import { normalizePath } from '@socketsecurity/lib-stable/paths/normalize'
-import {
- offsetToLineCol,
- tryParse,
-} from '../../.claude/hooks/fleet/_shared/ast/core.mts'
-import type { AcornNode } from '../../.claude/hooks/fleet/_shared/ast/core.mts'
-import {
- evaluateRepositoryExpression,
- repositoryAstNode,
- repositoryExpressionName,
-} from './cross-repo-expressions.mts'
-import type { RepositoryExpressionContext } from './cross-repo-expressions.mts'
-import {
- containsRepositoryPath,
- findRepositoryRoot,
- repositoryContainsTarget,
- repositoryPathApi,
- repositoryRealPath,
- resolveRepositoryPath,
-} from './repo-containment.mts'
-
-export interface CrossRepositoryHit {
- lineNumber: number
- line: string
- matched: string
-}
-
-export interface CrossRepositoryAnalysis {
- hits: CrossRepositoryHit[]
- unproven: number[]
-}
-
-function repositoryPathLiteral(value: string): boolean {
- const normalized = normalizePath(value)
- return (
- value.startsWith('./') ||
- value.startsWith('.\\') ||
- // Recognize drive, relative, absolute, and local dependency prefixes.
- /^(?:[a-z]:\/|\.\.?\/|\/|file:|link:)/iu.test(normalized)
- )
-}
-
-function isRepositorySourceRead(name: string): boolean {
- const separator = name.indexOf('.')
- if (
- separator !== -1 &&
- !['fs', 'fsp', 'fsPromises', 'promises'].includes(name.slice(0, separator))
- ) {
- return false
- }
- const qualifiedMethod = name.slice(separator + 1)
- const method = qualifiedMethod.endsWith('Sync')
- ? qualifiedMethod.slice(0, -4)
- : qualifiedMethod
- return ['createReadStream', 'readFile', 'readlink'].includes(method)
-}
-
-function isKernelRuntimePath(value: string): boolean {
- // Match the two virtual kernel roots and every path below them.
- return /^\/(?:proc|sys)(?:\/|$)/u.test(normalizePath(value))
-}
-
-function repositoryAstChildren(node: AcornNode): AcornNode[] {
- const children: AcornNode[] = []
- const values = Object.values(node)
- for (let index = 0, { length } = values; index < length; index += 1) {
- const value = values[index]
- for (const candidate of Array.isArray(value) ? value : [value]) {
- const child = repositoryAstNode(candidate)
- if (child) {
- children.push(child)
- }
- }
- }
- return children
-}
-
-function collectRepositoryBindings(
- node: AcornNode,
- context: RepositoryExpressionContext,
-): void {
- if (node.type === 'VariableDeclarator') {
- const id = repositoryAstNode(node['id'])
- if (id?.type === 'Identifier') {
- const name = String(id['name'])
- context.bindings.set(
- name,
- context.bindings.has(name)
- ? undefined
- : repositoryAstNode(node['init']),
- )
- }
- }
- if (
- node.type === 'AssignmentExpression' ||
- node.type === 'UpdateExpression'
- ) {
- const target = repositoryAstNode(node['left'] ?? node['argument'])
- if (target?.type === 'Identifier') {
- context.bindings.set(String(target['name']), undefined)
- }
- }
- if (/^(?:ArrowFunction|Function)/u.test(node.type)) {
- for (const parameter of (node['params'] ?? []) as AcornNode[]) {
- if (parameter.type === 'Identifier') {
- context.bindings.set(String(parameter['name']), undefined)
- }
- }
- }
- for (const child of repositoryAstChildren(node)) {
- collectRepositoryBindings(child, context)
- }
-}
-
-function repositoryReferenceEscapes(
- value: string,
- base: string,
- root: string,
- physicalRoot: string,
-): boolean {
- const target = resolveRepositoryPath(
- base,
- value.replace(/^(?:file:|link:)/u, ''),
- )
- const modules = resolveRepositoryPath(root, 'node_modules')
- if (containsRepositoryPath(modules, target)) {
- return false
- }
- if (base !== root) {
- const resolvedPhysicalRoot = repositoryRealPath(physicalRoot)
- const resolvedTarget = repositoryRealPath(target)
- if (
- resolvedPhysicalRoot !== undefined &&
- resolvedTarget !== undefined &&
- containsRepositoryPath(resolvedPhysicalRoot, resolvedTarget)
- ) {
- return false
- }
- }
- if (root === physicalRoot) {
- return !repositoryContainsTarget(root, target)
- }
- const resolvedRoot = repositoryRealPath(root)
- const resolvedTarget = repositoryRealPath(target)
- return (
- resolvedRoot === undefined ||
- resolvedTarget === undefined ||
- !containsRepositoryPath(resolvedRoot, resolvedTarget)
- )
-}
-
-export function analyzeCrossRepositoryPaths(
- source: string,
- fileAbsPath: string,
- options?: { root?: string | undefined } | undefined,
-): CrossRepositoryAnalysis {
- const settings = { __proto__: null, ...options }
- const file = resolveRepositoryPath(
- settings.root ??
- findRepositoryRoot(fileURLToPath(import.meta.url)) ??
- path.dirname(fileURLToPath(import.meta.url)),
- fileAbsPath,
- )
- const physicalRoot =
- settings.root ??
- findRepositoryRoot(file) ??
- repositoryPathApi(file).dirname(file)
- const root = repositorySourceRoot(physicalRoot, file)
- const context: RepositoryExpressionContext = {
- root,
- file,
- bindings: new Map(),
- }
- const analysis: CrossRepositoryAnalysis = { hits: [], unproven: [] }
- const lines = source.split(/\r?\n/u)
- const seen = new Set()
- const unproven = new Set()
- function recordUnproven(start: number): void {
- const line = offsetToLineCol(source, start).line
- if (!unproven.has(line)) {
- unproven.add(line)
- analysis.unproven.push(line)
- }
- }
- function record(value: string, start: number, base: string): void {
- if (
- !repositoryPathLiteral(value) ||
- isKernelRuntimePath(value) ||
- !repositoryReferenceEscapes(value, base, root, physicalRoot) ||
- seen.has(start)
- ) {
- return
- }
- seen.add(start)
- const lineNumber = offsetToLineCol(source, start).line
- analysis.hits.push({
- lineNumber,
- line: lines[lineNumber - 1] ?? '',
- matched: value,
- })
- }
- function visitRepositorySink(node: AcornNode, base: string): boolean {
- if (node.type.startsWith('Import') || node.type.startsWith('Export')) {
- const target = repositoryAstNode(node['source'])
- if (target) {
- visitPathSink(target, base)
- return true
- }
- }
- if (node.type === 'CallExpression') {
- const name = repositoryExpressionName(repositoryAstNode(node['callee']))
- if (name === 'require' || isRepositorySourceRead(name)) {
- const argument = repositoryAstNode(
- (node['arguments'] as unknown[] | undefined)?.[0],
- )
- if (argument) {
- visitPathSink(argument, name === 'require' ? base : root)
- }
- return true
- }
- }
- return false
- }
- function visitPathSink(node: AcornNode, base: string): void {
- const value = evaluateRepositoryExpression(node, context)
- if (value === undefined) {
- recordUnproven(node.start)
- } else {
- record(value, node.start, base)
- }
- }
- function visit(node: AcornNode, base: string): void {
- if (visitRepositorySink(node, base)) {
- return
- }
- for (const child of repositoryAstChildren(node)) {
- visit(child, base)
- }
- }
- const ast = parseRepositorySource(source)
- if (ast) {
- collectRepositoryBindings(ast, context)
- visit(ast, repositoryPathApi(file).dirname(file))
- } else if (
- // Dependency manifests carry quoted paths and file/link values.
- /\.(?:json|toml|ya?ml)$/iu.test(file) ||
- path.basename(file) === '.gitmodules'
- ) {
- let offset = 0
- for (
- let lineIndex = 0, { length } = lines;
- lineIndex < length;
- lineIndex += 1
- ) {
- const line = lines[lineIndex]!
- if (!line.trimStart().startsWith('#')) {
- for (const match of line.matchAll(
- // Capture each local dependency value or quoted filesystem path.
- /(?:file:|link:)(?:[/\\]|[a-z]:[/\\]|\.\.?[/\\])[^\s'",}]+|(?:["'])(?:[a-z]:[/\\](?![*?"<>|:])|\.\.?[/\\])[^"'\r\n]+["']/giu,
- )) {
- // Remove only the opening and closing quote delimiters.
- const value = match[0].replace(/^["']|["']$/gu, '')
- record(
- value,
- Buffer.byteLength(source.slice(0, offset + match.index)),
- repositoryPathApi(file).dirname(file),
- )
- }
- }
- offset += line.length + 1
- }
- } else if (source.trim()) {
- recordUnproven(0)
- }
- analysis.hits.sort((left, right) => left.lineNumber - right.lineNumber)
- return analysis
-}
-
-export function scanRepositoryReferences(
- source: string,
- file: string,
-): CrossRepositoryHit[] {
- return analyzeCrossRepositoryPaths(source, file).hits
-}
-
-const repositoryAstCache = new Map>()
-function parseRepositorySource(source: string): ReturnType {
- if (repositoryAstCache.has(source)) {
- return repositoryAstCache.get(source)
- }
- const ast = tryParse(source)
- if (source.length <= 262_144) {
- if (repositoryAstCache.size >= 32) {
- repositoryAstCache.clear()
- }
- repositoryAstCache.set(source, ast)
- }
- return ast
-}
diff --git a/.git-hooks/_shared/external-issue-ref.mts b/.git-hooks/_shared/external-issue-ref.mts
deleted file mode 100644
index c02dd20f..00000000
--- a/.git-hooks/_shared/external-issue-ref.mts
+++ /dev/null
@@ -1,108 +0,0 @@
-// External-issue-ref matcher — shared by the commit-msg git-stage backstop
-// AND the Claude-side no-ext-issue-ref-guard. Gate-free (no Node-25 hard-exit,
-// unlike helpers.mts) so the Claude hook can import it on the operator's Node.
-// Flags foreign /# refs + github.com issue/PR URLs that
-// would auto-link a backref into an upstream maintainer's issue.
-
-// CRLF-tolerant line split. Inlined (not imported from helpers.mts) so this
-// module stays free of the Node-25 hard-exit gate that helpers.mts carries —
-// a Claude hook importing it must run on the operator's, possibly older, Node.
-function splitLines(text: string): string[] {
- return text.replace(/\r\n/g, '\n').split(/\r?\n/)
-}
-
-//
-// Foreign `/#` tokens (and full github.com issue/PR
-// URLs) auto-link on GitHub and post an `added N commits that reference
-// this issue` event back to the target. A fleet cascade of N commits =
-// N pings to a maintainer who never asked to be tagged. The canonical
-// CLAUDE.md "public-surface hygiene" block documents the policy; this
-// scanner makes it mechanical on the commit-msg git-stage so a foreign
-// ref can't slip past `--no-verify` or onto a subprocess / worktree /
-// CI commit the Bash-time no-ext-issue-ref-guard never sees.
-//
-// Canonical home: this module. The Claude-side
-// .claude/hooks/fleet/no-ext-issue-ref-guard/index.mts imports the same
-// matcher cross-tree so the two surfaces never diverge.
-//
-// Allowed (NOT reported):
-// - bare `#123` (resolves against the current repo — no cross-repo leak)
-// - `SocketDev/#` (same org — case-insensitive)
-// - `https://github.com/SocketDev/...`, same org
-//
-// Blocked (reported):
-// - `/#`
-// - `https://github.com///{issues,pull}/`
-
-export interface ExternalIssueRef {
- kind: 'token' | 'url'
- owner: string
- repo: string
- num: string
- raw: string
-}
-
-// Org allowlist — case-insensitive, stored lowercase for comparison.
-// GitHub resolves orgs case-insensitively in URLs and refs, so
-// `socketdev` / `SocketDev` / `SOCKETDEV` all name the same org.
-export const ALLOWED_ISSUE_REF_ORGS = new Set(['socketdev'])
-
-// Detect `/#` token. Owner and repo names follow
-// GitHub's rules: alphanumerics, dashes, underscores, dots (no leading
-// dot/dash). Permissive on boundaries since we pattern-match prose, not
-// validate canonical refs.
-//
-// (^|\s|\() — anchor at start, whitespace, or open paren so we don't
-// re-match the owner/repo fragment already inside a URL.
-// ([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?) — owner / repo
-// #(\d+) — issue/PR number
-// (?=\b|[\s.,;:)\]]|$) — terminate cleanly
-const OWNER_REPO_REF_RE =
- /(?:^|\s|\()([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)\/([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)#(\d+)(?=\b|[\s.,;:)\]]|$)/g
-
-// Detect full GitHub issue/PR URLs to non-SocketDev orgs.
-const GITHUB_ISSUE_URL_RE =
- /https?:\/\/github\.com\/([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)\/([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)\/(?:issues|pull)\/(\d+)/g
-
-// Walk the text and collect every external-org reference. Returns an
-// empty array when the text only references same-repo (`#123`) or
-// SocketDev-owned (`SocketDev/socket-lib#42`) issues. Comment lines
-// (after leading whitespace, start with `#`) are skipped — git inlines
-// the diff snippet + "Please enter the commit message" hint there, and a
-// foreign ref quoted in that snippet isn't part of the authored message.
-export function scanExternalIssueRefs(text: string): ExternalIssueRef[] {
- const out: ExternalIssueRef[] = []
- for (const rawLine of splitLines(text)) {
- if (rawLine.trimStart().startsWith('#')) {
- continue
- }
- let m: RegExpExecArray | null
- OWNER_REPO_REF_RE.lastIndex = 0
- while ((m = OWNER_REPO_REF_RE.exec(rawLine)) !== null) {
- const owner = m[1]!
- const repo = m[2]!
- const num = m[3]!
- if (!ALLOWED_ISSUE_REF_ORGS.has(owner.toLowerCase())) {
- out.push({
- kind: 'token',
- owner,
- repo,
- num,
- raw: `${owner}/${repo}#${num}`,
- })
- }
- }
- GITHUB_ISSUE_URL_RE.lastIndex = 0
- while ((m = GITHUB_ISSUE_URL_RE.exec(rawLine)) !== null) {
- const owner = m[1]!
- const repo = m[2]!
- const num = m[3]!
- if (!ALLOWED_ISSUE_REF_ORGS.has(owner.toLowerCase())) {
- out.push({ kind: 'url', owner, repo, num, raw: m[0]! })
- }
- }
- }
- return out
-}
-
-// ── File classification ────────────────────────────────────────────
diff --git a/.git-hooks/_shared/file-scan.mts b/.git-hooks/_shared/file-scan.mts
deleted file mode 100644
index 613e2628..00000000
--- a/.git-hooks/_shared/file-scan.mts
+++ /dev/null
@@ -1,90 +0,0 @@
-// File classification + content reading for the git-hook scanners: which paths
-// to skip, and how to read a file's text (binaries run through `strings`).
-// Gate-free.
-
-import { existsSync, readFileSync, statSync } from 'node:fs'
-
-import { normalizePath } from '@socketsecurity/lib-stable/paths/normalize'
-import { spawnSync } from '@socketsecurity/lib-stable/process/spawn/child'
-
-// ── File classification ────────────────────────────────────────────
-
-// Files we never scan: hooks themselves (both the .mts files and the
-// shell shims under .git-hooks/), test fixtures, vendored lockfiles.
-const SKIP_FILE_RE =
- /\.(?:spec|test)\.(?:cts|m?[jt]s|mts|tsx?)$|\.example$|\/test\/|\/tests\/|fixtures\/|\.git-hooks\/|node_modules\/|pnpm-lock\.yaml/
-
-export const shouldSkipFile = (filePath: string): boolean =>
- SKIP_FILE_RE.test(filePath)
-
-/**
- * Source-code extensions. THE canonical definition for the fleet's source-only
- * convention scanners — the commit-time hook and the
- * `private-paths-are-absent-at-commit` check gate import this one constant so
- * they cannot disagree about what "source code" means.
- *
- * Markdown, docs, JSON, and YAML are deliberately out of scope: they reference
- * these patterns legitimately. That divergence was real — the check script
- * carried a private copy of this regex under a comment claiming it was
- * "lock-step with the hook", while the hook had no such constant and scanned
- * every non-skipped file. Generated JSON therefore passed the gate and failed
- * the hook, and the hook's suggested fix would have corrupted the data.
- */
-export const SOURCE_FILE_RE =
- /\.(?:[ch]|[cm]?[jt]sx?|bash|cc|cpp|cxx|go|hh|hpp|java|kt|py|rb|rs|sh|swift|zsh)$/
-
-export const isSourceCodeFile = (filePath: string): boolean =>
- SOURCE_FILE_RE.test(filePath)
-
-/**
- * The composed predicate for a SOURCE-ONLY convention scan: skip the universal
- * exclusions, then skip anything that is not source code.
- */
-export const shouldSkipSourceScan = (filePath: string): boolean =>
- shouldSkipFile(filePath) || !isSourceCodeFile(filePath)
-
-// Structured DATA payloads. A generated rule table, detector corpus, or
-// fixture blob legitimately CONTAINS the very strings a convention scanner
-// hunts for — a detection regex spelling `/Users/`, a rule description warning
-// about `npx` — and applying the scanner's suggested fix would corrupt the
-// data rather than repair a violation.
-const DATA_FILE_RE = /\.(?:jsonc?|ya?ml)$/
-// The one exception: a package manifest is data, but an `npx` in its `scripts`
-// really is the violation the rule exists to catch.
-const PACKAGE_MANIFEST_RE = /(?:^|\/)package\.json$/
-
-export const isStructuredDataFile = (filePath: string): boolean => {
- const p = normalizePath(filePath)
- return DATA_FILE_RE.test(p) && !PACKAGE_MANIFEST_RE.test(p)
-}
-
-// Returns file content as a string. Text files stay in-process; binaries run
-// through `strings` to catch paths embedded in WASM or compiled artifacts.
-// A NUL byte is the stable cross-platform binary signal for the artifacts this
-// hook scans, and avoids spawning Git-for-Windows `grep` for every text file.
-export const readFileForScan = (filePath: string): string => {
- if (!existsSync(filePath)) {
- return ''
- }
- try {
- if (statSync(filePath).isDirectory()) {
- return ''
- }
- } catch {
- return ''
- }
- let bytes: Buffer
- try {
- bytes = readFileSync(filePath)
- } catch {
- return ''
- }
- if (!bytes.includes(0)) {
- return bytes.toString('utf8')
- }
- // NUL-bearing binary — extract printable strings.
- const stringsResult = spawnSync('strings', [filePath], {
- encoding: 'utf8',
- })
- return stringsResult.stdout || ''
-}
diff --git a/.git-hooks/_shared/git-context-vars.mts b/.git-hooks/_shared/git-context-vars.mts
deleted file mode 100644
index e00df058..00000000
--- a/.git-hooks/_shared/git-context-vars.mts
+++ /dev/null
@@ -1,14 +0,0 @@
-/**
- * @file Git environment variables that override repository discovery.
- */
-export const GIT_CONTEXT_VARS: readonly string[] = [
- 'GIT_ALTERNATE_OBJECT_DIRECTORIES',
- 'GIT_CEILING_DIRECTORIES',
- 'GIT_COMMON_DIR',
- 'GIT_DIR',
- 'GIT_INDEX_FILE',
- 'GIT_NAMESPACE',
- 'GIT_OBJECT_DIRECTORY',
- 'GIT_PREFIX',
- 'GIT_WORK_TREE',
-]
diff --git a/.git-hooks/_shared/git-identity.mts b/.git-hooks/_shared/git-identity.mts
deleted file mode 100644
index 88bc9e7d..00000000
--- a/.git-hooks/_shared/git-identity.mts
+++ /dev/null
@@ -1,207 +0,0 @@
-// Git author/committer identity policy reader. The single source of truth is
-// the wheelhouse-cascaded config FILE, resolved repo-scoped only (no machine-
-// local fallback, by design — minimize outside-wheelhouse settings):
-//
-// .config/repo/socket-wheelhouse.json → "gitAuthors", per-repo, optional
-// .config/fleet/git-authors.json, cascaded fleet default
-//
-// The per-repo half is a SECTION of the member settings file rather than its
-// own JSON: per-repo config lives in ONE surface, and a second `.config/repo/
-// *.json` is the fragmentation `no-new-config-guard` refuses.
-//
-// Shape: { denylist: { emails[], names[] }, canonical: {name,email}, aliases[] }.
-//
-// Two checks, deliberately distinct:
-// - isDeniedIdentity: a placeholder/sandbox identity (test@example.com, Test,
-// empty) that is NEVER valid anywhere — the universal fleet denylist.
-// - isAllowedAuthor: when an allowlist (canonical/aliases) is configured, the
-// email must be in it. With no allowlist configured, only the denylist
-// applies (so a repo without a .config/repo allowlist still blocks junk).
-//
-// This is the .git-hooks/ copy; .claude/hooks/fleet/_shared/git-identity.mts is
-// a byte-equivalent copy for the other (separately-cascaded) hook tree — the
-// shared thing is the config file, not cross-tree code.
-
-import { existsSync, readFileSync } from 'node:fs'
-import path from 'node:path'
-
-export interface GitAuthor {
- readonly name?: string | undefined
- readonly email?: string | undefined
-}
-
-export interface IdentityPolicy {
- readonly denyEmails: readonly string[]
- readonly denyNames: readonly string[]
- readonly canonical: GitAuthor
- readonly aliases: readonly GitAuthor[]
-}
-
-interface RawConfig {
- denylist?:
- | { emails?: string[] | undefined; names?: string[] | undefined }
- | undefined
- canonical?: GitAuthor | undefined
- aliases?: GitAuthor[] | undefined
-}
-
-// The per-repo half lives as a SECTION of the one member settings file, not
-// as its own `.config/repo/*.json`. Per-repo config lives in ONE surface
-// (`config-segregation`); a second file there is exactly the fragmentation
-// `no-new-config-guard` exists to refuse.
-const REPO_SETTINGS = '.config/repo/socket-wheelhouse.json'
-const REPO_SETTINGS_KEY = 'gitAuthors'
-const FLEET_CONFIG = '.config/fleet/git-authors.json'
-
-function loadJson(file: string): RawConfig | undefined {
- if (!existsSync(file)) {
- return undefined
- }
- try {
- return JSON.parse(readFileSync(file, 'utf8')) as RawConfig
- } catch {
- return undefined
- }
-}
-
-/**
- * The `gitAuthors` section of the member settings file, or undefined when the
- * file or the section is absent.
- */
-function loadRepoSection(repoRoot: string): RawConfig | undefined {
- const settings = loadJson(path.join(repoRoot, REPO_SETTINGS)) as
- | { [REPO_SETTINGS_KEY]?: RawConfig | undefined }
- | undefined
- return settings?.[REPO_SETTINGS_KEY]
-}
-
-/**
- * Split a `git var GIT_AUTHOR_IDENT` / `GIT_COMMITTER_IDENT` string
- * (`Name `) into its name and email. A field git left
- * empty comes back as undefined rather than an empty string, so the denylist
- * and allowlist checks see the same "unset" shape either way.
- */
-export function parseGitIdentLine(ident: string): GitAuthor {
- const match = /^(.*?)\s*<([^>]*)>/.exec(ident)
- return {
- name: match?.[1]?.trim() || undefined,
- email: match?.[2]?.trim() || undefined,
- }
-}
-
-/**
- * One config's denylist halves, each defaulted to empty.
- */
-function readDenyEntries(config: RawConfig | undefined): {
- emails: string[]
- names: string[]
-} {
- return {
- emails: config?.denylist?.emails ?? [],
- names: config?.denylist?.names ?? [],
- }
-}
-
-/**
- * The union of both denylists, lowercased. A repo can ADD denied identities;
- * the fleet denylist always applies, so neither side can shorten the other.
- */
-function mergeIdentityDenyList(
- fleet: RawConfig | undefined,
- repo: RawConfig | undefined,
-): { denyEmails: string[]; denyNames: string[] } {
- const fleetDeny = readDenyEntries(fleet)
- const repoDeny = readDenyEntries(repo)
- return {
- denyEmails: [...fleetDeny.emails, ...repoDeny.emails].map(e =>
- e.toLowerCase(),
- ),
- denyNames: [...fleetDeny.names, ...repoDeny.names].map(n =>
- n.toLowerCase(),
- ),
- }
-}
-
-/**
- * The allowlist half, taken whole from the first config that declares one:
- * the repo override when it names a canonical email or any alias, else the
- * cascaded fleet default.
- */
-function selectIdentityAllowlist(
- fleet: RawConfig | undefined,
- repo: RawConfig | undefined,
-): { canonical: GitAuthor; aliases: GitAuthor[] } {
- const repoHasAllow = !!repo?.canonical?.email || !!repo?.aliases?.length
- const src = repoHasAllow ? repo! : (fleet ?? {})
- return {
- aliases: Array.isArray(src.aliases) ? src.aliases : [],
- canonical: src.canonical ?? {},
- }
-}
-
-/**
- * Resolve the identity policy: a repo override (.config/repo) takes precedence
- * over the cascaded fleet default (.config/fleet). The denylist merges both (a
- * repo can ADD denied identities but the fleet denylist always applies); the
- * allowlist is taken from the first config that declares a non-empty one.
- * `repoRoot` is the directory both config paths resolve against.
- */
-export function readIdentityPolicy(repoRoot: string): IdentityPolicy {
- const fleet = loadJson(path.join(repoRoot, FLEET_CONFIG))
- const repo = loadRepoSection(repoRoot)
- const { denyEmails, denyNames } = mergeIdentityDenyList(fleet, repo)
- const { aliases, canonical } = selectIdentityAllowlist(fleet, repo)
- return { denyEmails, denyNames, canonical, aliases }
-}
-
-/**
- * True when an identity is on the universal denylist — a placeholder email
- * (exact, or a `*@domain` whole-domain wildcard) or a placeholder name.
- */
-export function isDeniedIdentity(
- candidate: GitAuthor,
- policy: IdentityPolicy,
-): boolean {
- const email = candidate.email?.toLowerCase() ?? ''
- const name = candidate.name?.toLowerCase() ?? ''
- for (let i = 0, { length } = policy.denyEmails; i < length; i += 1) {
- const pat = policy.denyEmails[i]!
- if (pat.startsWith('*@')) {
- if (email.endsWith(pat.slice(1))) {
- return true
- }
- } else if (email === pat) {
- return true
- }
- }
- return !!name && policy.denyNames.includes(name)
-}
-
-/**
- * True when `candidate`'s email is the canonical identity or an alias. When no
- * allowlist is configured (empty canonical + aliases), returns true — only the
- * denylist gates that repo. A candidate with no email is treated as allowed
- * git fails on its own when no identity is set.
- */
-export function isAllowedAuthor(
- candidate: GitAuthor,
- policy: IdentityPolicy,
-): boolean {
- const email = candidate.email?.toLowerCase()
- if (!email) {
- return true
- }
- const hasAllowlist = !!policy.canonical.email || policy.aliases.length > 0
- if (!hasAllowlist) {
- return true
- }
- if (policy.canonical.email?.toLowerCase() === email) {
- return true
- }
- for (let i = 0, { length } = policy.aliases; i < length; i += 1) {
- if (policy.aliases[i]!.email?.toLowerCase() === email) {
- return true
- }
- }
- return false
-}
diff --git a/.git-hooks/_shared/git.mts b/.git-hooks/_shared/git.mts
deleted file mode 100644
index 6bc847c1..00000000
--- a/.git-hooks/_shared/git.mts
+++ /dev/null
@@ -1,66 +0,0 @@
-// Git subprocess wrappers for the hooks. Gate-free.
-//
-// Two flavors:
-//
-// git(...) — loose. Returns '' on failure. Used by callers that
-// legitimately tolerate a missing ref (e.g. probing
-// remote default-branch HEAD which may not be set up
-// locally) and provide their own fallback. Silent
-// by design — _shared/util.mts can't import the canonical
-// logger because it runs before the Node-version
-// gate has cleared, and a fire-and-forget dynamic
-// import races process exit. Callers that need to
-// know about failure should use gitOrThrow().
-//
-// gitOrThrow(...) — strict. Throws on either spawn error (git not on
-// PATH, EAGAIN, …) or non-zero exit. Used by gitLines
-// and every security-gate caller in pre-commit /
-// pre-push: if `git diff --cached --name-only` fails
-// we MUST refuse to greenlight the commit, not pass
-// it with "no files to check."
-//
-// gitLines goes through gitOrThrow because every call site we have
-// staged-file iteration, push-range walking, repo-toplevel lookup
-// makes a security or correctness decision based on the result; an
-// empty array from a failed git invocation is a fail-open.
-
-import { spawnSync } from '@socketsecurity/lib-stable/process/spawn/child'
-
-import { splitLines } from './scan-core.mts'
-
-export const git = (...args: string[]): string => {
- const result = spawnSync('git', args, { encoding: 'utf8' })
- return (result.stdout ?? '').trim()
-}
-
-export const gitOrThrow = (...args: string[]): string => {
- const result = spawnSync('git', args, { encoding: 'utf8' })
- if (result.error) {
- throw new Error(`git ${args.join(' ')}: ${result.error.message}`)
- }
- if (typeof result.status !== 'number' || result.status !== 0) {
- const err = result.stderr?.trim() || `exit ${result.status}`
- throw new Error(`git ${args.join(' ')}: ${err}`)
- }
- return (result.stdout ?? '').trim()
-}
-
-export const gitLines = (...args: string[]): string[] => {
- const out = gitOrThrow(...args)
- return out ? splitLines(out) : []
-}
-
-/**
- * Run git with `stdin` piped in, returning the raw status and stdout.
- *
- * Unlike `git`/`gitOrThrow`, this surfaces the exit status instead of
- * swallowing it or throwing — a query like `git check-ignore` uses its status
- * as the ANSWER (0 = matched, 1 = no match), not as a failure.
- */
-export function gitWithInput(
- args: readonly string[],
- stdin: string,
-): { status: number | undefined; stdout: string } {
- const result = spawnSync('git', [...args], { encoding: 'utf8', input: stdin })
- return { status: result.status ?? undefined, stdout: result.stdout ?? '' }
-}
diff --git a/.git-hooks/_shared/isolate-git-env.mts b/.git-hooks/_shared/isolate-git-env.mts
deleted file mode 100644
index bb71cba5..00000000
--- a/.git-hooks/_shared/isolate-git-env.mts
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * @file Neutralize the inherited git environment so a test's `git` spawns can
- * never touch the live repo. Importing this module runs the SAFE default
- * strip discovery vars, as a side effect; call `isolateGitEnv({ … })` for
- * the stronger variant. Why this is load-bearing: when a suite runs from the
- * pre-commit / pre-push hook, or just inherits the ambient env, git exports
- * `GIT_DIR` / `GIT_WORK_TREE` / `GIT_INDEX_FILE` pointing at THE LIVE repo,
- * and git honors those above cwd-based discovery. A fixture that does `git
- * init` + `git config user.email …` in a `cwd: tmpDir` then escapes onto the
- * real `.git/config` and HEAD — observed damage: `core.bare=true` (breaks
- * every worktree op with "must be run in a work tree"), a junk
- * `test@example.com` identity, and stray commits on the working branch. Two
- * consumers:
- *
- * - `node --test` git-fixture suites (`.git-hooks/fleet/test/*`, etc.) do NOT
- * load the vitest setup, so each imports this module — the side-effect
- * default (strip-only) stops the escape while leaving each fixture free to
- * scope its own `GIT_CONFIG_GLOBAL` per-spawn (the signing-gate tests need
- * that). `no-unisolated-git-fixture-guard` recognizes the import.
- * - vitest, via `setupFiles` (`test/fleet/scripts/setup.mts`), calls
- * `isolateGitEnv({ pinConfigToNull: true })` for the stronger form (no
- * fixture there manipulates a controlled global config). Lives in
- * `.git-hooks/_shared/` (alongside `git-identity.mts`) so the git-hook test
- * tree imports it within-tree; the vitest setup reaches it cross-tree (both
- * cascade together).
- */
-
-import process from 'node:process'
-import { GIT_CONTEXT_VARS } from './git-context-vars.mts'
-
-export interface IsolateGitEnvOptions {
- /**
- * Also pin `GIT_CONFIG_GLOBAL` / `GIT_CONFIG_SYSTEM` to `/dev/null` so `git
- * config` (without `--local`) can't reach a real config file at all.
- * Stronger, but it OVERRIDES any per-spawn global a fixture sets — only use
- * it where no fixture manipulates a controlled global config (vitest). The
- * strip alone already prevents escape; this is belt-and-suspenders.
- */
- pinConfigToNull?: boolean | undefined
-}
-
-/**
- * Strip the inherited git context vars (always). Optionally pin the config
- * files to `/dev/null`. Idempotent — safe to call or import more than once.
- */
-export function isolateGitEnv(options: IsolateGitEnvOptions = {}): void {
- for (let i = 0, { length } = GIT_CONTEXT_VARS; i < length; i += 1) {
- delete process.env[GIT_CONTEXT_VARS[i]!]
- }
- if (options.pinConfigToNull) {
- process.env['GIT_CONFIG_GLOBAL'] = '/dev/null'
- process.env['GIT_CONFIG_SYSTEM'] = '/dev/null'
- }
-}
-
-// Side-effect default for the bare `import '…/isolate-git-env.mts'` form: the
-// safe strip-only isolation. Consumers wanting the stronger pin call
-// isolateGitEnv({ pinConfigToNull: true }) explicitly.
-isolateGitEnv()
diff --git a/.git-hooks/_shared/logger-leaks.mts b/.git-hooks/_shared/logger-leaks.mts
deleted file mode 100644
index 9c8b6e80..00000000
--- a/.git-hooks/_shared/logger-leaks.mts
+++ /dev/null
@@ -1,189 +0,0 @@
-// Logger-leak matcher — shared by the commit-time scanLoggerLeaks
-// (.git-hooks/_shared/util.mts) and the edit-time logger-guard
-// (.claude/hooks/fleet/). Both flag direct `console.*` / `process.std*.write`
-// calls (the fleet rule: use getDefaultLogger()). Previously the commit side
-// used a REGEX and the edit side an AST walk — divergent engines that could
-// disagree (a regex flags `console.log` inside a string literal or comment;
-// the AST walk does not). This is the single AST-based source so they agree.
-//
-// AST (acorn-wasm) not regex: the parser is loaded for other commit-time
-// checks anyway, and it eliminates the string/comment false positives the
-// regex had. Gate-free: imports only the vendored acorn (no Node-25 exit), so
-// either hook tree can use it.
-
-import { findMemberCalls } from '../../.claude/hooks/fleet/_shared/ast/calls.mts'
-
-export interface LoggerLeak {
- // 1-based line of the call.
- line: number
- // Source text of the line, trimmed by the caller as needed.
- text: string
- // The dotted call, e.g. `console.log` / `process.stderr.write`.
- fullCall: string
- // Canonical logger replacement, e.g. `logger.info`.
- replacement: string
-}
-
-// The forbidden direct-output calls and their canonical logger replacement.
-// Two-segment (`console.log`) and three-segment (`process.stderr.write`)
-// chains — findMemberCalls handles both via a dotted `object`.
-export const FORBIDDEN_LOGGER_CALLS: ReadonlyArray<{
- object: string
- property: string
- replacement: string
-}> = [
- { object: 'console', property: 'debug', replacement: 'logger.debug' },
- { object: 'console', property: 'error', replacement: 'logger.error' },
- { object: 'console', property: 'info', replacement: 'logger.info' },
- { object: 'console', property: 'log', replacement: 'logger.info' },
- { object: 'console', property: 'warn', replacement: 'logger.warn' },
- { object: 'process.stderr', property: 'write', replacement: 'logger.error' },
- { object: 'process.stdout', property: 'write', replacement: 'logger.info' },
-]
-
-// Find every direct logger-leak call in `source` via the AST. Returns one entry
-// per call site with its line, the dotted call, and the canonical replacement.
-// Per-line `// oxlint-disable-next-line socket/no-console-prefer-logger` suppression is the CALLER's job
-// each tree applies its own marker semantics.
-export function findLoggerLeaks(source: string): LoggerLeak[] {
- const leaks: LoggerLeak[] = []
- for (let i = 0, { length } = FORBIDDEN_LOGGER_CALLS; i < length; i += 1) {
- const spec = FORBIDDEN_LOGGER_CALLS[i]!
- const matches = findMemberCalls(source, spec.object, spec.property)
- for (let j = 0, mlen = matches.length; j < mlen; j += 1) {
- const m = matches[j]!
- leaks.push({
- line: m.line,
- text: m.text,
- fullCall: `${spec.object}.${spec.property}`,
- replacement: spec.replacement,
- })
- }
- }
- return leaks
-}
-
-export interface LoggerDecoration {
- // 1-based line of the call.
- line: number
- // Source text of the line.
- text: string
- // The logger method called (e.g. `error`, `fail`, `log`).
- method: string
- // What kind of hand-rolled decoration leads the first argument.
- kind: 'glyph' | 'indent' | 'bullet'
- // For 'glyph': the leading glyph + the logger method that OWNS it, the fix.
- glyph: string | undefined
- ownerMethod: string | undefined
-}
-
-// Logger methods whose first string/template arg we inspect for a hand-rolled
-// prefix. Excludes structural methods (group/groupEnd/dir/table).
-const DECORATION_SCAN_METHODS = [
- 'error',
- 'fail',
- 'info',
- 'log',
- 'progress',
- 'skip',
- 'step',
- 'substep',
- 'success',
- 'warn',
-]
-
-// Status glyph → the logger method that OWNS it. The method renders the glyph;
-// hand-writing it double-marks, and skips theme-aware color. Keep in lockstep
-// with @socketsecurity/lib-stable/logger symbols-builder + the no-status-emoji rule.
-const GLYPH_OWNER: Readonly> = {
- '‼': 'warn',
- '×': 'fail',
- '√': 'success',
- '☑': 'success',
- '⚠': 'warn',
- '⛔': 'warn',
- '✅': 'success',
- '✓': 'success',
- '✔': 'success',
- '✖': 'fail',
- '✗': 'fail',
- '✘': 'fail',
- '❌': 'fail',
- '❎': 'fail',
- '❕': 'warn',
- '❗': 'warn',
- '🚨': 'warn',
- ℹ: 'info',
-}
-
-// Leading status glyph, after optional whitespace. One named capture, consumed
-// below to look up the owning method. Built dynamically from GLYPH_OWNER so the
-// table is the single source.
-const GLYPH_LEAD_RE = new RegExp(
- `^\\s*(?${Object.keys(GLYPH_OWNER)
- .map(g => g.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
- .join('|')})`,
-)
-// Leading indentation: a tab or 2+ spaces. The logger owns indentation via
-// group()/substep(); a hand-rolled indent fights it.
-const INDENT_LEAD_RE = /^(?: {2,}|\t)/
-// Leading bullet glyph — use logger.substep() for indented sub-items.
-const BULLET_LEAD_RE = /^\s*[•‣◦·]\s/
-
-// Find every `logger.(...)` call whose first argument's leading static
-// text is hand-rolled decoration the logger/group already owns — a status
-// glyph, leading indentation, or a bullet. Works for string literals AND
-// template literals (`\` ✗ ${x}\``). Per-line suppression is the CALLER's job.
-export function findLoggerDecoration(source: string): LoggerDecoration[] {
- const out: LoggerDecoration[] = []
- for (let i = 0, { length } = DECORATION_SCAN_METHODS; i < length; i += 1) {
- const method = DECORATION_SCAN_METHODS[i]!
- const calls = findMemberCalls(source, 'logger', method)
- for (let j = 0, clen = calls.length; j < clen; j += 1) {
- const call = calls[j]!
- const lead = call.firstArgLeadingText
- if (typeof lead !== 'string' || lead.length === 0) {
- continue
- }
- const glyphMatch = GLYPH_LEAD_RE.exec(lead)
- const glyph = glyphMatch?.groups?.['glyph']
- if (glyph) {
- out.push({
- __proto__: null,
- line: call.line,
- text: call.text,
- method,
- kind: 'glyph',
- glyph,
- ownerMethod: GLYPH_OWNER[glyph],
- } as LoggerDecoration)
- continue
- }
- if (BULLET_LEAD_RE.test(lead)) {
- out.push({
- __proto__: null,
- line: call.line,
- text: call.text,
- method,
- kind: 'bullet',
- glyph: undefined,
- ownerMethod: undefined,
- } as LoggerDecoration)
- continue
- }
- if (INDENT_LEAD_RE.test(lead)) {
- out.push({
- __proto__: null,
- line: call.line,
- text: call.text,
- method,
- kind: 'indent',
- glyph: undefined,
- ownerMethod: undefined,
- } as LoggerDecoration)
- }
- }
- }
- out.sort((a, b) => a.line - b.line)
- return out
-}
diff --git a/.git-hooks/_shared/node-version.mts b/.git-hooks/_shared/node-version.mts
deleted file mode 100644
index 5ec40c3f..00000000
--- a/.git-hooks/_shared/node-version.mts
+++ /dev/null
@@ -1,24 +0,0 @@
-import process from 'node:process'
-
-// Hard-fail if Node is below 24. This runs at module load — every hook
-// invocation imports this module before doing anything.
-const NODE_MIN_MAJOR = 24
-const nodeMajor = Number.parseInt(
- process.versions.node.split('.')[0] || '0',
- 10,
-)
-if (nodeMajor < NODE_MIN_MAJOR) {
- // This import-light shared helper does not own a logger. Use raw
- // process.stderr with ASCII (no status-emoji glyph) so the no-status-emoji
- // lint rule stays clean — the recommendation to use logger.fail() does not
- // apply when the entire branch is the logger-unavailable bail.
- // oxlint-disable-next-line socket/no-module-eval-side-effects -- floor bail
- process.stderr.write(
- `\x1b[0;31mHook requires Node >= ${NODE_MIN_MAJOR}.0.0 (have v${process.versions.node})\x1b[0m\n`,
- )
- // oxlint-disable-next-line socket/no-module-eval-side-effects -- floor bail
- process.stderr.write(
- 'Install Node 24+ — these hooks rely on default-on .mts type stripping.\n',
- )
- process.exit(1)
-}
diff --git a/.git-hooks/_shared/personal-path.mts b/.git-hooks/_shared/personal-path.mts
deleted file mode 100644
index 37d39b19..00000000
--- a/.git-hooks/_shared/personal-path.mts
+++ /dev/null
@@ -1,57 +0,0 @@
-// Personal-path leak matcher — shared by the commit-time scanPersonalPaths
-// (.git-hooks/_shared/util.mts) and the edit-time personal-path-guard
-// (.claude/hooks/fleet/). Both surfaces import THESE regexes + helpers so the
-// two can't drift, which lock-step inline copies cannot promise. Gate-free
-// (no Node-25 hard-exit like helpers.mts) so the Claude hook can import it on
-// the operator's possibly-older Node.
-//
-// Flags a hardcoded USERNAME leak: /Users//, /home//,
-// C:\Users\\. Username-free forms (`~/`, `$HOME/`) are the OPPOSITE — the
-// recommended shapes — and are NOT flagged. Pure-placeholder lines
-// (/Users//, $USER) are documentation, not leaks.
-
-// Real personal paths to flag. NFKC-normalize the line before matching (the
-// caller does this) so full-width / ligature variants of `/Users` don't slip
-// past the ASCII-only class.
-export const PERSONAL_PATH_RE =
- /(?:\/Users\/[^/\s]+\/|\/home\/[^/\s]+\/|C:\\Users\\[^\\]+\\)/
-
-// Placeholder forms we ALLOW, documentation, not leaks: `<...>` components and
-// `$VAR` / `${VAR}` under the platform user dir. Canonical fleet style:
-// /Users//... /home//... C:\Users\\...
-export const PERSONAL_PATH_PLACEHOLDER_RE =
- /(?:\/Users\/<[^>]*>\/|\/home\/<[^>]*>\/|C:\\Users\\<[^>]*>\\|\/Users\/\$\{?[A-Z_]+\}?\/|\/home\/\$\{?[A-Z_]+\}?\/)/
-
-// Well-known CI / system home dirs whose "username" is a service account, not a
-// person, so the GitHub Actions and Ubuntu CI homes below are not personal
-// leaks. gh-aw's compiled `.lock.yml` emits tool-cache mounts under the runner
-// home; those are correct, not a leak. Matched as the path's username segment
-// only.
-// oxlint-disable-next-line socket/personal-path-placeholders -- `runner` is CI
-export const KNOWN_NON_PERSONAL_PATH_RE =
- /(?:\/Users\/(?:runner)\/|\/home\/(?:circleci|runner|ubuntu|vscode|vsts)\/)/
-
-// True when a line is a PURE placeholder: it matches the placeholder shape AND
-// nothing real remains after stripping every placeholder. Such lines are
-// documentation, so the scanners skip them. A line whose only "personal" paths
-// are well-known CI/system homes (e.g. `/home//` for a CI service
-// account) is also pure, because those usernames are not people.
-export function isPurePlaceholder(line: string): boolean {
- const hasPlaceholder = PERSONAL_PATH_PLACEHOLDER_RE.test(line)
- const hasCiHome = KNOWN_NON_PERSONAL_PATH_RE.test(line)
- if (!hasPlaceholder && !hasCiHome) {
- return false
- }
- let stripped = line.replace(new RegExp(PERSONAL_PATH_PLACEHOLDER_RE, 'g'), '')
- stripped = stripped.replace(new RegExp(KNOWN_NON_PERSONAL_PATH_RE, 'g'), '')
- return !PERSONAL_PATH_RE.test(stripped)
-}
-
-// Rewrite the real personal paths on a line into the canonical placeholders, so
-// both surfaces print the same fix recipe.
-export function suggestPlaceholder(line: string): string {
- return line
- .replace(/\/Users\/[^/\s]+\//g, '/Users//')
- .replace(/\/home\/[^/\s]+\//g, '/home//')
- .replace(/C:\\Users\\[^\\]+\\/g, 'C:\\Users\\\\')
-}
diff --git a/.git-hooks/_shared/pkg-script-target.mts b/.git-hooks/_shared/pkg-script-target.mts
deleted file mode 100644
index a56514eb..00000000
--- a/.git-hooks/_shared/pkg-script-target.mts
+++ /dev/null
@@ -1,80 +0,0 @@
-/**
- * @file Resolve a package.json script to the node script it delegates to, so
- * the pre-commit hook can run that script directly instead of through `pnpm
- * run`. `pnpm` on PATH is the Socket Firewall shim: it boots the sfw proxy,
- * which boots pnpm, which re-resolves the workspace — seconds of startup
- * before the script's first line, many times the work a staged-scope gate
- * does, and the same wrapper whose deadlock the step budget exists to
- * survive. Prints the script path when the body is exactly `node ` —
- * that command runs identically under the repo-pinned node
- * `_shared/resolve-node.sh` already put on PATH. Prints nothing for any other
- * body (a `bun test`, a body carrying extra flags, a shell pipeline), so
- * those keep the wrapper: the hook must run what `pnpm run