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
82 changes: 51 additions & 31 deletions .claude/hooks/guard-main-checkout.sh
Original file line number Diff line number Diff line change
@@ -1,47 +1,67 @@
#!/usr/bin/env bash
# guard-main-checkout.sh — PreToolUse guard enforcing AGENTS.md Prime Directive #11
# (worktree-first). Blocks Edit / Write / NotebookEdit while the session's checkout
# is on the shared `main` branch.
# (worktree-first). Blocks Edit / Write / NotebookEdit unless the file being edited
# lives in a dedicated git WORKTREE — not the shared primary checkout.
#
# Why: this repo is worked on by multiple agents in parallel. The shared `main`
# checkout has its HEAD switched and the tree reset *under you* by other agents,
# silently clobbering uncommitted edits (observed: a full session's work reverted
# twice). Dedicated per-task worktrees are physically isolated, so edits there are
# safe. This guard turns the documented discipline into a hard stop for the one
# place it actually fails — editing on `main`.
# Why: this repo (and siblings objectui/cloud) are edited by MULTIPLE agents at once.
# The shared primary checkout has its HEAD switched and its tree reset *under you* by
# other agents, silently clobbering uncommitted work. A feature branch on the shared
# checkout is NOT enough — it still gets switched under you. Only a dedicated per-task
# worktree is physically isolated.
#
# Deliberate exception (a human quick-fix that will still land via PR, never task
# work committed straight to main): export OS_ALLOW_MAIN_EDITS=1 for the session.
# Hardened vs the old guard (two holes agents fell through):
# 1. Checks "am I in a linked worktree?" — not merely "branch != main". Creating a
# feature branch on the shared checkout used to pass the guard; now it's blocked.
# 2. Checks the EDITED FILE's repo — not just $CLAUDE_PROJECT_DIR. So editing a
# sibling repo (objectui/cloud) on its shared checkout from this session is
# guarded too, instead of silently allowed.
#
# Deliberate exception (a human quick-fix that still lands via PR): OS_ALLOW_MAIN_EDITS=1.

set -uo pipefail

# Escape hatch.
if [ "${OS_ALLOW_MAIN_EDITS:-}" = "1" ]; then
exit 0
[ "${OS_ALLOW_MAIN_EDITS:-}" = "1" ] && exit 0

