Ctrl K
ComunidadGuías…

lefthook.yml

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
commit-msg:
  commands:
    no-ai-attribution:
      # The harness's system prompt tells the agent to append `Co-Authored-By: Claude`
      # + `Claude-Session:` to every commit. CLAUDE.md forbids both. Prose lost that
      # fight 332 times on main (32 of the last 100 commits), so this strips them.
      #
      # It STRIPS and exits 0 rather than failing: a hook that aborts a commit over a
      # trailer the harness inserted teaches people to reach for --no-verify. The gate
      # for the bypassed case is the `no-ai-attribution` CI job, which fails.
      # A human Co-Authored-By is left alone.
      run: bash scripts/no-ai-attribution.sh strip {1}

pre-push:
  # SCRIPTS, not commands, for every check that has no CI twin. lefthook (2.1.5 and 2.1.8, measured
  # in real clones) computes its push files from the CHECKED-OUT HEAD against that branch's upstream
  # (no upstream: origin/HEAD). With zero files it skips EVERY pre-push command — "(skip) no
  # matching push files", exit 0 — whatever refs are being pushed: an empty commit on an
  # already-pushed branch, a fresh branch carrying only an empty commit, a push of a branch you are
  # not standing on. The commands below have CI twins, so for them that is latency. The scripts
  # here do not, and a script ran in every measured case.
  # No `skip` or `only` on a script that reads pushed-ranges.sh: it takes its range from the pushed
  # refs, not the checkout, so a merge or rebase in progress does not change what it checks, and a
  # push made while a rebase is paused publishes like any other. (Its own code is read from the
  # checkout; a conflict in one of these files makes the hook fail and the push is refused.)
  # tests/pre-push-scripts.test.ts derives the set from this file and holds it.
  scripts:
    "push-lands-guard.sh":
      # A push to a branch whose pull request is already merged or closed lands nowhere, and git
      # reports success anyway. Fail-closed: an unreadable PR state blocks the push. Its backstop
      # for --no-verify is the scheduled sweep (.github/workflows/push-lands-sweep.yml).
      runner: sh
      use_stdin: true
    "public-repo-guard-meta.sh":
      # The customer-name class does NOT scan the tree — it scans commit
      # messages, which is where that leak actually happens and the one surface
      # that cannot be edited after a merge. So it needs its own invocation:
      # without this the class would be enforced nowhere at all locally, since
      # the scan above never looks at a message.
      #
      # Without ~/.lynox/private-names.re the class warns and stands down rather
      # than blocking: an unpushable hook is how people learn --no-verify.
      #
      # WHAT is pushed comes from .lefthook/pre-push/pushed-ranges.sh, shared by
      # all three range hooks: the pushed refs on stdin (hence use_stdin), a ref
      # the remote already has measured from the remote's sha (when this clone
      # has it), a new one from where it leaves origin/main. This hook used to take
      # `merge-base origin/main HEAD`..HEAD instead, so pushing a branch you are
      # not standing on (`git push origin other`) scanned nothing of `other`.
      #
      # That form also carried `|| echo HEAD`: a clone with no local origin/main
      # got an empty range, reported as "NO commits" and let through. The trade
      # is reversed on purpose. This class has no CI twin, so the empty range
      # was the only check there was, and it passed; a line in the output is not
      # a barrier. A new branch with no origin/main is now refused (exit 2) with
      # the one command that fixes it:
      #   git fetch origin main:refs/remotes/origin/main
      # A ref the remote has at a sha this clone also has needs no origin/main;
      # one at a sha this clone lacks is measured like a new branch, and with
      # no origin/main refused with the fetch of that ref. A push with nothing
      # to do (git hands no ref line) scans nothing; closed or unreadable stdin
      # is refused. pushed-ranges.sh lists each case.
      # The invocation lives in .lefthook/pre-push/public-repo-guard-meta.sh.
      runner: bash
      use_stdin: true
    "public-repo-guard-files.sh":
      # The same class over the other surface: the PATHS and CONTENT of the files
      # this push adds. The message half above cannot see them, so a file named
      # after a customer — or holding the name in its body — reached this PUBLIC
      # repo untouched as long as the message stayed neutral.
      #
      # ⛔ This runs at pre-push and MUST have no CI twin. It prints the offending
      # path, which is only safe on the operator's own machine; a public Actions
      # log printing a customer name is the exact failure that got an earlier
      # tree-scanning version of this class deleted. tests/public-repo-guard.test.ts
      # asserts that no workflow invokes `check-files` — keep it that way.
      #
      # Same ranges, same refusal, same reasons as the block above: both come
      # from pushed-ranges.sh.
      # The invocation lives in .lefthook/pre-push/public-repo-guard-files.sh.
      runner: bash
      use_stdin: true
    "lint-changed.sh":
      # eslint over the .ts/.svelte source each pushed ref changes, linted in a temporary checkout
      # of the pushed commit (scripts/lint-changed.mjs). Until this hook, eslint ran only in CI, so
      # a rule the typecheck does not know (eqeqeq) reached the merge queue red. A script, not a
      # command, for the reason at the top of this block, and because it needs the pushed refs:
      # as a command it linted HEAD, whatever was pushed. Ranges from pushed-ranges.sh. Exit 2 when
      # it could not check. Its CI twin is the `lint` job in ci.yml.
      runner: bash
      use_stdin: true
    "public-repo-guard-commits.sh":
      # The lines each commit this push ADDS, for an internal register id or wording that marks a
      # security finding as open. The tree scan in `commands` sees only HEAD; an id one commit adds
      # and a later one removes is not there and still ships. THIS hook is what prevents the
      # publication — once pushed, the commit is readable on the pull request. Its CI twin (the
      # required public-repo-guard job) only prevents the merge. A script, not a command, for the
      # reason at the top of this block. Its ranges come from pushed-ranges.sh, like the two above.
      # The invocation lives in .lefthook/pre-push/public-repo-guard-commits.sh.
      runner: bash
      use_stdin: true
    "corpus-structure-guard-commits.sh":
      # The files each commit this push ADDS OR CHANGES, read for the shape of an eval corpus (an
      # object with a `turns` or `gold` array), whatever the file is called. The pre-commit command
      # below reads the index; this hook reads every pushed commit, so a commit made without that
      # hook is still read before it is published. Its CI twin is a step in the required public-repo-guard job, which
      # prevents the merge. Ranges from pushed-ranges.sh; exit 2 when it could not check.
      # The invocation lives in .lefthook/pre-push/corpus-structure-guard-commits.sh.
      # No `skip` for merge or rebase: the range comes from the pushed refs, not the checkout, and
      # a push made while a rebase is paused publishes like any other.
      runner: bash
      use_stdin: true
  commands:
    security-scan:
      run: sh scripts/security-scan.sh
    default-on-inventory:
      # What runs when an operator configures nothing? v2.11.0 shipped a config
      # flag reading `?? true`, so a new HTML parser went live fleet-wide without
      # anyone deciding it. Pairs with the `default-on-inventory` CI job, which is
      # the gate for the bypassed case.
      run: bash scripts/default-on-inventory.sh
    public-repo-guard:
      # Block internal infra / control-plane / ops leaks (staging hostnames,
      # CP DB schema, SSH-as-root chains) from reaching this PUBLIC repo —
      # the class gitleaks doesn't catch. Scans the whole tracked tree; the
      # CI job re-runs it so `git push --no-verify` can't slip a leak past.
      run: bash scripts/public-repo-guard.sh
      skip:
        - merge
        - rebase
    drift-guard:
      # Catch the mechanizable slices of doc/code drift gitleaks + public-repo-
      # guard don't: committed merge-conflict markers, removed features (Telegram
      # /WhatsApp/MCP) reappearing in live docs, and dead `path/file` references
      # in CLAUDE.md / docs / READMEs. Re-run by CI so --no-verify can't bypass.
      run: bash scripts/drift-guard.sh
      skip:
        - merge
        - rebase
    positioning-guard:
      # Checks public copy against the internal positioning source.
      # Tightly scoped to copy surfaces.
      run: bash scripts/positioning-guard.sh
      skip:
        - merge
        - rebase

