diff --git a/.claude/cloud-bootstrap.sh b/.claude/cloud-bootstrap.sh index 4f1c6ae..4520468 100755 --- a/.claude/cloud-bootstrap.sh +++ b/.claude/cloud-bootstrap.sh @@ -109,8 +109,24 @@ fi if [[ -f package-lock.json ]]; then if [[ ! -f node_modules/.package-lock.json ]] || [[ package-lock.json -nt node_modules/.package-lock.json ]]; then - npm ci --no-audit --no-fund >/dev/null 2>&1 || + if npm ci --no-audit --no-fund >/dev/null 2>&1; then + # A repo whose .npmrc sets ignore-scripts=true skips Lefthook's + # postinstall, so install the hooks through `prepare`, skipped under + # the same condition as that postinstall: CI set and LEFTHOOK unset + # (a value of "", 0 or false counts as unset). + hooks_skipped=false + case "${CI:-}" in + '' | 0 | false) ;; + *) case "${LEFTHOOK:-}" in '' | 0 | false) hooks_skipped=true ;; *) ;; esac ;; + esac + if [[ "$hooks_skipped" == false && + "$(npm config get ignore-scripts 2>/dev/null)" == true ]]; then + npm run prepare --if-present >/dev/null 2>&1 || + toolchain_warn 'npm run prepare failed; git hooks are not installed' + fi + else toolchain_warn 'npm ci failed; node_modules is unavailable this session' + fi fi fi diff --git a/.gitattributes b/.gitattributes index 78f0bce..49aa178 100644 --- a/.gitattributes +++ b/.gitattributes @@ -41,6 +41,20 @@ package-lock.json -diff packages.lock.json -diff *.lock -diff +############################################################################### +# Append-only lists: one entry per line, order carries no meaning, and each +# list's checker compares sets (keyed lists fail on a key listed twice). A union +# merge keeps both sides' appended lines instead of conflicting. These paths +# exist only in claude-code-plugins; in other repositories they match nothing. +############################################################################### + +scripts/affected-tests-no-suite.txt merge=union +scripts/em-dash-purged-paths.txt merge=union +scripts/fragment-plugins.txt merge=union +scripts/outside-node-packages.txt merge=union +scripts/shared-copies.txt merge=union +scripts/skill-leaf-name-registry.txt merge=union + ############################################################################### # Binary: no line-ending conversion, no diff. ############################################################################### diff --git a/.gitleaks.toml b/.gitleaks.toml index 68e82ea..2ec95c9 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -1,8 +1,8 @@ # gitleaks: https://github.com/gitleaks/gitleaks # Config reference: https://github.com/gitleaks/gitleaks#configuration # -# Root-canonical policy: inherit the upstream default ruleset and add nothing -# repo-specific. Managed consumers do not edit this file. Intentional findings +# Root-canonical policy: inherit the upstream default ruleset and add only +# rules that are broadly valid, none repo-specific. Managed consumers do not edit this file. Intentional findings # use a repository-owned .gitleaksignore or inline `gitleaks:allow` comment. # Floor the engine version: silences the "no minVersion specified" debug @@ -12,3 +12,14 @@ minVersion = "v8.25.0" [extend] useDefault = true + +# GitHub App installation tokens in both formats: the classic `ghs_` + 36 +# characters and the stateless `ghs__`. The default +# `github-app-token` rule stops at the `_` after the app id and the default +# `jwt` rule's `\b` cannot match between `_` and `eyJ`. GitHub's recommended +# regex: https://github.blog/changelog/2026-05-15-github-app-installation-tokens-per-request-override-header/ +[[rules]] +id = "github-app-installation-token" +description = "GitHub App installation token, classic and stateless (ghs__) formats" +regex = '''ghs_[A-Za-z0-9._-]{36,}''' +keywords = ["ghs_"] diff --git a/REVIEW.md b/REVIEW.md new file mode 100644 index 0000000..d213590 --- /dev/null +++ b/REVIEW.md @@ -0,0 +1,110 @@ +# Review instructions + +Managed Code Review injects this file verbatim as the highest-priority +instruction block for every review agent. It does not expand `@` imports and +does not read a cited file into the prompt. It sees only the words printed +below (see "Depth"). A self-hosted or local review running the `review` +plugin reads this file too, and additionally follows its citations into the +`standards` repository for the full reasoned criterion behind each line. + +Review in this organization is split into two mutually exclusive scopes, +so a finding belongs to exactly one scope and is never reported twice. +The **code-review lane** (`pr-review`, and Managed Code Review) +reads this file and applies the code-review scope below. The **security +lane** (`pr-review-security`) runs from its own security-only prompt +rather than reading this file; its scope section below records the same +split for every surface that does read it, so the code-review lane +knows what to leave to the security lane, and so a self-hosted or local +review (for example a `review`-plugin security agent) applies the right +section. + +## Severity + +This organization's criteria (`conventions/review/` in `standards`) resolve +to three severities: **Critical**, **Important**, **Suggestion**. On this +surface's three markers: + +| SSOT severity | Managed marker | +| --- | --- | +| Critical | 🔴 Important | +| Important | 🔴 Important | +| Suggestion | 🟡 Nit | + +🟣 Pre-existing is not an SSOT severity. It is this surface's own +detection-time judgment of whether a finding predates the diff, orthogonal +to the three-tier vocabulary above and not something a criterion assigns. + +A finding tagged `blocking` is always 🔴 Important here, regardless of what +severity its underlying criterion would otherwise carry in context. +`blocking` is this file's own tag, not a severity a `conventions/review/` +criterion assigns. + +## Depth + +Managed Code Review cannot resolve a citation below: `@` imports are not +expanded and cited files are not read into the prompt. A self-hosted or +local review using the `review` plugin can follow one into the full +criterion, its severity nuance, and its sources. A citation is a path of the +form `conventions/review/.md#`, resolved relative to wherever +the `standards` checkout root is available: this repository's own root when +`standards` reviews itself, or the `--add-dir` mount root everywhere else. + +## Code-review lane scope + +This lane owns every review dimension except security: correctness, +design, conventions, error handling, observability, tests, and +documentation. On a repository whose CI runs the security lane (a +`.github/workflows/pr-review-security*.yml` workflow exists), it does +**not** report security findings, such as vulnerabilities, authorization +or tenancy gaps, credential exposure, or injection: every security +finding belongs exclusively to that lane and is omitted here even when a +hunk plainly contains one. +On a repository with no such workflow no security lane exists yet, and +suppressed findings would have no other reader: report security findings +under this lane too, applying the security-scope checks below. + +Always check: + +- A high-risk security action, such as authentication, an authorization + failure, a privilege change, or sensitive-data access, has a + corresponding audit-log + entry (`conventions/review/observability.md#logging`). This is an + observability completeness check on the logging seam; whether the action + itself is safe is the security lane's question. +- A change that writes two or more related records, files, or state + locations carries an atomicity mechanism spanning them: a transaction, + an atomic rename, a constraint, or a compensation step. An + interruption between steps must not leave state no code path expects + (`conventions/review/error-handling.md#atomicity`). +- No Dependabot pull request waits on a human. One whose updates are all + non-major, with GitHub Actions only from `actions/*`, `github/*` or + `anthropics/*`, and none in the policy's `reviewOnly` security-critical + list, merges once `ci-status` passes; any other, a major included, merges + on an AI review lane's `merge` verdict at its current head once + `ci-status` passes, and a verdict on a security-critical dependency cites + the threat-model control it checked. A change that adds an action from another + publisher says why no vendor action or `gh` or script step serves + (`components/dependabot-policy/README.md#merge-tiers-for-dependabot-pull-requests`). + +## Security lane scope + +This lane reports **only** security findings: vulnerabilities, missing +authorization or tenant scoping, exposed secrets or credentials, injection, +and their direct enablers. Everything else is out of scope here and is +owned by the code-review lane; omit it. That includes style, design, +correctness with no security impact, and test coverage. + +Always check: + +- A handler or endpoint that receives an object id (a path segment, body + field, or query parameter) checks the caller is authorized for that + specific record, not only that the id is well-formed. Tagged `blocking` + (`conventions/review/security.md#object-level-authorization`). +- A data-access path that reaches tenant-owned data carries an explicit + tenant scope. Tagged `blocking` + (`conventions/review/multi-tenancy.md#tenant-scoped-data-access`). +- No secret, token, or credential is added to tracked source or config. + Tagged `blocking` (`conventions/review/security.md#secrets-and-credentials`). +- A query or process invocation built from external input is parameterized, + never string-concatenated. Tagged `blocking` + (`conventions/review/security.md#trust-boundaries-and-injection`). diff --git a/lychee.toml b/lychee.toml index 60e8247..74fccd2 100644 --- a/lychee.toml +++ b/lychee.toml @@ -108,6 +108,10 @@ exclude = [ # pass was not enough evidence to drop it. A workstation gets 200 with # lychee's User-Agent. '^https?://help\.miro\.com/hc/en-us/articles/31624028247058/?([?#].*)?$', + # GitHub REST API endpoints: docs cite them as how a fact was fetched. The + # runner gets 403 (unauthenticated API limits), so a failure says nothing + # about the endpoint. + '^https://api\.github\.com/', # Loopback hosts: never reachable from the checker. The example.com/.org/.net # placeholder domains need no entry: lychee excludes them by default. '^https?://localhost',