diff --git a/.claude/hooks/guard-main-checkout.sh b/.claude/hooks/guard-main-checkout.sh index 09f9a191d8..d72df19294 100755 --- a/.claude/hooks/guard-main-checkout.sh +++ b/.claude/hooks/guard-main-checkout.sh @@ -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 </.git/worktrees/ → 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 < -b main - cd ../framework- && pnpm install + git worktree add ../${name}- -b main + cd ../${name}- && 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 diff --git a/AGENTS.md b/AGENTS.md index 75fecf8a28..cdd2f514c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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- -b main && cd ../framework- && 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- -b main && cd ../framework- && 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. --- diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..aa8db33739 --- /dev/null +++ b/CLAUDE.md @@ -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 ../- -b main && cd ../- && 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.