~bigbes/agents-dev · parade

main · last commit 13 days ago · 7g0stsfu

← Back to the parade

ah-nyl.11 fix: runner/ahub-run review findings 1-10 (FIX-FIRST verdict) Past Stand

status: closed P0 bug
bd reopen ah-nyl.11
Created byEugene Blikh
Ownerbigbes@gmail.com
Created2026-07-13T00:37:45Z
Started2026-07-13T00:37:46Z
Updated2026-07-13T01:05:34Z
Closed2026-07-13T01:05:34Z
Description
Hostile review of internal/runner + cmd/ahub-run @ HEAD (4348d2b) returned FIX-FIRST: findings 1-3 blocking, 4 strongly recommended, 5-9 hardening, 10 test gaps. Fix ALL. Line refs @ 4348d2b.

F1 MAJOR (runner.go:201-225, commands.go:45-47): dir missing + branch exists + worktree still REGISTERED (human rm -rf'd it) -> `git worktree add` exits 128 "missing but already registered"; Start wedges forever. FIX: in ensureWorktree, when the worktree dir is missing, run `git -C <repo> worktree prune` before add (reproduced working on git 2.55). Add a regression test that registers a worktree, rm -rf's the dir WITHOUT prune, and asserts Start succeeds.

F2 MAJOR (cmd/ahub-run/main.go): no signal handling — SIGTERM/SIGINT/SIGHUP to ahub-run kills the supervisor with NO exit.json; the child pi survives until its next stdout write (SIGPIPE), burning tokens; the zellij session survives so Status says Running for the full 30m timeout, then reports the wrong outcome. FIX: signal.Notify(SIGINT, SIGTERM, SIGHUP); forward the signal to the child (process group where sensible), wait for it, then STILL write exit.json (128+sig) and POST the report. Regression test: signal a running ahub-run, assert child死 + exit.json written with 128+sig.

F3 MAJOR (runner.go:103-133, 158-163): Status ignores its attempt arg (`_ = attempt`) and never checks ef.Attempt -> a stale attempt-K supervisor's exit.json completes attempt K+1 with K's exit code; the truncated-then-repopulated events.jsonl attributes K's session id and costs to K+1. Kill swallows every kill-session failure at Debug, so a wedged zellij is indistinguishable from dead. FIX: (a) in Status, ef.Attempt != attempt => treat as no-exit.json and fall through to session check; (b) in Kill, after kill-session verify via list-sessions that the session is gone; if still alive, return an error (Warn+error). Regression tests for both.

F4 MAJOR (runner.go:62-97): Start is not idempotent per attempt — after a claim-persist failure the reconciler re-claims the SAME attempt and Start re-prepares (truncating the live events.jsonl) and spawns a duplicate pane. FIX: at the top of Start, if .task/meta.json matches (task_id, attempt) AND exit.json is absent AND the zellij session is alive -> return the existing RunInfo (worktree/branch/session) without re-preparing or re-spawning. Regression test.

F5 MINOR (runner.go:344-346): sessionAlive maps non-zero exit + EMPTY output to "no sessions" -> silent zellij failure finalizes a live run as crashed. FIX: only the recognized "no active session" text (or clean exit) means no-sessions; empty output + non-zero exit => return an observation error. Update the test at runner_test.go:396-403 that pins the old hazardous mapping.

F6 MINOR (runner.go:406-413): runCmd merges stderr into the parsed stdout buffer -> git warnings corrupt gitResolveCommonDir (exclude entry lands in a bogus silently-created path) and Summary turns stderr into fake commit lines. FIX: separate stdout/stderr buffers; parse stdout only; include stderr in error messages.

F7 MINOR (taskfiles.go:61-79): WriteExitFile renames without fsync -> post-power-loss empty-but-present exit.json makes ReadExitFile error forever. FIX: tmp.Sync() before Close/rename.

F8 MINOR (internal/config/config.go:201-208 + commands.go:32-34): repo slug is not shape-validated; "/" or ".." in a slug makes worktreePath escape work_root. FIX in internal/config (you MAY touch config for exactly this): reject slugs not matching ^[A-Za-z0-9._-]+$ (and not "." / ".."), with a validation-collected error + test. This is the ONLY change allowed outside runner/ahub-run.

F9 MINOR (events.go:49-51, 94-108): (a) a single line >10MB aborts the whole scan silently (message_end lines grow with conversation) -> switch to a reader that SKIPS an over-long line and continues; (b) cost summation semantics are under-pinned — fixtures have one assistant message; if pi's usage.cost.total is cumulative, += double-counts. Without re-running pi: add a two-assistant-message fixture built from the existing captured shape, document the per-message assumption in a comment referencing the fixture provenance, and add a TODO-marked guard test so the fixture is easy to re-pin from a real capture later.

F10 test honesty: add the tests named above; also remove/rework TestKillDeadSessionIsNotAnError so it distinguishes "session already gone (ok)" from "kill failed but session alive (error)".

Constraints: scope = internal/runner/, cmd/ahub-run/, plus the single F8 validation in internal/config/. Conventions: testify + culpa (SPEC §13). Build/vet/test ONLY your packages (./internal/runner/... ./cmd/ahub-run/... ./internal/config/...) with -race; never ./... . Another fixer is working in internal/reconcile concurrently — do not touch it. Commit 'runner: fix review findings (worktree prune, signals, per-attempt status, idempotent start, hardening)' (+ separate 'config: validate repo slug shape' commit) staging only your paths; hooks bypassed (git -c core.hooksPath=/tmp/nohooks commit); retry on index.lock; do NOT push; do NOT touch .beads/.
Notes
F11 (VALIDATOR blocker D1, empirically established on real zellij 0.44.3): sessions that lived past zellij's session-serialization tick (~65s, default config) and then die WITHOUT delete-session (pi/ahub-run crash killing the pane, zellij server death, reboot with cache) remain listed by 'list-sessions --short' as bare names indistinguishable from live sessions -> Status reports Running forever; OutcomeCrashed unreachable for real crashes. FIX: switch session liveness to 'zellij list-sessions --no-formatting' (plain text, dead sessions carry the '(EXITED - attach to resurrect)' suffix) and treat EXITED as NOT alive; parse defensively (name = first whitespace-separated token; EXITED detection by substring); update stub scripts + tests incl. an EXITED-listed case. Kill() keeps kill-then-delete (validator confirmed delete-session removes serialized dead sessions, exit 0). ALSO F12 (validator observation): zellij pane env = SESSION-CREATION-TIME server env, not run-client env — so bare 'ahub-run'/'pi' argv depend on the daemon's PATH at attach --create-background time. Harden: resolve ahub-run and pi to ABSOLUTE paths via exec.LookPath at Start (error clearly if not found) and use those in the pane argv; keeps working under systemd's minimal PATH later.

Depends on

  • ah-nyl — Stage 1 MVP: poll-driven happy path parent-child closed

Depended on by

Nothing depends on this issue.

No comments.

Close reason

landed 7a489b9 (runner+ahub-run) + b978e0e (config slug): prune-before-add, signal handling w/ pgid, per-attempt status, idempotent start, EXITED-aware liveness via --no-formatting, absolute pane binaries, stream split, fsync, long-line skip; 5 negative controls confirmed
  • Eugene Blikh added under epic ah-nyl · 2026-07-13T03:37:45Z
  • Eugene Blikh created the issue · 2026-07-13T03:37:45Z
  • Eugene Blikh changed status to in_progress · 2026-07-13T03:37:45Z
  • Eugene Blikh updated notes to F11 (VALIDATOR blocker D1, empirically established on real zellij 0.44.3): sessions that lived past zellij's session-serialization tick (~65s, default config) and then die WITHOUT delete-session (pi/ahub-run crash killing the pane, zellij server death, reboot with cache) remain listed by 'list-sessions --short' as bare names indistinguishable from live sessions -> Status reports Running forever; OutcomeCrashed unreachable for real crashes. FIX: switch session liveness to 'zellij list-sessions --no-formatting' (plain text, dead sessions carry the '(EXITED - attach to resurrect)' suffix) and treat EXITED as NOT alive; parse defensively (name = first whitespace-separated token; EXITED detection by substring); update stub scripts + tests incl. an EXITED-listed case. Kill() keeps kill-then-delete (validator confirmed delete-session removes serialized dead sessions, exit 0). ALSO F12 (validator observation): zellij pane env = SESSION-CREATION-TIME server env, not run-client env — so bare 'ahub-run'/'pi' argv depend on the daemon's PATH at attach --create-background time. Harden: resolve ahub-run and pi to ABSOLUTE paths via exec.LookPath at Start (error clearly if not found) and use those in the pane argv; keeps working under systemd's minimal PATH later. · 2026-07-13T03:44:07Z
  • Eugene Blikh closed the issue · 2026-07-13T04:05:34Z
    landed 7a489b9 (runner+ahub-run) + b978e0e (config slug): prune-before-add, signal handling w/ pgid, per-attempt status, idempotent start, EXITED-aware liveness via --no-formatting, absolute pane binaries, stream split, fsync, long-line skip; 5 negative controls confirmed
  • Eugene Blikh added label milestone:stage-1 · 2026-07-20T03:08:46Z
  • Eugene Blikh removed label milestone:stage-1 · 2026-07-20T03:10:19Z
Stored rows — what this pane was built from, as read
issues 1 row
id ah-nyl.11
content_hash ccf963c6cdbbcf9043784dbdf1f98f71af8baca8cbd9a58cca23607dc1ff7f92
title fix: runner/ahub-run review findings 1-10 (FIX-FIRST verdict)
description Hostile review of internal/runner + cmd/ahub-run @ HEAD (4348d2b) returned FIX-FIRST: findings 1-3 blocking, 4 strongly recommended, 5-9 hardening, 10 test gaps. Fix ALL. Line refs @ 4348d2b. F1 MAJOR (runner.go:201-225, commands.go:45-47): dir missing + branch exists + worktree still REGISTERED (human rm -rf'd it) -> `git worktree add` exits 128 "missing but already registered"; Start wedges forever. FIX: in ensureWorktree, when the worktree dir is missing, run `git -C <repo> worktree prune` before add (reproduced working on git 2.55). Add a regression test that registers a worktree, rm -rf's the dir WITHOUT prune, and asserts Start succeeds. F2 MAJOR (cmd/ahub-run/main.go): no signal handling — SIGTERM/SIGINT/SIGHUP to ahub-run kills the supervisor with NO exit.json; the child pi survives until its next stdout write (SIGPIPE), burning tokens; the zellij session survives so Status says Running for the full 30m timeout, then reports the wrong outcome. FIX: signal.Notify(SIGINT, SIGTERM, SIGHUP); forward the signal to the child (process group where sensible), wait for it, then STILL write exit.json (128+sig) and POST the report. Regression test: signal a running ahub-run, assert child死 + exit.json written with 128+sig. F3 MAJOR (runner.go:103-133, 158-163): Status ignores its attempt arg (`_ = attempt`) and never checks ef.Attempt -> a stale attempt-K supervisor's exit.json completes attempt K+1 with K's exit code; the truncated-then-repopulated events.jsonl attributes K's session id and costs to K+1. Kill swallows every kill-session failure at Debug, so a wedged zellij is indistinguishable from dead. FIX: (a) in Status, ef.Attempt != attempt => treat as no-exit.json and fall through to session check; (b) in Kill, after kill-session verify via list-sessions that the session is gone; if still alive, return an error (Warn+error). Regression tests for both. F4 MAJOR (runner.go:62-97): Start is not idempotent per attempt — after a claim-persist failure the reconciler re-claims the SAME attempt and Start re-prepares (truncating the live events.jsonl) and spawns a duplicate pane. FIX: at the top of Start, if .task/meta.json matches (task_id, attempt) AND exit.json is absent AND the zellij session is alive -> return the existing RunInfo (worktree/branch/session) without re-preparing or re-spawning. Regression test. F5 MINOR (runner.go:344-346): sessionAlive maps non-zero exit + EMPTY output to "no sessions" -> silent zellij failure finalizes a live run as crashed. FIX: only the recognized "no active session" text (or clean exit) means no-sessions; empty output + non-zero exit => return an observation error. Update the test at runner_test.go:396-403 that pins the old hazardous mapping. F6 MINOR (runner.go:406-413): runCmd merges stderr into the parsed stdout buffer -> git warnings corrupt gitResolveCommonDir (exclude entry lands in a bogus silently-created path) and Summary turns stderr into fake commit lines. FIX: separate stdout/stderr buffers; parse stdout only; include stderr in error messages. F7 MINOR (taskfiles.go:61-79): WriteExitFile renames without fsync -> post-power-loss empty-but-present exit.json makes ReadExitFile error forever. FIX: tmp.Sync() before Close/rename. F8 MINOR (internal/config/config.go:201-208 + commands.go:32-34): repo slug is not shape-validated; "/" or ".." in a slug makes worktreePath escape work_root. FIX in internal/config (you MAY touch config for exactly this): reject slugs not matching ^[A-Za-z0-9._-]+$ (and not "." / ".."), with a validation-collected error + test. This is the ONLY change allowed outside runner/ahub-run. F9 MINOR (events.go:49-51, 94-108): (a) a single line >10MB aborts the whole scan silently (message_end lines grow with conversation) -> switch to a reader that SKIPS an over-long line and continues; (b) cost summation semantics are under-pinned — fixtures have one assistant message; if pi's usage.cost.total is cumulative, += double-counts. Without re-running pi: add a two-assistant-message fixture built from the existing captured shape, document the per-message assumption in a comment referencing the fixture provenance, and add a TODO-marked guard test so the fixture is easy to re-pin from a real capture later. F10 test honesty: add the tests named above; also remove/rework TestKillDeadSessionIsNotAnError so it distinguishes "session already gone (ok)" from "kill failed but session alive (error)". Constraints: scope = internal/runner/, cmd/ahub-run/, plus the single F8 validation in internal/config/. Conventions: testify + culpa (SPEC §13). Build/vet/test ONLY your packages (./internal/runner/... ./cmd/ahub-run/... ./internal/config/...) with -race; never ./... . Another fixer is working in internal/reconcile concurrently — do not touch it. Commit 'runner: fix review findings (worktree prune, signals, per-attempt status, idempotent start, hardening)' (+ separate 'config: validate repo slug shape' commit) staging only your paths; hooks bypassed (git -c core.hooksPath=/tmp/nohooks commit); retry on index.lock; do NOT push; do NOT touch .beads/.
design
acceptance_criteria
notes F11 (VALIDATOR blocker D1, empirically established on real zellij 0.44.3): sessions that lived past zellij's session-serialization tick (~65s, default config) and then die WITHOUT delete-session (pi/ahub-run crash killing the pane, zellij server death, reboot with cache) remain listed by 'list-sessions --short' as bare names indistinguishable from live sessions -> Status reports Running forever; OutcomeCrashed unreachable for real crashes. FIX: switch session liveness to 'zellij list-sessions --no-formatting' (plain text, dead sessions carry the '(EXITED - attach to resurrect)' suffix) and treat EXITED as NOT alive; parse defensively (name = first whitespace-separated token; EXITED detection by substring); update stub scripts + tests incl. an EXITED-listed case. Kill() keeps kill-then-delete (validator confirmed delete-session removes serialized dead sessions, exit 0). ALSO F12 (validator observation): zellij pane env = SESSION-CREATION-TIME server env, not run-client env — so bare 'ahub-run'/'pi' argv depend on the daemon's PATH at attach --create-background time. Harden: resolve ahub-run and pi to ABSOLUTE paths via exec.LookPath at Start (error clearly if not found) and use those in the pane argv; keeps working under systemd's minimal PATH later.
status closed
priority 0
issue_type bug
assignee NULL
estimated_minutes NULL
created_at 2026-07-13T00:37:45Z
created_by Eugene Blikh
owner bigbes@gmail.com
updated_at 2026-07-13T01:05:34Z
closed_at 2026-07-13T01:05:34Z
closed_by_session
external_ref NULL
spec_id
compaction_level 0
compacted_at NULL
compacted_at_commit NULL
original_size NULL
sender
ephemeral 0
wisp_type
pinned 0
is_template 0
mol_type
work_type
source_system
metadata �{}
source_repo
close_reason landed 7a489b9 (runner+ahub-run) + b978e0e (config slug): prune-before-add, signal handling w/ pgid, per-attempt status, idempotent start, EXITED-aware liveness via --no-formatting, absolute pane binaries, stream split, fsync, long-line skip; 5 negative controls confirmed
event_kind
actor
target
payload
await_type
await_id
timeout_ns 0
waiters
hook_bead
role_bead
agent_state
last_activity NULL
role_type
rig
due_at NULL
defer_until NULL
no_history 0
started_at 2026-07-13T00:37:46Z
is_blocked 0
dependencies 1 row
id 2445d556-ac08-56f8-88f6-40ad5ab239e9
issue_id ah-nyl.11
type parent-child
created_at 2026-07-13T03:37:45Z
created_by Eugene Blikh
metadata �{}
thread_id
depends_on_issue_id ah-nyl
depends_on_wisp_id NULL
depends_on_external NULL
events 6 rows
id 019f58e8-94da-7148-9e0b-903f4fa6ca9a
issue_id ah-nyl.11
event_type created
actor Eugene Blikh
old_value
new_value
comment NULL
created_at 2026-07-13T03:37:45Z
id 019f58e8-96a0-7bc7-bf23-9b0427282819
issue_id ah-nyl.11
event_type status_changed
actor Eugene Blikh
old_value {"id":"ah-nyl.11","title":"fix: runner/ahub-run review findings 1-10 (FIX-FIRST verdict)","description":"Hostile review of internal/runner + cmd/ahub-run @ HEAD (4348d2b) returned FIX-FIRST: findings 1-3 blocking, 4 strongly recommended, 5-9 hardening, 10 test gaps. Fix ALL. Line refs @ 4348d2b.\n\nF1 MAJOR (runner.go:201-225, commands.go:45-47): dir missing + branch exists + worktree still REGISTERED (human rm -rf'd it) -\u003e `git worktree add` exits 128 \"missing but already registered\"; Start wedges forever. FIX: in ensureWorktree, when the worktree dir is missing, run `git -C \u003crepo\u003e worktree prune` before add (reproduced working on git 2.55). Add a regression test that registers a worktree, rm -rf's the dir WITHOUT prune, and asserts Start succeeds.\n\nF2 MAJOR (cmd/ahub-run/main.go): no signal handling — SIGTERM/SIGINT/SIGHUP to ahub-run kills the supervisor with NO exit.json; the child pi survives until its next stdout write (SIGPIPE), burning tokens; the zellij session survives so Status says Running for the full 30m timeout, then reports the wrong outcome. FIX: signal.Notify(SIGINT, SIGTERM, SIGHUP); forward the signal to the child (process group where sensible), wait for it, then STILL write exit.json (128+sig) and POST the report. Regression test: signal a running ahub-run, assert child死 + exit.json written with 128+sig.\n\nF3 MAJOR (runner.go:103-133, 158-163): Status ignores its attempt arg (`_ = attempt`) and never checks ef.Attempt -\u003e a stale attempt-K supervisor's exit.json completes attempt K+1 with K's exit code; the truncated-then-repopulated events.jsonl attributes K's session id and costs to K+1. Kill swallows every kill-session failure at Debug, so a wedged zellij is indistinguishable from dead. FIX: (a) in Status, ef.Attempt != attempt =\u003e treat as no-exit.json and fall through to session check; (b) in Kill, after kill-session verify via list-sessions that the session is gone; if still alive, return an error (Warn+error). Regression tests for both.\n\nF4 MAJOR (runner.go:62-97): Start is not idempotent per attempt — after a claim-persist failure the reconciler re-claims the SAME attempt and Start re-prepares (truncating the live events.jsonl) and spawns a duplicate pane. FIX: at the top of Start, if .task/meta.json matches (task_id, attempt) AND exit.json is absent AND the zellij session is alive -\u003e return the existing RunInfo (worktree/branch/session) without re-preparing or re-spawning. Regression test.\n\nF5 MINOR (runner.go:344-346): sessionAlive maps non-zero exit + EMPTY output to \"no sessions\" -\u003e silent zellij failure finalizes a live run as crashed. FIX: only the recognized \"no active session\" text (or clean exit) means no-sessions; empty output + non-zero exit =\u003e return an observation error. Update the test at runner_test.go:396-403 that pins the old hazardous mapping.\n\nF6 MINOR (runner.go:406-413): runCmd merges stderr into the parsed stdout buffer -\u003e git warnings corrupt gitResolveCommonDir (exclude entry lands in a bogus silently-created path) and Summary turns stderr into fake commit lines. FIX: separate stdout/stderr buffers; parse stdout only; include stderr in error messages.\n\nF7 MINOR (taskfiles.go:61-79): WriteExitFile renames without fsync -\u003e post-power-loss empty-but-present exit.json makes ReadExitFile error forever. FIX: tmp.Sync() before Close/rename.\n\nF8 MINOR (internal/config/config.go:201-208 + commands.go:32-34): repo slug is not shape-validated; \"/\" or \"..\" in a slug makes worktreePath escape work_root. FIX in internal/config (you MAY touch config for exactly this): reject slugs not matching ^[A-Za-z0-9._-]+$ (and not \".\" / \"..\"), with a validation-collected error + test. This is the ONLY change allowed outside runner/ahub-run.\n\nF9 MINOR (events.go:49-51, 94-108): (a) a single line \u003e10MB aborts the whole scan silently (message_end lines grow with conversation) -\u003e switch to a reader that SKIPS an over-long line and continues; (b) cost summation semantics are under-pinned — fixtures have one assistant message; if pi's usage.cost.total is cumulative, += double-counts. Without re-running pi: add a two-assistant-message fixture built from the existing captured shape, document the per-message assumption in a comment referencing the fixture provenance, and add a TODO-marked guard test so the fixture is easy to re-pin from a real capture later.\n\nF10 test honesty: add the tests named above; also remove/rework TestKillDeadSessionIsNotAnError so it distinguishes \"session already gone (ok)\" from \"kill failed but session alive (error)\".\n\nConstraints: scope = internal/runner/, cmd/ahub-run/, plus the single F8 validation in internal/config/. Conventions: testify + culpa (SPEC §13). Build/vet/test ONLY your packages (./internal/runner/... ./cmd/ahub-run/... ./internal/config/...) with -race; never ./... . Another fixer is working in internal/reconcile concurrently — do not touch it. Commit 'runner: fix review findings (worktree prune, signals, per-attempt status, idempotent start, hardening)' (+ separate 'config: validate repo slug shape' commit) staging only your paths; hooks bypassed (git -c core.hooksPath=/tmp/nohooks commit); retry on index.lock; do NOT push; do NOT touch .beads/.\n","status":"open","priority":0,"issue_type":"bug","owner":"bigbes@gmail.com","created_at":"2026-07-13T00:37:45Z","created_by":"Eugene Blikh","updated_at":"2026-07-13T00:37:45Z"}
new_value {"status":"in_progress"}
comment NULL
created_at 2026-07-13T03:37:45Z
id 019f58ee-6a8b-7301-a3df-53bb9f205694
issue_id ah-nyl.11
event_type updated
actor Eugene Blikh
old_value {"id":"ah-nyl.11","title":"fix: runner/ahub-run review findings 1-10 (FIX-FIRST verdict)","description":"Hostile review of internal/runner + cmd/ahub-run @ HEAD (4348d2b) returned FIX-FIRST: findings 1-3 blocking, 4 strongly recommended, 5-9 hardening, 10 test gaps. Fix ALL. Line refs @ 4348d2b.\n\nF1 MAJOR (runner.go:201-225, commands.go:45-47): dir missing + branch exists + worktree still REGISTERED (human rm -rf'd it) -\u003e `git worktree add` exits 128 \"missing but already registered\"; Start wedges forever. FIX: in ensureWorktree, when the worktree dir is missing, run `git -C \u003crepo\u003e worktree prune` before add (reproduced working on git 2.55). Add a regression test that registers a worktree, rm -rf's the dir WITHOUT prune, and asserts Start succeeds.\n\nF2 MAJOR (cmd/ahub-run/main.go): no signal handling — SIGTERM/SIGINT/SIGHUP to ahub-run kills the supervisor with NO exit.json; the child pi survives until its next stdout write (SIGPIPE), burning tokens; the zellij session survives so Status says Running for the full 30m timeout, then reports the wrong outcome. FIX: signal.Notify(SIGINT, SIGTERM, SIGHUP); forward the signal to the child (process group where sensible), wait for it, then STILL write exit.json (128+sig) and POST the report. Regression test: signal a running ahub-run, assert child死 + exit.json written with 128+sig.\n\nF3 MAJOR (runner.go:103-133, 158-163): Status ignores its attempt arg (`_ = attempt`) and never checks ef.Attempt -\u003e a stale attempt-K supervisor's exit.json completes attempt K+1 with K's exit code; the truncated-then-repopulated events.jsonl attributes K's session id and costs to K+1. Kill swallows every kill-session failure at Debug, so a wedged zellij is indistinguishable from dead. FIX: (a) in Status, ef.Attempt != attempt =\u003e treat as no-exit.json and fall through to session check; (b) in Kill, after kill-session verify via list-sessions that the session is gone; if still alive, return an error (Warn+error). Regression tests for both.\n\nF4 MAJOR (runner.go:62-97): Start is not idempotent per attempt — after a claim-persist failure the reconciler re-claims the SAME attempt and Start re-prepares (truncating the live events.jsonl) and spawns a duplicate pane. FIX: at the top of Start, if .task/meta.json matches (task_id, attempt) AND exit.json is absent AND the zellij session is alive -\u003e return the existing RunInfo (worktree/branch/session) without re-preparing or re-spawning. Regression test.\n\nF5 MINOR (runner.go:344-346): sessionAlive maps non-zero exit + EMPTY output to \"no sessions\" -\u003e silent zellij failure finalizes a live run as crashed. FIX: only the recognized \"no active session\" text (or clean exit) means no-sessions; empty output + non-zero exit =\u003e return an observation error. Update the test at runner_test.go:396-403 that pins the old hazardous mapping.\n\nF6 MINOR (runner.go:406-413): runCmd merges stderr into the parsed stdout buffer -\u003e git warnings corrupt gitResolveCommonDir (exclude entry lands in a bogus silently-created path) and Summary turns stderr into fake commit lines. FIX: separate stdout/stderr buffers; parse stdout only; include stderr in error messages.\n\nF7 MINOR (taskfiles.go:61-79): WriteExitFile renames without fsync -\u003e post-power-loss empty-but-present exit.json makes ReadExitFile error forever. FIX: tmp.Sync() before Close/rename.\n\nF8 MINOR (internal/config/config.go:201-208 + commands.go:32-34): repo slug is not shape-validated; \"/\" or \"..\" in a slug makes worktreePath escape work_root. FIX in internal/config (you MAY touch config for exactly this): reject slugs not matching ^[A-Za-z0-9._-]+$ (and not \".\" / \"..\"), with a validation-collected error + test. This is the ONLY change allowed outside runner/ahub-run.\n\nF9 MINOR (events.go:49-51, 94-108): (a) a single line \u003e10MB aborts the whole scan silently (message_end lines grow with conversation) -\u003e switch to a reader that SKIPS an over-long line and continues; (b) cost summation semantics are under-pinned — fixtures have one assistant message; if pi's usage.cost.total is cumulative, += double-counts. Without re-running pi: add a two-assistant-message fixture built from the existing captured shape, document the per-message assumption in a comment referencing the fixture provenance, and add a TODO-marked guard test so the fixture is easy to re-pin from a real capture later.\n\nF10 test honesty: add the tests named above; also remove/rework TestKillDeadSessionIsNotAnError so it distinguishes \"session already gone (ok)\" from \"kill failed but session alive (error)\".\n\nConstraints: scope = internal/runner/, cmd/ahub-run/, plus the single F8 validation in internal/config/. Conventions: testify + culpa (SPEC §13). Build/vet/test ONLY your packages (./internal/runner/... ./cmd/ahub-run/... ./internal/config/...) with -race; never ./... . Another fixer is working in internal/reconcile concurrently — do not touch it. Commit 'runner: fix review findings (worktree prune, signals, per-attempt status, idempotent start, hardening)' (+ separate 'config: validate repo slug shape' commit) staging only your paths; hooks bypassed (git -c core.hooksPath=/tmp/nohooks commit); retry on index.lock; do NOT push; do NOT touch .beads/.\n","status":"in_progress","priority":0,"issue_type":"bug","owner":"bigbes@gmail.com","created_at":"2026-07-13T00:37:45Z","created_by":"Eugene Blikh","updated_at":"2026-07-13T00:37:46Z","started_at":"2026-07-13T00:37:46Z"}
new_value {"notes":"F11 (VALIDATOR blocker D1, empirically established on real zellij 0.44.3): sessions that lived past zellij's session-serialization tick (~65s, default config) and then die WITHOUT delete-session (pi/ahub-run crash killing the pane, zellij server death, reboot with cache) remain listed by 'list-sessions --short' as bare names indistinguishable from live sessions -\u003e Status reports Running forever; OutcomeCrashed unreachable for real crashes. FIX: switch session liveness to 'zellij list-sessions --no-formatting' (plain text, dead sessions carry the '(EXITED - attach to resurrect)' suffix) and treat EXITED as NOT alive; parse defensively (name = first whitespace-separated token; EXITED detection by substring); update stub scripts + tests incl. an EXITED-listed case. Kill() keeps kill-then-delete (validator confirmed delete-session removes serialized dead sessions, exit 0). ALSO F12 (validator observation): zellij pane env = SESSION-CREATION-TIME server env, not run-client env — so bare 'ahub-run'/'pi' argv depend on the daemon's PATH at attach --create-background time. Harden: resolve ahub-run and pi to ABSOLUTE paths via exec.LookPath at Start (error clearly if not found) and use those in the pane argv; keeps working under systemd's minimal PATH later."}
comment NULL
created_at 2026-07-13T03:44:07Z
id 019f5902-0bc9-7a7f-a47e-45cb6663fabc
issue_id ah-nyl.11
event_type closed
actor Eugene Blikh
old_value
new_value landed 7a489b9 (runner+ahub-run) + b978e0e (config slug): prune-before-add, signal handling w/ pgid, per-attempt status, idempotent start, EXITED-aware liveness via --no-formatting, absolute pane binaries, stream split, fsync, long-line skip; 5 negative controls confirmed
comment NULL
created_at 2026-07-13T04:05:34Z
id 019f7cda-9083-7cb9-a9b3-45a60b99b5ca
issue_id ah-nyl.11
event_type label_added
actor Eugene Blikh
old_value NULL
new_value NULL
comment Added label: milestone:stage-1
created_at 2026-07-20T03:08:46Z
id 019f7cdb-fc5c-719a-96be-a8265b0cc49a
issue_id ah-nyl.11
event_type label_removed
actor Eugene Blikh
old_value NULL
new_value NULL
comment Removed label: milestone:stage-1
created_at 2026-07-20T03:10:19Z