Skip to content
Merged
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
80 changes: 80 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# AGENTS.md — Codex / agent guidance for this repo

This file is read by AI coding agents (Codex, Claude Code, etc.). It duplicates the most
critical rules from CLAUDE.md in a format optimised for agent consumption.

## Non-negotiable rules

1. **Branch must contain all commits from main.** Before every push, run:
```bash
git fetch origin && git log --oneline origin/main ^HEAD
```
If that prints anything, rebase first: `git rebase origin/main`. Never push a branch
that is missing commits from `main`.

3. **One PR per branch.** Check `gh pr list --head <current-branch>` before creating a PR.
Push to the existing PR if one already exists.

4. **CI must be green before you stop.** After every push, poll `gh run list --limit 3`
until all checks complete. Fix failures before proceeding or declaring done.

5. **Never edit a test to make it pass.** Fix the code. Tests may only be changed when the
test itself is wrong or when the feature under test was intentionally changed.

6. **Always add unit tests for new behaviour.** Place tests in `__tests__/`. Unit tests live
in `__tests__/hooks/`, e2e tests in `__tests__/e2e/hooks/`.

7. **Docker is available.** Use `oven/bun:latest` with `--network=host` to do clean-install
end-to-end testing after every non-trivial implementation. See CLAUDE.md for the exact
Docker test recipe.

## Test commands

```bash
bun run test:run # unit tests (fast, run first)
bun run test:e2e # end-to-end hook tests (slower, run before push)
bun run lint # eslint
bunx tsc --noEmit # type check
```

## Docker smoke test (run after every change to src/hooks/ or package.json)

```bash
npm pack --ignore-scripts
docker run --rm --network=host \
-v $(pwd)/failproofai-*.tgz:/pkg.tgz \
oven/bun:latest bash -c "
apt-get update -qq && apt-get install -y -qq nodejs npm 2>&1 | tail -2
npm install -g /pkg.tgz --ignore-scripts 2>&1 | tail -3
cat > /tmp/tp.mjs << 'EOF'
import { customPolicies, allow } from 'failproofai';
customPolicies.add({ name: 't', description: 't', match: { events: ['PreToolUse'] }, fn: async () => allow() });
EOF
failproofai p -i -c /tmp/tp.mjs
"
rm failproofai-*.tgz
```

Expected: `Validated 1 custom hook(s): t`, exit 0.

## Regression checklist

Run through this mentally after any change to `src/hooks/` or `dist/` build:

- [ ] ESM import (`from 'failproofai'`) in custom policy resolves
- [ ] CJS require (`require('failproofai')`) in custom policy resolves
- [ ] Transitive local imports inside custom policy file work
- [ ] Builtin policies run when no custom file is configured
- [ ] `failproofai p -i -c <nonexistent>` fails gracefully (no crash)
- [ ] `bun run test:run` — all unit tests pass
- [ ] `bun run test:e2e` — all e2e tests pass
- [ ] CI is green

## Key files

| File | Purpose |
|------|---------|
| `src/hooks/loader-utils.ts` | `findDistIndex()`, ESM shim, import rewriting |
| `src/hooks/custom-hooks-loader.ts` | Top-level custom hook loading orchestrator |
| `src/index.ts` | Public API → `dist/index.js` bundle entry |
| `package.json` | `files` must include `dist/`; `build` must build `dist/index.js` |
137 changes: 137 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# CLAUDE.md — Agent guidance for this repo

## Environment

- **Runtime:** bun (≥1.3.0) and Node.js (≥20.9.0) are both present.
- **Docker CLI** is available. Use it to spin up clean containers that mimic real user
installs and validate every non-trivial change end-to-end before pushing.
- **Package manager:** bun (`bun install`, `bun run <script>`). Do not use npm/yarn to
install deps locally.

## Workflow rules

### One PR per branch
Each local branch maps to exactly one PR. Before opening a PR, check with
`gh pr list --head <branch>`. If one exists, push new commits to the same branch — never
open a second PR for the same branch.

### Branch must contain all commits from main
Before pushing, verify your branch is up to date with `main`:

```bash
git fetch origin
git log --oneline origin/main ^HEAD # should print nothing
```

If it prints commits, rebase before pushing:

```bash
git rebase origin/main
```

Resolve any conflicts, then continue. Never push a branch that is missing commits from
`main` — the PR diff will be polluted and CI may test against a stale base.

### CI must be green after every commit you push
After every `git push`, run `gh run watch` or poll `gh run list --limit 3` until all checks
finish. If any job fails, **stop and fix it before continuing**. Never leave a red CI.

The CI runs four jobs — all must pass:
| Job | Command |
|-----|---------|
| quality | lint + tsc + version-consistency check |
| test | `bun run test:run` (unit, 4 env configs) |
| build | `bun run build` (Next.js + dist/index.js) |
| test-e2e | `bun run test:e2e` |

### Always add unit tests for new behaviour
When you add or change logic, add a corresponding test in `__tests__/`. Never modify
existing tests just to make them pass — if a test breaks, fix the code, not the test.
Exception: updating a test that explicitly tests the value you're changing (e.g. a version
string or an error message you intentionally changed).

## Testing protocol

### After every implementation change

1. **Unit tests first** — fast, in-process:
```bash
bun run test:run
```

2. **Local smoke test** — use the dev dist directly:
```bash
bun build --target=node --format=cjs --outfile=dist/index.js src/index.ts
FAILPROOFAI_DIST_PATH=$(pwd)/dist failproofai p -i -c <policy-file>
```

3. **Docker clean-install test** — mimics a real `npm install -g` from scratch.
Use the `oven/bun:latest` image (bun pre-installed) with `--network=host`:

```bash
# Pack without running the full build
npm pack --ignore-scripts

docker run --rm --network=host \
-v $(pwd)/failproofai-*.tgz:/pkg.tgz \
oven/bun:latest bash -c "
apt-get update -qq && apt-get install -y -qq nodejs npm 2>&1 | tail -2
npm install -g /pkg.tgz --ignore-scripts 2>&1 | tail -3
cat > /tmp/test-policy.mjs << 'EOF'
import { customPolicies, allow } from 'failproofai';
customPolicies.add({
name: 'smoke-test',
description: 'Smoke test',
match: { events: ['PreToolUse'] },
fn: async (ctx) => allow(),
});
EOF
failproofai --version
failproofai p -i -c /tmp/test-policy.mjs
"

rm failproofai-*.tgz
```

Expected output includes `Validated 1 custom hook(s): smoke-test` and exit 0.

4. **E2E tests** (before pushing):
```bash
bun run test:e2e
```

### Regression areas to always check

After any change to `src/hooks/`, verify these scenarios don't regress:

| Scenario | How to check |
|----------|-------------|
| Custom policy with `from 'failproofai'` ESM import | Docker clean-install test above |
| Custom policy with `require('failproofai')` CJS | Write a `.js` test file with `require` and run `p -i -c` |
| Transitive local imports in custom policy | Use `examples/policies-advanced/index.js` |
| Builtin policies still fire (no custom file) | `failproofai p -i` without `-c` |
| `findDistIndex()` fallback when `FAILPROOFAI_DIST_PATH` unset | Unset the var and test |
| `loadCustomHooks` fail-open (bad file path) | Pass a nonexistent file without `--strict` |

## Project structure cheatsheet

```
bin/failproofai.mjs Entry point (bun shebang); sets FAILPROOFAI_DIST_PATH
src/hooks/
custom-hooks-loader.ts Orchestrates temp-file creation + dynamic import
loader-utils.ts findDistIndex(), createEsmShim(), rewriteFileTree()
custom-hooks-registry.ts globalThis registry shared between loader and handler
policy-helpers.ts allow() / deny() / instruct()
handler.ts Called by Claude Code --hook events
manager.ts policies --install / --uninstall / list
src/index.ts Public API entry point → compiled to dist/index.js
dist/index.js CJS bundle (built by `bun run build`; shipped in npm pkg)
__tests__/ Unit + e2e tests (vitest)
examples/ Sample custom policy files
```

## Version bumps

When bumping the version, update **only** `package.json` (root). The CI version-consistency
check compares `packages/*/package.json` against root — that directory does not currently
exist, so no other files need updating.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "failproofai",
"version": "0.0.1-beta.10",
"version": "0.0.1-beta.11",
"description": "Open-source hooks, policies, and project visualization for Claude Code & Agents SDK",
"bin": {
"failproofai": "./bin/failproofai.mjs"
Expand All @@ -11,6 +11,7 @@
"scripts/",
"lib/",
".next/standalone/",
"dist/",
"README.md"
],
"engines": {
Expand All @@ -20,7 +21,7 @@
"scripts": {
"predev": "bun link",
"dev": "FAILPROOFAI_TELEMETRY_DISABLED=1 bun scripts/dev.ts --port 8020",
"build": "bun --bun next build && node -e \"const {cpSync}=require('fs');cpSync('.next/static','.next/standalone/.next/static',{recursive:true});\"",
"build": "bun build --target=node --format=cjs --outfile=dist/index.js src/index.ts && bun --bun next build && node -e \"const {cpSync}=require('fs');cpSync('.next/static','.next/standalone/.next/static',{recursive:true});\"",
"prestart": "bun link",
"start": "FAILPROOFAI_TELEMETRY_DISABLED=1 bun scripts/start.ts",
"test": "vitest",
Expand Down