diff --git a/.claude/skills/open-edit/SKILL.md b/.claude/skills/open-edit/SKILL.md index 9a67d8a..eaedc24 100644 --- a/.claude/skills/open-edit/SKILL.md +++ b/.claude/skills/open-edit/SKILL.md @@ -47,9 +47,17 @@ any of that means. ## PREFLIGHT — ALWAYS run at session start An installed skill contains this file plus `scripts/preflight.sh`; the full runtime may not exist yet. -Resolve **SKILL_ROOT** as the directory containing this `SKILL.md`, and **WORKSPACE** as the user's current -project root (or current directory outside a project). Resolve the supplied video to an absolute path -from WORKSPACE before changing working directories. +Resolve **SKILL_ROOT** as the directory containing this `SKILL.md`. Then resolve **WORKSPACE** by the first +rule that applies: + +1. **If SKILL_ROOT sits inside an Open Edit checkout, WORKSPACE is that checkout** — preflight reuses it, + and the run exercises that code. +2. **Otherwise** WORKSPACE is the user's current project root, or the current directory outside a project — + preflight creates its own runtime at `/.open-edit/runtime` and every step below runs there. + +Preflight names which of the two it resolved (`reusing the local checkout at …` or `will use a managed clone +at …`); read that line before trusting a run to be testing your changes. Resolve the supplied video to an +absolute path from WORKSPACE before changing working directories. At the start of EVERY session, before doing Open Edit work, run: ``` @@ -73,8 +81,12 @@ bash "$SKILL_ROOT/scripts/preflight.sh" --auto-approve --workspace "$WORKSPACE" approval from the original render request. If the user approves only selected actions, perform only those exact commands yourself, then rerun `--dry`. If nothing needs approval, do not mention preflight. -Exit **0** means stdout is **OPEN_EDIT_ROOT**; use it for every repo-relative command below. Exit **10** means -approval is required or an approved prerequisite is still missing. Exit **1** is a hard invariant/install error. +Exit **0** means stdout is **OPEN_EDIT_ROOT**; use it for every repo-relative command below. It does **not** +mean setup is finished — a `--dry` run exits 0 while listing the `WOULD APPLY LOCALLY` work that bare +preflight performs itself, and then ends on `not ready yet — run bare preflight …`. Read the final +`preflight:` line, not the exit code: `ready — OPEN_EDIT_ROOT=…` means go. Exit **10** means **only** that +`APPROVAL REQUIRED` was printed and the user must approve every listed action first. Exit **1** is a hard +invariant/install error. For development, `--repository --ref ` overrides the initial clone source. A managed clone records its origin, branch, and commit and rejects conflicting later overrides. A clean checkout is offered a fast-forward update; any local or untracked changes are reported and left untouched. diff --git a/.claude/skills/open-edit/hooks/install-project-hooks.mjs b/.claude/skills/open-edit/hooks/install-project-hooks.mjs index 94c2c61..b7c6775 100755 --- a/.claude/skills/open-edit/hooks/install-project-hooks.mjs +++ b/.claude/skills/open-edit/hooks/install-project-hooks.mjs @@ -25,14 +25,30 @@ async function readJson(path) { } } +// Identity is the hook PATH, not the exact command string. Comparing strings meant a config holding +// `bash codex` never matched the generated `bash "" codex`, so every run appended another +// copy — for anyone whose config was written by anything but this exact generator. +const isOurs = (group, hookPath) => + Array.isArray(group?.hooks) && group.hooks.some(hook => typeof hook?.command === 'string' + && hook.command.includes(hookPath)); + async function merge(path, event) { const document = await readJson(path); document.hooks ??= {}; document.hooks.SessionStart ??= []; - const groups = document.hooks.SessionStart; - const alreadyPresent = groups.some(group => - Array.isArray(group?.hooks) && group.hooks.some(hook => hook?.command === event.command)); - if (alreadyPresent) return; + let groups = document.hooks.SessionStart; + const mine = groups.filter(group => isOurs(group, relativeHook)); + if (mine.length > 0) { + // Collapse any duplicates a previous version accumulated; never touch anyone else's hooks. + if (mine.length === 1) return; + const first = mine[0]; + document.hooks.SessionStart = groups.filter(group => !isOurs(group, relativeHook) || group === first); + groups = document.hooks.SessionStart; + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, `${JSON.stringify(document, null, 2)}\n`); + console.error(`preflight: collapsed ${mine.length - 1} duplicate SessionStart hook(s) in ${relative(workspace, path)}`); + return; + } groups.push(event.group); await mkdir(dirname(path), { recursive: true }); await writeFile(path, `${JSON.stringify(document, null, 2)}\n`); diff --git a/.claude/skills/open-edit/scripts/preflight.sh b/.claude/skills/open-edit/scripts/preflight.sh index fe86205..d40db64 100755 --- a/.claude/skills/open-edit/scripts/preflight.sh +++ b/.claude/skills/open-edit/scripts/preflight.sh @@ -133,6 +133,22 @@ discover_runtime() { } discover_runtime + +# Say which code is about to run, and warn when a local checkout is being bypassed. WORKSPACE is only +# reused when it IS an Open Edit checkout; otherwise everything below runs from a clone of +# $DEFAULT_REF, so pointing --workspace at the wrong directory silently runs different code. +BUNDLED_CHECKOUT="$(resolve_dir "$SCRIPT_ROOT/../../.." 2>/dev/null || true)" +if [ "$ROOT_KIND" = "reused" ]; then + say "reusing the local checkout at $ROOT" +else + say "workspace $WORKSPACE will use a managed clone at $MANAGED_ROOT" + if [ -n "$BUNDLED_CHECKOUT" ] && is_open_edit_checkout "$BUNDLED_CHECKOUT" \ + && [ "$BUNDLED_CHECKOUT" != "$WORKSPACE" ]; then + say "NOTE: this skill lives in the checkout $BUNDLED_CHECKOUT, which will NOT be used." + say " To run that code instead, pass --workspace $BUNDLED_CHECKOUT" + fi +fi + if [ "$ROOT_KIND" = "managed" ]; then REPOSITORY="$RECORDED_REPOSITORY"; REF="$RECORDED_REF" else @@ -309,8 +325,12 @@ if [ "$ROOT_KIND" = "missing" ]; then say "runtime is not ready" elif repo_deps_ready && [ -x "$(engine_path)" ]; then say "ready — OPEN_EDIT_ROOT=$ROOT" -else +elif [ "$NEEDS_APPROVAL" -ne 0 ]; then say "local setup is incomplete because an approved prerequisite is missing" +else + # Nothing is awaiting approval: the outstanding work is the WOULD APPLY LOCALLY list above, which + # bare preflight performs itself. Saying "approval" here sent agents looking for a user to ask. + say "not ready yet — run bare preflight (no --dry) to apply the local setup listed above" fi if [ "$NEEDS_APPROVAL" -ne 0 ]; then diff --git a/pipeline/scripts/preflight.sh b/pipeline/scripts/preflight.sh index defb7a6..b1dc173 100755 --- a/pipeline/scripts/preflight.sh +++ b/pipeline/scripts/preflight.sh @@ -2,4 +2,8 @@ # Compatibility entrypoint. The installable skill owns the canonical preflight implementation. set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +# Only default the workspace when the caller did not give one, otherwise it is passed twice. +for arg in "$@"; do + [ "$arg" = "--workspace" ] && exec bash "$REPO_ROOT/.claude/skills/open-edit/scripts/preflight.sh" "$@" +done exec bash "$REPO_ROOT/.claude/skills/open-edit/scripts/preflight.sh" --workspace "$REPO_ROOT" "$@"