pre-commit:
  commands:
    # gitleaks/pattern-scan belong here, not pre-push: `--staged` and
    # `git diff --cached` only see content at COMMIT time (the index is empty at
    # push), so at pre-push they were no-ops. CI `gitleaks detect` is the real gate.
    gitleaks:
      run: gitleaks protect --staged --verbose
      skip:
        - merge
        - rebase
    pattern-scan:
      # Exit 1 = a staged change adds a credential-shaped line; exit 2 = the staged
      # diff could not be read. The script prints which, above this line.
      run: bash scripts/secret-pattern-scan.sh
      fail_text: "secret-pattern-scan refused the commit — see its output above"
    disclosure-vocabulary:
      # A WARNING, never a refusal: lines this commit adds that point at internal tracking
      # or mark work as put off ("filed rather than built here"). This repo is public, and
      # such a sentence tells every reader that a known gap exists. The script cannot tell
      # an honest note on a feature's limit from a disclosed gap in a check, so the author
      # decides; a refusal would teach rewording, and a reworded gap is harder to find.
      # Exit 2 only when the staged diff cannot be read. No CI twin, on purpose: a warning
      # in a CI log reaches nobody. added-lines-guard refuses only two narrow forms (a
      # `DEF-` register id, and a short list of phrases about security findings) at push
      # time and in CI.
      run: node scripts/disclosure-vocabulary-scan.mjs
      skip:
        - merge
        - rebase
    corpus-structure-guard:
      # Refuses a file this commit ADDS OR CHANGES whose content has the shape of an eval corpus — an object
      # with a `turns` or `gold` array — whatever it is called: `.gitignore` can only match names,
      # and a corpus built from real threads must not reach this public repo. A file named as JSON
      # that does not parse is refused too. Exit 1 = refused, 2 = the index could not be read.
      # Twins: the pre-push script above and a step in the public-repo-guard CI job.
      run: node scripts/corpus-structure-guard.mjs --staged
      skip:
        - merge
        - rebase
    typecheck:
      # A fresh worktree has no node_modules, and the typecheck then dies deep inside
      # tsc/pnpm output where the real cause is invisible. What the operator sees is not
      # "install your dependencies" — it is a failed commit, then `git push` creating an
      # EMPTY branch, then `gh pr create` saying "No commits between". Three documented
      # occurrences on 2026-08-20 alone, in three different sessions, against a rule that
      # is written in /track step 3 and in two memory files. Prose lost; this is the
      # mechanism. A loud one-line refusal beats a rule everyone has already read.
      run: |
        test -d node_modules || {
          echo ""
          echo "  ✋ node_modules is missing in this worktree — the typecheck cannot run."
          echo "     Run:  pnpm install --force"
          echo ""
          echo "     (A fresh worktree needs this once. Without it the commit fails here,"
          echo "      the push creates an empty branch, and gh pr create reports"
          echo "      \"No commits between\" — three sessions hit exactly that on 2026-08-20.)"
          echo ""
          exit 1
        }
        # Lower CPU priority only: the check runs in full and its exit code is passed through,
        # but it no longer crowds out other work on a busy machine.
        nice -n 10 npm run typecheck
      stage_fixed: true
    hex-guard:
      # Fail if a web-ui change introduces hardcoded 6-digit hex outside the
      # allowlisted asset-export / categorical paths.
      run: bash packages/web-ui/scripts/hex-guard.sh
      skip:
        - merge
        - rebase
    token-contract:
      # hex-guard allowlists app.css — "the token definitions themselves" — so
      # nothing guarded the VALUES in it. These primitives are painted on more
      # than one surface (this UI, and the lynox.ai site, a separate codebase),
      # so a change here is a change over there. Fail now, on your commit,
      # rather than by someone else weeks later.
      #
      # Fast local feedback only. The gate is .github/workflows/token-contract.yml
      # — `--no-verify` skips this hook, not that.
      glob:
        - "packages/web-ui/src/app.css"
        - "packages/web-ui/tokens.contract.json"
      run: node packages/web-ui/scripts/check-token-contract.mjs
      skip:
        - merge
        - rebase
    shape-contract:
      # The colour half of the contract stopped the two surfaces' TOKENS drifting.
      # Nothing stopped their SHAPES: a design system draws the chat turn, the tool
      # call, the inbox row and the state surface by hand, from reading these
      # components, and two of the four were wrong within a week of being drawn.
      # shapes.contract.json is that reading, written down; this asserts the
      # components still have the shapes it records.
      #
      # Globs every component it cites, not just the contract — the drift that matters
      # is someone restyling a shape here without knowing it is drawn over there.
      #
      # Fast local feedback only. The gate is the same workflow.
      glob:
        - "packages/web-ui/shapes.contract.json"
        - "packages/web-ui/src/lib/components/*.svelte"
        - "packages/web-ui/src/lib/primitives/*.svelte"
      run: node packages/web-ui/scripts/check-shape-contract.mjs
      skip:
        - merge
        - rebase