Skip to content

feat(spawn): add general post-worktree-create hook seam#371

Open
zachlandes wants to merge 4 commits into
kunchenguid:mainfrom
zachlandes:fm/post-worktree-create-hook
Open

feat(spawn): add general post-worktree-create hook seam#371
zachlandes wants to merge 4 commits into
kunchenguid:mainfrom
zachlandes:fm/post-worktree-create-hook

Conversation

@zachlandes

Copy link
Copy Markdown

Summary

Adds a general post-worktree-create hook to fm-spawn.sh: after a task's worktree — or a secondmate's home — is created and asserted to be a genuine isolated worktree, fm-spawn runs a local, gitignored config/hooks/post-worktree-create script if the operator installed one. This is a general extension seam for fleet-local worktree provisioning that should not live in tracked code.

firstmate ships no hook, so an absent hook is a complete no-op with zero behavior change for anyone not using it.

Why

Worktree provisioning that a fleet needs (e.g. dropping per-project config into each worktree) previously had no seam and would have to edit tracked code. That's a poor fit here: firstmate self-updates fast-forward-only, so a local commit on tracked files blocks future updates, and fleet-specific features don't belong upstream. A general hook keeps the tracked code additive and lets each fleet keep its own wiring local and gitignored.

Behavior

  • Runs for every task kind: ship, scout, and secondmate.
  • Never gates a launch. A hook that exits non-zero is warned to stderr and the spawn continues; it must never be relied on to block a spawn.
  • A hang cannot gate a launch either. The hook runs under timeout/gtimeout with a budget of FM_HOOK_TIMEOUT seconds (default 120) and a 5s kill-after grace, so even a hook that traps/ignores SIGTERM is stopped and reported like a failure. FM_HOOK_TIMEOUT=0 is a documented no-limit opt-out. When neither timeout binary is on PATH, the hook runs unbounded after a stderr warning — a missing binary never fails the spawn.
  • Never runs against the primary checkout: the call site only reaches it past fm-spawn's isolation assertion, and fm-hooks-lib.sh additionally refuses to run when the worktree resolves to the firstmate root as a backstop.
  • Receives the worktree path, project name, task id, and kind as both positional args ($1..$4) and FM_HOOK_* environment.
  • config/hooks/ is gitignored, so an installed hook stays fleet-local. It is also inheritable: config/hooks/post-worktree-create propagates from the primary into secondmate homes like the other crew-affecting config, with the executable bit preserved (and repaired on re-convergence if a destination copy loses it).

Contents

  • bin/fm-hooks-lib.sh — the seam runner (fm_run_post_worktree_create_hook).
  • bin/fm-spawn.sh — sources the lib and calls the seam after worktree isolation is asserted.
  • bin/fm-config-inherit-lib.sh — makes the hook inheritable and preserves/repairs file mode on copy.
  • docs/configuration.md — the hook contract; docs/examples/post-worktree-create — a runnable sample.
  • AGENTS.md — one-line config-index pointer.
  • .gitignoreconfig/hooks/.
  • tests/fm-hooks.test.sh — no-op-absent, runs-with-args-and-env, skips-primary, non-fatal-on-error, non-fatal-on-hang (incl. a SIGTERM-ignoring hook), and a fake-timeout check of the -k 5 <budget> invocation. tests/fm-secondmate-harness.test.sh — hook inheritance with mode preservation.

Validated through the no-mistakes pipeline (review, test, document, lint all green). shellcheck bin/*.sh bin/backends/*.sh tests/*.sh is clean.

fm-spawn now runs a local, gitignored config/hooks/post-worktree-create
hook right after it creates a task's worktree - or a secondmate's home -
and asserts it is a genuine isolated worktree, for every task kind. This
is a general extension seam for fleet-local worktree provisioning that
should not live in tracked code.

firstmate ships no hook, so an absent hook is a complete no-op with zero
behavior change. The hook is purely additive: a hook that errors is
warned and swallowed so the spawn always continues, and it never runs
against the primary checkout (the call site only reaches it past the
isolation assertion, and fm-hooks-lib.sh refuses to run when the worktree
resolves to the firstmate root as a backstop). The hook receives the
worktree path, project name, task id, and kind as both positional args
and environment.

Adds bin/fm-hooks-lib.sh, a docs/examples/ sample hook, the contract in
docs/configuration.md, an AGENTS.md config-index pointer, and colocated
seam tests (no-op absent, runs present, skips primary, non-fatal on
error).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant