diff --git a/.claude/hooks/block-push.mjs b/.claude/hooks/block-push.mjs index 51583678..57ccd720 100644 --- a/.claude/hooks/block-push.mjs +++ b/.claude/hooks/block-push.mjs @@ -3,22 +3,28 @@ // for explicit approval instead of letting agents run them silently. // Emits the documented `permissionDecision: "ask"` shape so Claude Code // shows the standard tool-permission prompt; the user approves or denies. +// +// `main` moves only via scripts/finalize-isolated-clone.sh and +// scripts/push-main.sh — never by hand. import { readFileSync } from "node:fs"; -// `main` matched as a whole bookmark token (not e.g. `main-feature`): +// `main` matched as a whole branch token (not e.g. `main-feature`): // `(?:\s+\S+)*\s+main(?:\s|$)` — any number of preceding args, then `main` -// at a whitespace boundary. +// (bare or as a `:main` refspec) at a whitespace boundary. const PATTERNS = [ { - re: /^\s*jj\s+bookmark\s+(?:set|move|create|delete|forget|rename|track|untrack)\b(?:\s+\S+)*\s+main(?:\s|$)/, - why: "modifies the main bookmark", + re: /^\s*git\s+branch\s+(?:-f|--force)\b(?:\s+\S+)*\s+main(?:\s|$)/, + why: "force-moves the main branch by hand", }, { - re: /^\s*jj\s+op\s+(?:restore|undo|abandon)\b/, - why: "rewinds jj operation history (can move main as a side effect)", + re: /^\s*git\s+reset\s+--hard\b/, + why: "hard-resets the current branch (moves main if it's checked out)", + }, + { + re: /^\s*git\s+push\b(?:\s+\S+)*\s+(?:\S*:)?main(?:\s|$)/, + why: "pushes a ref targeting main by hand", }, - { re: /^\s*jj\s+git\s+push\b/, why: "pushes via jj" }, { re: /^\s*git\s+push\b/, why: "bare git push" }, ]; diff --git a/.claude/hooks/require-jj-new.mjs b/.claude/hooks/require-jj-new.mjs deleted file mode 100644 index 52c9fcec..00000000 --- a/.claude/hooks/require-jj-new.mjs +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env node -// PreToolUse hook for Edit/Write/MultiEdit/NotebookEdit. -// -// Blocks file mutations when `@` already has a description — that means it -// was finalized and the next edit would silently amend the finished commit. -// The fix is `jj new` first to create a fresh working commit. -// -// Override: set JJ_AMEND=1 in the environment to bypass (e.g. intentional -// amend, or describe-first workflow where description is set up front). - -import { readFileSync } from "node:fs"; -import { execFileSync } from "node:child_process"; -import { dirname } from "node:path"; - -if (process.env.JJ_AMEND === "1") process.exit(0); - -let input; -try { - input = JSON.parse(readFileSync(0, "utf-8")); -} catch { - process.exit(0); -} - -const filePath = input?.tool_input?.file_path; -if (typeof filePath !== "string" || !filePath) process.exit(0); - -const dir = dirname(filePath); - -function jj(...args) { - try { - return execFileSync("jj", args, { - cwd: dir, - stdio: ["ignore", "pipe", "ignore"], - encoding: "utf-8", - timeout: 2000, - }); - } catch { - return null; - } -} - -const description = jj("log", "-r", "@", "-T", "description", "--no-graph"); -if (description === null) process.exit(0); // not in a jj repo - -const trimmed = description.trim(); -if (!trimmed) process.exit(0); // empty description — working commit, fine - -const firstLine = trimmed.split("\n")[0]; -process.stdout.write( - JSON.stringify({ - hookSpecificOutput: { - hookEventName: "PreToolUse", - permissionDecision: "deny", - permissionDecisionReason: - `mpeek policy: @ is the finished commit "${firstLine}" with a clean working copy. Run \`jj new\` to start a fresh commit before editing — otherwise this edit silently amends the finished commit. Override: set JJ_AMEND=1.`, - }, - }), -); -process.exit(0); diff --git a/.claude/settings.json b/.claude/settings.json index 73ad34f6..7b299fc1 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -6,12 +6,6 @@ "hooks": [ { "type": "command", "command": "node .claude/hooks/block-push.mjs" } ] - }, - { - "matcher": "Edit|Write|MultiEdit|NotebookEdit", - "hooks": [ - { "type": "command", "command": "node .claude/hooks/require-jj-new.mjs" } - ] } ] } diff --git a/.claude/skills/agent-review/SKILL.md b/.claude/skills/agent-review/SKILL.md index b137808a..99be8b49 100644 --- a/.claude/skills/agent-review/SKILL.md +++ b/.claude/skills/agent-review/SKILL.md @@ -29,8 +29,8 @@ failure mode (see [Failure modes](#failure-modes-name-them-in-the-brief)). The real gate is `yarn test:desktop:electron` in the main checkout — not a second LLM read. Orchestrator review: -1. `jj -R .worktrees/ diff --stat -r 'main..@'` — scope check. -2. `jj -R .worktrees/ diff -r 'main..@'` or `Read` each changed file. +1. `git -C .worktrees/ diff --stat main...HEAD` — scope check. +2. `git -C .worktrees/ diff main...HEAD` or `Read` each changed file. 3. Look for: cross-app touches, missing tests, scope creep, secrets, `webSecurity:false` / `sandbox:false` regressions, lazy `require('electron')`, `setTimeout` in main without `isDestroyed()` guard, top-level @@ -45,7 +45,7 @@ You are reviewing agent-produced work in clone .worktrees//. The agent claims it implemented . Verify the claim — do NOT trust it. ACCEPTANCE (all runtime, all must pass — no grep shortcuts): -1. `jj -R .worktrees/ diff --stat -r 'main..@'` — list files; flag any +1. `git -C .worktrees/ diff --stat main...HEAD` — list files; flag any touch outside the declared scope. 2. `yarn workspace @peek/desktop test:unit` from the clone — must pass; report numeric count. @@ -89,8 +89,8 @@ Reviewer subagents drift toward these. Naming them in the brief reduces it. a. echo > /tmp/mpeek-agent-name b. scripts/finalize-isolated-clone.sh c. If FF rejected: rebase IN THE CLONE - (jj -R .worktrees/ git fetch --remote origin - jj -R .worktrees/ rebase -s 'roots(::@ ~ ::main)' -d main) + (git -C .worktrees/ fetch origin + git -C .worktrees/ rebase origin/main) Then retry finalize. 3. After ALL targeted clones finalized: yarn test:desktop:electron # run_in_background, FULL suite (all 6 runners, ~3300+, ~10 min), canonical gate @@ -115,7 +115,7 @@ When tests are green but user approval is pending, the orchestrator must: (see the `peek` skill's tag model). 2. Surface to the user, in one short message: - The commit IDs landed on main - - The diff stat (`jj diff --stat -r 'main..@'` or per-commit) + - The diff stat (`git diff --stat main...HEAD` in the clone, or per-commit) - The test result (numeric pass count, runtime) - "Approve to mark done, or call out anything to revisit" 3. Wait for explicit "approve" / "ok mark done" / equivalent before @@ -132,24 +132,28 @@ sufficient. When `test:desktop:electron` fails after landing: 1. Identify which finalized commit caused the failure (often obvious from the - failing test's surface area; if not, `jj log main@tangled..main` + bisect by - `jj op restore`). -2. `jj abandon ` in main — this leaves `main` pointing one - commit back. The chaos-defense scripts will FF correctly on next finalize. + failing test's surface area; if not, `git log --oneline tangled/main..main` + and bisect by resetting the clone to earlier commits). +2. In the CLONE (never the main checkout) — + `git -C .worktrees/ reset --hard ` — + discards the clone's landed commit back to the last good state. Git has no + operation log to restore from, so this is destructive: uncommitted work in + the clone is gone. `main` already fast-forwarded past the bad commit and + stays there; the fix lands as a new commit on top rather than rewriting + history. 3. The clone still exists (preserved by finalize). SendMessage the **original agent** (not a fresh spawn) with: ``` Your landed but yarn test:desktop:electron failed: . The commit was abandoned from main. The clone at - .worktrees// still has your work. Diagnose, fix in the clone, - commit, and report back. Same brief as before re: do-not-finalize, - do-not-push. + relevant log lines>. The clone at .worktrees// has been reset back + to before your commit. Diagnose, fix in the clone, commit, and report back. + Same brief as before re: do-not-finalize, do-not-push. ``` 4. When the agent returns, run `/agent-review` on the updated diff. Why SendMessage and not a new Agent: the original agent has full context on what it did and why. A fresh spawn re-reads everything. The clone still has -its node_modules + jj state; no respawn overhead. +its node_modules; no respawn overhead. --- diff --git a/.claude/skills/fix/SKILL.md b/.claude/skills/fix/SKILL.md index 1a39b348..fc7821f6 100644 --- a/.claude/skills/fix/SKILL.md +++ b/.claude/skills/fix/SKILL.md @@ -1,6 +1,6 @@ --- name: fix -description: Spawn a background coding agent with mpeek repo rules baked in (jj not git, yarn scripts, one command per Bash call). Use when delegating a well-scoped fix or implementation to a background agent. +description: Spawn a background coding agent with mpeek repo rules baked in (yarn scripts, one command per Bash call). Use when delegating a well-scoped fix or implementation to a background agent. --- # /fix — Spawn background coding agent @@ -10,7 +10,6 @@ When the user invokes /fix with a description: 1. Spawn a **background coding agent** (`subagent_type=general-purpose`, `run_in_background=true`) with the task description. 2. Include these rules in the agent prompt: - - Use `jj` not `git` for version control - Use `yarn