Skip to content

Commit f1de71e

Browse files
authored
Merge pull request #61 from hack-dance/codex/event-agent-runtime-repair
fix(runtime): isolate detached worktrees and recover orphan groups
2 parents 8f7f86f + f50f408 commit f1de71e

22 files changed

Lines changed: 548 additions & 46 deletions

.codex/skills/hack-cli/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ Use `hack` as the primary interface for local-first development.
7070
## Linked git worktrees
7171

7272
- Secret key inherits from the primary checkout automatically through the shared git common dir; set `HACK_ENV_SECRET_KEY` for CI or detached environments.
73-
- `hack up` in a linked worktree defaults to a branch instance named after the worktree's git branch; opt out with config `worktree.auto_branch=false` or an explicit `--branch`.
73+
- `hack up` in a linked worktree defaults to a branch instance named after the worktree's git branch; a detached linked worktree requires an explicit `--branch`, unless config `worktree.auto_branch=false` explicitly opts into the base instance.
7474
- `hack doctor` flags divergent secret keys and dev_host collisions across checkouts.
7575

7676
## Advanced networking (extra_hosts + local proxies/tunnels)

.cursor/rules/hack.mdc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Prefer `hack` when shell access is available. Use MCP only when shell access is
3434
## Linked git worktrees
3535

3636
- Secret key inherits from the primary checkout automatically through the shared git common dir; set `HACK_ENV_SECRET_KEY` for CI or detached environments.
37-
- `hack up` in a linked worktree defaults to a branch instance named after the worktree's git branch; opt out with config `worktree.auto_branch=false` or an explicit `--branch`.
37+
- `hack up` in a linked worktree defaults to a branch instance named after the worktree's git branch; a detached linked worktree requires an explicit `--branch`, unless config `worktree.auto_branch=false` explicitly opts into the base instance.
3838
- `hack doctor` flags divergent secret keys and dev_host collisions across checkouts.
3939

4040
## Standard workflow

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ Project files (managed vs generated):
261261

262262
Linked git worktrees:
263263
- Secret key inherits from the primary checkout automatically through the shared git common dir; set `HACK_ENV_SECRET_KEY` for CI or detached environments.
264-
- `hack up` in a linked worktree defaults to a branch instance named after the worktree's git branch; opt out with config `worktree.auto_branch=false` or an explicit `--branch`.
264+
- `hack up` in a linked worktree defaults to a branch instance named after the worktree's git branch; a detached linked worktree requires an explicit `--branch`, unless config `worktree.auto_branch=false` explicitly opts into the base instance.
265265
- `hack doctor` flags divergent secret keys and dev_host collisions across checkouts.
266266

267267
Advanced networking (extra_hosts + local proxies/tunnels):

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ Project files (managed vs generated):
131131

132132
Linked git worktrees:
133133
- Secret key inherits from the primary checkout automatically through the shared git common dir; set `HACK_ENV_SECRET_KEY` for CI or detached environments.
134-
- `hack up` in a linked worktree defaults to a branch instance named after the worktree's git branch; opt out with config `worktree.auto_branch=false` or an explicit `--branch`.
134+
- `hack up` in a linked worktree defaults to a branch instance named after the worktree's git branch; a detached linked worktree requires an explicit `--branch`, unless config `worktree.auto_branch=false` explicitly opts into the base instance.
135135
- `hack doctor` flags divergent secret keys and dev_host collisions across checkouts.
136136

137137
Advanced networking (extra_hosts + local proxies/tunnels):

docs/architecture.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,16 @@ Projects can run host-side hooks around `hack up/down` and start managed host pr
122122
local proxies/tunnels). Processes are started inside a mux session (tmux or zellij) so they have a
123123
stable home and can be torn down on `hack down`.
124124

