Recursive Gate-Execution Containment
internal/gatecontextis the single classifier for recursive pipeline control. It combines canonical registered gate common-directory identity with OS-authenticated IPC peer ancestry;NO_MISTAKES_GATEis diagnostic only. CLI preflight, daemon mutation ingress, gate init/eject, branch-sync mutation, and the managed pre-receive hook must all keep using that owner so marker removal, cwd changes, and direct pushes cannot bypass refusal. Read-only AXI status/logs, help, and doctor remain available. Regressions:internal/gatecontext,TestGateStepCannotStartRecursivePipeline.- Every pipeline agent prompt receives the phase boundary from
internal/gateguidance, and the generated user-level skill reuses the same owner. Step agents return only their assigned phase; the outer executor alone controls other validation, push, PR, and CI phases. Editinternal/skill/skill.go, then runmake skill; never edit the generated skill directly.
Filesystem and Paths
- Use
filepath.Join; respectNM_HOMEfor app state; directories are0o755and files0o644by convention. - On macOS, path comparisons may need symlink resolution (
/varvs/private/var); useworktrees.Canonical/worktrees.Containswherever run worktree paths are compared, so one spelling matches everywhere. - Run worktree placement (
worktree_roots) is owned byinternal/worktrees. Configuration decides it exactly once, at run creation (Layout.DirinRunManager.startRunWithIntentSource), and the result is persisted inruns.worktree_dir; every later consumer - resume, step diff, startup cleanup,procreap, eject, gatecontext attribution - must read it back throughworktrees.RecordedDirand never re-derive it from config, so a mid-flight edit can neither strand a parked run nor point a removal at a directory the run never used. An empty column means the default<NM_HOME>/worktrees/<repoID>/<runID>. worktrees.CheckPlacementis the single policy for an unusable root (insideNM_HOME, inside any registered checkout);config.ValidateWorktreeRootsowns what the config can judge alone. The daemon refuses to start on an unusable placement, soinit --worktree-rootmust refuse exactly the same set or it prints a paste that takes the operator's CLI down, and EVERYinitrefuses to register a checkout that contains a configured root - the same state reached from the other direction. User-facing semantics live indocs/src/content/docs/reference/global-config.md. Regressions:internal/worktrees,internal/config/config_worktree_roots_test.go,internal/daemon/worktree_roots_test.go,internal/gate/eject_sweep_test.go,internal/cli/init_test.go.
Git on Bare Gate Repos (safe.bareRepository)
- Agent harnesses and hardened CI inject
safe.bareRepository=explicit, which forbids cwd-based discovery of bare repositories. Route every gate git call throughgit.Run, which detects a bare git dir and prepends--git-dir=<dir>; never shell out to git in a bare gate repo relying oncmd.Diror-Cdiscovery (issue #362). - Startup gate migration is DB-authoritative with a strict validated
<id>.gitlegacy fallback; it must reject non-gates before hook or Git mutation and usegit.RunBareso a malformed directory cannot discover an ancestor worktree. Completed migrations carry the content-versioned gate-config stamp and normal restarts must stay filesystem-only for current gates. Regressions:TestMigrateGateConfigsRejectsInvalidDirectoriesAndSkipsCurrentGates,TestColdDetachedStartupProductionGateCardinality. - Regressions:
TestRunOnBareRepoUnderSafeBareRepositoryExplicit,TestWorktreeAddRemoveOnBareRepoUnderSafeBareRepositoryExplicit,TestInitUnderSafeBareRepositoryExplicit.
gh PR-Targeting From the Bare Gate Repo (internal/scm/github)
- The daemon runs
ghfrom the detached bare gate repo whose HEAD is the default branch, so every PR-targeting command must name the exact PR explicitly: an empty positional makesgh pr <verb>infer the cwd branch (main) and returnno pull requests found for branch maineven when the feature PR's checks are green.GetChecks,GetPRState,GetMergeableState, andUpdatePRroute through the sharedprSelector(number, else URL, else fail closed) - never append a barepr.Number/pr.URLthat can be empty. This is theghanalogue of the git bare-gate-repo trap above. - Regressions:
TestGetChecksTargetsKnownPRByURLWhenNumberMissing,TestPRTargetingReadsFailClosedWithoutIdentity,TestPRStateAndMergeableTargetKnownPRByURL,TestUpdatePRTargetsKnownPRByURLWhenNumberMissing,TestUpdatePRFailsClosedWithoutIdentity.
Post-Receive Hook Gate Path Resolution (internal/git/hook.go)
- The hook's
--gatevalue must never come from a bare$(pwd): Git can invokepost-receivefrom a cwd that collapses to.(issue #269), which the daemon rejects and the pipeline silently never starts. The hook script resolves an absolute gate dir (git first, hook location fallback), andnormalizeNotifyGatePathininternal/cli/daemon_cmd.gois an independent second layer that absolutizes whatever an already-installed older hook sends. - Regressions:
TestPostReceiveHook_ResolvesAbsoluteGateDir,TestPostReceiveHook_FallsBackToHookLocationForGateDir,TestNormalizeNotifyGatePathResolvesLegacyDotGate.