# PreToolUse passes the tool call as JSON on stdin; pull out tool_input.file_path.
input="$(cat 2>/dev/null || true)"
file=""
if command -v jq >/dev/null 2>&1; then
file="$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty' 2>/dev/null || true)"
fi
if [ -z "$file" ]; then
file="$(printf '%s' "$input" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/^.*"\([^"]*\)"$/\1/' || true)"
fi

dir="${CLAUDE_PROJECT_DIR:-$PWD}"
# Directory whose checkout we judge: the edited file's dir, else the project/cwd.
if [ -n "$file" ]; then chk="$(dirname "$file")"; else chk="${CLAUDE_PROJECT_DIR:-$PWD}"; fi

# Not a git repo (or git unavailable) → nothing to guard, allow.
branch="$(git -C "$dir" rev-parse --abbrev-ref HEAD 2>/dev/null)" || exit 0
# Resolve the git dir for that path. Not a git repo (or git missing) → nothing to guard.
gitdir="$(git -C "$chk" rev-parse --git-dir 2>/dev/null)" || exit 0

if [ "$branch" = "main" ]; then
root="$(git -C "$dir" rev-parse --show-toplevel 2>/dev/null || printf '%s' "$dir")"
cat >&2 <<EOF
⛔ Blocked: editing files while on the shared 'main' checkout (branch=main, root=$root).
# A linked worktree's git-dir lives under <common>/.git/worktrees/<name> → isolated → allow.
case "$gitdir" in
*/worktrees/*) exit 0 ;;
esac

This repo is worked on by multiple agents in parallel — the shared 'main' tree gets
its HEAD switched and reset under you, silently clobbering uncommitted edits.
# Otherwise this is the shared PRIMARY checkout → block regardless of branch.
root="$(git -C "$chk" rev-parse --show-toplevel 2>/dev/null || printf '%s' "$chk")"
branch="$(git -C "$chk" rev-parse --abbrev-ref HEAD 2>/dev/null || printf '?')"
name="$(basename "$root")"
cat >&2 <<EOF
⛔ Blocked: editing on the shared PRIMARY checkout, not a worktree.
repo: $root (branch: $branch)

Per AGENTS.md Prime Directive #11 (worktree-first), create a dedicated worktree and
re-run your edits from there:
Per AGENTS.md Prime Directive #11 (worktree-first): this repo is edited by multiple
agents at once — the shared checkout gets its HEAD switched and tree reset under you,
silently clobbering uncommitted work. A feature branch on the shared checkout is NOT
enough; you must be in a dedicated worktree:

git worktree add ../framework-<task> -b <branch> main
cd ../framework-<task> && pnpm install
git worktree add ../${name}-<task> -b <branch> main
cd ../${name}-<task> && pnpm install # then re-run your edits there

Deliberate exception (not task work): re-run with OS_ALLOW_MAIN_EDITS=1.
EOF
exit 2
fi
This guard now checks the edited file's OWN repo, so sibling repos (objectui / cloud)
are covered too — not just this project.

exit 0
Deliberate non-task exception: re-run with OS_ALLOW_MAIN_EDITS=1.
EOF
exit 2
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Other scripts: `objectui:bump` (pull only), `objectui:build`, `objectui:clean`.

When renaming a legacy var, use `readEnvWithDeprecation('OS_NEW', 'LEGACY')` from `@objectstack/types` (keeps legacy working one release). Third-party exceptions kept as-is: `NODE_ENV`, `HOME`, `OPENAI_API_KEY`, `TURSO_*`, OAuth `*_CLIENT_ID/SECRET`, `RESEND_API_KEY`, `POSTMARK_TOKEN`, `AI_GATEWAY_*`, `SMTP_*`. See #1382.
10. **File issues for out-of-scope findings — don't silently expand scope or leave them buried.** When you hit a bug, gap, or unenforced capability that's unrelated to the current task, or too large to fix in scope, open a GitHub issue (`gh issue create`) with a clear repro/decision and link it from your PR. Corollary: **never advertise or demo a capability the runtime doesn't actually deliver** (declared ≠ enforced) — fix it, trim it, or file an issue, but don't fake coverage. Example: the spec declares 9 validation-rule types but the write-path validator enforces only 3 (`state_machine`/`script`/`cross_field`); the other 6 are tracked in #1475 rather than demoed in the showcase.
11. **Worktree-first — never edit on the shared `main` checkout.** This repo is edited by **multiple agents at once**; the shared `main` tree has its HEAD switched and reset *under you*, silently clobbering uncommitted work. Before your **first file edit**, you MUST be in a dedicated worktree on a feature branch: `git worktree add ../framework-<task> -b <branch> main && cd ../framework-<task> && pnpm install`. A PreToolUse hook (`.claude/hooks/guard-main-checkout.sh`) **enforces** this — it blocks `Edit`/`Write`/`NotebookEdit` whenever HEAD is on `main` (override for a deliberate non-task fix with `OS_ALLOW_MAIN_EDITS=1`). Full playbook below.
11. **Worktree-first — never edit on the shared `main` checkout.** This repo is edited by **multiple agents at once**; the shared `main` tree has its HEAD switched and reset *under you*, silently clobbering uncommitted work. Before your **first file edit**, you MUST be in a dedicated worktree on a feature branch: `git worktree add ../framework-<task> -b <branch> main && cd ../framework-<task> && pnpm install`. A PreToolUse hook (`.claude/hooks/guard-main-checkout.sh`) **enforces** this — it blocks `Edit`/`Write`/`NotebookEdit` unless the edited file is in a dedicated **worktree** — a feature branch on the *shared* checkout is **not** enough (it still gets switched under you) — and it checks the **edited file's own repo**, so sibling repos (`objectui`/`cloud`) you touch are covered too (override for a deliberate non-task fix with `OS_ALLOW_MAIN_EDITS=1`). Full playbook below.
12. **Contract-first — fix the metadata, not the runtime.** This is a metadata-driven framework: `packages/spec` is the one contract between metadata *producers* and the runtime/renderers that *consume* it. When a piece of metadata "doesn't work," ask **first**: *is it spec-compliant? is this the long-term-correct direction?* If the metadata is wrong, fix it at the **producer** and **reject it at authoring/publish** (validation / lint) so the error surfaces loudly — do **not** add a lenient alias or `??` fallback in the consumer (a node executor, the REST layer, a renderer) to tolerate off-spec input. A tolerant fallback fossilizes the wrong convention into a second de-facto contract, dilutes the spec, and hides the producer's bug — one strict contract beats N dialects. This is an **internal** contract (we own both ends), so "be liberal in what you accept" (Postel) does **not** apply — that's for untrusted boundaries. Change the **spec** only when the spec itself is genuinely wrong, and then deliberately (edit the Zod schema + migrate), never by accreting consumer-side fallbacks. The existing `cfg.filter ?? cfg.filters` / `cfg.objectName ?? cfg.object` in the flow executors are **debt to pay down, not a pattern to copy**. *Worked example:* an AI-authored `create_record` used `fieldValues` / `today()` / `{{trigger.record.id}}` while the executor reads `fields` / `{TODAY()}` / `{record.id}` → the fix was correcting the authoring skill + a publish-gate lint that rejects the wrong shape (cloud#688), **not** a `cfg.fields ?? cfg.fieldValues` runtime alias (framework#2419, rejected). Strengthens #5.

---
Expand Down
27 changes: 27 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# CLAUDE.md

**[AGENTS.md](./AGENTS.md) is the source of truth for working in this repo — read it.**
Its Prime Directives are binding. Do not rely on this file alone; the one rule that must
never be missed is inlined here because missing it corrupts other agents' work.

## ⛔ Worktree-first — before your FIRST file edit (AGENTS.md Prime Directive #11)

This repo — **and every sibling repo you touch (`objectui`, `cloud`)** — is edited by
**multiple agents at once**. The shared primary checkout has its HEAD switched and its
tree reset *under you*, silently clobbering uncommitted work. **A feature branch on the
shared checkout is NOT enough** — it still gets switched under you. You MUST be in a
**dedicated per-task worktree**:

```
git worktree add ../<repo>-<task> -b <branch> main && cd ../<repo>-<task> && pnpm install
```

Then make all edits there. This applies **per repo**: if a task spans `framework` and
`objectui`, create a worktree in *each*. A PreToolUse hook
(`.claude/hooks/guard-main-checkout.sh`) enforces this — it blocks `Edit`/`Write`/
`NotebookEdit` unless the edited file is in a linked worktree, and it checks the edited
file's own repo (so sibling repos are covered). Deliberate non-task exception:
`OS_ALLOW_MAIN_EDITS=1`. Follow the rule because it's correct, not because the hook fires.

See **AGENTS.md** for the full playbook: branch hygiene, the dev stack, PR flow, and the
rest of the Prime Directives.