125+
Lifecycle state is checkout-local at `.hack/.internal/lifecycle/state.json`. For tmux processes, the
126+
saved process group belongs to the wrapped user command rather than its mux pane shell. Each compose
127+
instance has its own entry, so starting a branch instance does not replace the base instance's entry
128+
in the same checkout. Cleanup reconciles the saved process group with the live process table; if the group
129+
leader exited while members remain, `hack doctor` reports the orphan. `hack down` terminates the
130+
persisted leaderless group and descendant process groups only while a matching lifecycle mux session
131+
still proves ownership; without that session, cleanup stays non-destructive because the numeric PGID
132+
may have been reused. A live group leader without its saved pane PID is likewise not trusted as
133+
lifecycle ownership.
134+
125135
For fixed-port helpers such as SSM/database/search tunnels, lifecycle config can also declare a
126136
`singleton` listener set. This lets Hack reuse an already-running equivalent helper or fail fast on
127137
partial conflicts instead of launching a competing duplicate supervisor. The intent is to reduce local
@@ -267,6 +277,10 @@ the primary checkout through the shared git common dir (see "Project env + secre
267277
`hack doctor` checks for divergent secret keys and `dev_host` collisions across checkouts sharing the
268278
same repo.
269279

280+
If the linked worktree is detached, Hack refuses to silently target the base compose project because
281+
there is no branch name from which to derive an isolated instance. Pass `--branch <name>` to select
282+
one, or set `worktree.auto_branch=false` to opt into the base instance explicitly.
283+
270284
## Files and directories
271285

272286
- `~/.hack/` (default; override the whole root with `HACK_HOME`)

docs/cli.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ branch when no `--branch` is passed (`worktree.auto_branch`), so two checkouts n
9494
same hostnames. A one-line notice is printed to stderr when the default kicks in, so captured
9595
stdout stays clean.
9696

97+
A detached linked worktree has no branch name to derive, so these commands fail instead of silently
98+
targeting the base instance. Pass `--branch <name>` to select an isolated instance, or set
99+
`worktree.auto_branch` to `false` only when intentionally opting into the base instance.
100+
97101
Opt out:
98102

99103
- pass `--branch <name>` explicitly (always wins), or

docs/core.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ alive. You do not need gateway, remote nodes, or extension setup to use this pat
3131
`HACK_NO_INTERACTIVE=1`) so scripted/agent runs never block on prompts.
3232
- In a linked git worktree, `hack up` defaults to a branch instance named after the worktree's git
3333
branch (`worktree.auto_branch`), and the secret key is inherited automatically from the primary
34-
checkout via the shared git common dir.
34+
checkout via the shared git common dir. A detached linked worktree has no branch name to derive;
35+
pass `--branch <name>`, or set `worktree.auto_branch=false` when intentionally targeting the base
36+
instance.
3537

3638
## When to leave core
3739

docs/lifecycle.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,8 @@ Lifecycle session name:
212212
Notes:
213213
- If no mux backend is available, lifecycle process startup fails with an actionable error.
214214
- Teardown is implemented by killing the lifecycle session; anything running inside that session will be stopped.
215-
- For tmux-backed lifecycle sessions, Hack also persists pane PID and process-group metadata to
216-
`.hack/.internal/lifecycle/state.json`, with per-hook/process output logged to
215+
- For tmux-backed lifecycle sessions, Hack also persists the pane PID and the wrapped command's actual
216+
process-group metadata to `.hack/.internal/lifecycle/state.json`, with per-hook/process output logged to
217217
`.hack/.internal/lifecycle/*.log`. If tmux pane state disappears before teardown, `hack down` still uses that persisted metadata to clean up any live lifecycle process groups instead of leaving orphaned host processes behind.
218218
- `hack doctor` reports stale lifecycle state when the persisted lifecycle entry no longer has a live mux session and points operators to `hack down` so cleanup and state removal happen through the supported path. `hack doctor --fix` does not tear down lifecycle sessions itself — use `hack down` for that.
219219

src/agents/instruction-source.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export const INSTRUCTION_SECTIONS: readonly InstructionSection[] = [
122122
surfaces: ALL_SURFACES,
123123
bullets: [
124124
"Secret key inherits from the primary checkout automatically through the shared git common dir; set `HACK_ENV_SECRET_KEY` for CI or detached environments.",
125-
"`hack up` in a linked worktree defaults to a branch instance named after the worktree's git branch; opt out with config `worktree.auto_branch=false` or an explicit `--branch`.",
125+
"`hack up` in a linked worktree defaults to a branch instance named after the worktree's git branch; a detached linked worktree requires an explicit `--branch`, unless config `worktree.auto_branch=false` explicitly opts into the base instance.",
126126
"`hack doctor` flags divergent secret keys and dev_host collisions across checkouts.",
127127
],
128128
},

src/commands/doctor.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1845,7 +1845,10 @@ async function checkProjectLifecycleHygiene({
18451845
const inspection = await inspectProjectLifecycleHygiene({
18461846
projectDir: ctx.projectDir,
18471847
});
1848-
if (inspection.staleEntries.length === 0) {
1848+
if (
1849+
inspection.staleEntries.length === 0 &&
1850+
inspection.orphanedProcessGroups.length === 0
1851+
) {
18491852
return {
18501853
name: "lifecycle hygiene",
18511854
status: "ok",
@@ -1854,15 +1857,19 @@ async function checkProjectLifecycleHygiene({
18541857
};
18551858
}
18561859

1857-
const orphanedGroups = new Set<number>();
1860+
const orphanedGroups = new Set<number>(inspection.orphanedProcessGroups);
18581861
for (const entry of inspection.staleEntries) {
18591862
for (const group of entry.liveProcessGroups) {
18601863
orphanedGroups.add(group);
18611864
}
18621865
}
18631866

18641867
const details = [
1865-
`${inspection.staleEntries.length} stale lifecycle state entr${inspection.staleEntries.length === 1 ? "y" : "ies"}`,
1868+
...(inspection.staleEntries.length > 0
1869+
? [
1870+
`${inspection.staleEntries.length} stale lifecycle state entr${inspection.staleEntries.length === 1 ? "y" : "ies"}`,
1871+
]
1872+
: []),
18661873
...(orphanedGroups.size > 0
18671874
? [
18681875
`${orphanedGroups.size} orphaned lifecycle process group${orphanedGroups.size === 1 ? "" : "s"}`,

0 commit comments

Comments
 (0)