Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ task. Use `docs/agents/autonomous-loop.md` for the detailed Linear state
contract and queue-moving/worker loop. Use `docs/agents/repo-navigation.md`
for the repo map and common lookup paths.

**Never edit the `ziw-*` skills (`.agents/skills/ziw-*`) in this repo.** They are
synced in from the upstream `zaks-io/skills` repo (see AP-298), so any local edit
is silently overwritten on the next refresh and your change is lost. Do not
rewrite, hand-patch, or "fix" a skill here. If a skill needs to change, record the
needed change as a metadata-only note in the **AP-98 orchestrator friction log**
(`category: config-gap`, with the target skill named); the skills agent reviews
that log and applies fixes upstream. The same rule applies to any other
synced-in skill, not just `ziw-*`.

### Issue tracker

Linear, team prefix `AP-`. See `docs/agents/issue-tracker.md`.
Expand Down
52 changes: 31 additions & 21 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,25 +77,26 @@ pnpm hooks:install

### Quality

| Command | Purpose |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pnpm build` | Run `turbo run build`. |
| `pnpm check` | Run package `check` tasks where defined. |
| `pnpm lint` | Run `turbo run lint`, including repo policy checks. |
| `pnpm typecheck` | Run `turbo run typecheck`. |
| `pnpm typecheck:scripts` | Type-check the root `scripts/` decision-logic tier via `tsc` against `tsconfig.scripts.json`. |
| `pnpm test` | Run `turbo run test`. |
| `pnpm test:scripts` | Run Vitest tests for root `scripts/` helpers. |
| `pnpm test:coverage` | Run Vitest coverage across workspace projects. |
| `pnpm knip` | Run Knip unused file, dependency, and export checks. |
| `pnpm dupes` | Run jscpd copy-paste duplication gate over `apps/` and `packages/`. |
| `pnpm format` | Format code with Biome and Markdown with Prettier. |
| `pnpm format:code` | Format non-doc files with Biome. |
| `pnpm format:code:check` | Check non-doc formatting with Biome (no writes; fails if anything is unformatted). |
| `pnpm format:docs` | Format Markdown files with Prettier. |
| `pnpm format:docs:check` | Check Markdown formatting. |
| `pnpm verify` | Full CI-style local verification: code + docs format checks, Knip, duplication, lint, typecheck, tests, scripts typecheck, OpenAPI check, and DB check. |
| `pnpm ci:check` | Alias for `pnpm verify`. |
| Command | Purpose |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `pnpm build` | Run `turbo run build`. |
| `pnpm check` | Run package `check` tasks where defined. |
| `pnpm lint` | Run `turbo run lint`, including repo policy checks. |
| `pnpm typecheck` | Run `turbo run typecheck`. |
| `pnpm typecheck:scripts` | Type-check the root `scripts/` decision-logic tier via `tsc` against `tsconfig.scripts.json`. |
| `pnpm test` | Run `turbo run test`. |
| `pnpm test:scripts` | Run Vitest tests for root `scripts/` helpers. |
| `pnpm test:coverage` | Run Vitest coverage across workspace projects. |
| `pnpm test:coverage:strict` | Like `pnpm test:coverage` but `--force`-recomputes every workspace report so a stale turbo cache cannot serve a false green on the repo-wide threshold. Used by the `pre-push` gate. |
| `pnpm knip` | Run Knip unused file, dependency, and export checks. |
| `pnpm dupes` | Run jscpd copy-paste duplication gate over `apps/` and `packages/`. |
| `pnpm format` | Format code with Biome and Markdown with Prettier. |
| `pnpm format:code` | Format non-doc files with Biome. |
| `pnpm format:code:check` | Check non-doc formatting with Biome (no writes; fails if anything is unformatted). |
| `pnpm format:docs` | Format Markdown files with Prettier. |
| `pnpm format:docs:check` | Check Markdown formatting. |
| `pnpm verify` | Full CI-style local verification: code + docs format checks, Knip, duplication, lint, typecheck, tests, scripts typecheck, OpenAPI check, and DB check. |
| `pnpm ci:check` | Alias for `pnpm verify`. |

### Contracts And Database

Expand Down Expand Up @@ -210,8 +211,17 @@ deploy production from a laptop.
Lefthook is configured at the repo root. `pre-commit` runs Biome on staged
files, Prettier on staged Markdown, `gitleaks protect --staged --redact`, and
Turbo typecheck for packages affected since `origin/main` with a local `HEAD`
fallback. `pre-push` runs `pnpm knip` and `pnpm test:coverage` so dead-code and
global coverage checks run before local pushes.
fallback. `pre-push` runs the full `pnpm verify` (format, Knip, duplication,
lint, typecheck, tests, OpenAPI, and DB checks) plus `pnpm test:coverage:strict`
(cache-busted coverage) so a push matches the CI `Validate` gate before it
leaves the machine.

Hooks are installed by `pnpm install` (the `prepare` script) and by
`pnpm setup:worktree`. Installation is skipped only inside GitHub Actions
(`GITHUB_ACTIONS`) or when `SKIP_LEFTHOOK=1` is set; it is **not** skipped in
unattended agent VMs (Cursor / Codex) just because `CI=true`, so remote workers
still get the `pre-push` gate. If a worker bypasses hooks, run `pnpm verify` and
`pnpm test:coverage:strict` by hand before handing off a PR.

## Related Docs

Expand Down
8 changes: 5 additions & 3 deletions lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ pre-push:
# Cached — per-package checks are safe to cache, keep push fast.
run: pnpm verify
test-coverage:
# Per-workspace coverage reports are cacheable; the root merge/check stays
# uncached and enforces the repo-wide threshold on the latest artifacts.
run: pnpm test:coverage
# Force the per-workspace coverage run (`--force`) so a stale turbo cache
# cannot serve a false green: the root merge then enforces the repo-wide
# threshold on freshly recomputed artifacts, matching CI exactly. A cached
# `test:coverage` previously let a coverage-tipping diff push red (AP-136).
run: pnpm test:coverage:strict
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"test": "turbo run test",
"test:scripts": "vitest run --config vitest.scripts.config.ts",
"test:coverage": "node scripts/clean-coverage.mjs && turbo run test:coverage && node scripts/merge-coverage.mjs",
"test:coverage:strict": "node scripts/clean-coverage.mjs && turbo run test:coverage --force && node scripts/merge-coverage.mjs",
"typecheck": "turbo run typecheck",
"typecheck:scripts": "tsc -p tsconfig.scripts.json",
"verify": "pnpm format:code:check && pnpm format:docs:check && pnpm knip && pnpm dupes && turbo run lint typecheck test openapi:check db:check && pnpm typecheck:scripts && pnpm test:scripts"
Expand Down
45 changes: 32 additions & 13 deletions scripts/install-hooks.mjs
Original file line number Diff line number Diff line change
@@ -1,23 +1,42 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { pathToFileURL } from "node:url";

if (process.env.CI === "true" || process.env.SKIP_LEFTHOOK === "1") {
process.exit(0);
// Skip hook installation only when we are truly inside GitHub Actions (the hosted
// CI that runs `pnpm verify` itself), or when an operator opts out explicitly.
// We deliberately do NOT key off the generic `CI` env var: unattended agent VMs
// (Cursor / Codex background agents) run non-interactively and end up with
// `CI=true`, which previously suppressed hook install there too. That left remote
// workers with no `pre-push` gate, so they pushed red PRs that only CI caught.
export function shouldSkipInstall(env = process.env) {
return Boolean(env.GITHUB_ACTIONS) || env.SKIP_LEFTHOOK === "1";
}

const hooksPath = spawnSync("git", ["config", "--get", "core.hooksPath"], { encoding: "utf8" });
const installArgs = hooksPath.status === 0 && hooksPath.stdout.trim() !== "" ? ["install", "--force"] : ["install"];
const result = spawnSync("lefthook", installArgs, { stdio: "inherit" });
export function installHooks({ env = process.env, spawn = spawnSync, log = console.warn } = {}) {
if (shouldSkipInstall(env)) {
return { installed: false, skipped: true };
}

const hooksPath = spawn("git", ["config", "--get", "core.hooksPath"], { encoding: "utf8" });
const installArgs = hooksPath.status === 0 && hooksPath.stdout.trim() !== "" ? ["install", "--force"] : ["install"];
const result = spawn("lefthook", installArgs, { stdio: "inherit" });

if (result.status !== 0) {
const forcedResult = installArgs.includes("--force")
? result
: spawnSync("lefthook", ["install", "--force"], { stdio: "inherit" });
if (result.status !== 0) {
const forcedResult = installArgs.includes("--force")
? result
: spawn("lefthook", ["install", "--force"], { stdio: "inherit" });

if (forcedResult.status !== 0) {
console.warn("[install-hooks] lefthook install failed; hooks were not installed.");
process.exit(1);
if (forcedResult.status !== 0) {
log("[install-hooks] lefthook install failed; hooks were not installed.");
return { installed: false, skipped: false, failed: true };
}
}

return { installed: true, skipped: false };
}

process.exit(0);
const isMainModule = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
if (isMainModule) {
const outcome = installHooks();
process.exit(outcome.failed ? 1 : 0);
}
87 changes: 87 additions & 0 deletions scripts/install-hooks.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { describe, expect, it, vi } from "vitest";
import { installHooks, shouldSkipInstall } from "./install-hooks.mjs";

describe("shouldSkipInstall", () => {
it("installs in an unattended agent VM where only generic CI is set", () => {
// Cursor/Codex background agents run non-interactively with CI=true but are
// NOT GitHub Actions. They must still get the pre-push gate installed.
expect(shouldSkipInstall({ CI: "true" })).toBe(false);
});

it("skips inside GitHub Actions", () => {
expect(shouldSkipInstall({ GITHUB_ACTIONS: "true" })).toBe(true);
});

it("skips when an operator opts out with SKIP_LEFTHOOK", () => {
expect(shouldSkipInstall({ SKIP_LEFTHOOK: "1" })).toBe(true);
});

it("installs on a normal developer machine", () => {
expect(shouldSkipInstall({})).toBe(false);
});
});

describe("installHooks", () => {
function gitConfigMiss() {
return { status: 1, stdout: "" };
}

it("does not shell out when install is skipped", () => {
const spawn = vi.fn();
const result = installHooks({ env: { GITHUB_ACTIONS: "true" }, spawn, log: () => {} });

expect(result).toEqual({ installed: false, skipped: true });
expect(spawn).not.toHaveBeenCalled();
});

it("runs `lefthook install` when hooks should be installed", () => {
const spawn = vi
.fn()
.mockReturnValueOnce(gitConfigMiss()) // git config --get core.hooksPath
.mockReturnValueOnce({ status: 0 }); // lefthook install

const result = installHooks({ env: { CI: "true" }, spawn, log: () => {} });

expect(result).toEqual({ installed: true, skipped: false });
expect(spawn).toHaveBeenLastCalledWith("lefthook", ["install"], { stdio: "inherit" });
});

it("forces install when a custom core.hooksPath is configured", () => {
const spawn = vi
.fn()
.mockReturnValueOnce({ status: 0, stdout: ".husky\n" }) // core.hooksPath set
.mockReturnValueOnce({ status: 0 }); // lefthook install --force

installHooks({ env: {}, spawn, log: () => {} });

expect(spawn).toHaveBeenLastCalledWith("lefthook", ["install", "--force"], { stdio: "inherit" });
});

it("does not retry when the first --force attempt already failed", () => {
const spawn = vi
.fn()
.mockReturnValueOnce({ status: 0, stdout: ".husky\n" }) // core.hooksPath set -> first attempt is --force
.mockReturnValueOnce({ status: 1 }); // lefthook install --force fails
const log = vi.fn();

const result = installHooks({ env: {}, spawn, log });

expect(result).toEqual({ installed: false, skipped: false, failed: true });
// git config + one (already-forced) install attempt; no pointless second retry.
expect(spawn).toHaveBeenCalledTimes(2);
});

it("retries with --force and reports failure when install keeps failing", () => {
const spawn = vi
.fn()
.mockReturnValueOnce(gitConfigMiss()) // git config --get core.hooksPath
.mockReturnValueOnce({ status: 1 }) // lefthook install
.mockReturnValueOnce({ status: 1 }); // lefthook install --force
const log = vi.fn();

const result = installHooks({ env: {}, spawn, log });

expect(result).toEqual({ installed: false, skipped: false, failed: true });
expect(log).toHaveBeenCalledOnce();
});
});
Loading