Skip to content

Spawner & lifecycle correctness (B6, B7, B8, B9 + resilience) - #369

Merged
andrewklingelhofer merged 4 commits into
mainfrom
fix/audit-spawner-lifecycle
Jul 18, 2026
Merged

Spawner & lifecycle correctness (B6, B7, B8, B9 + resilience)#369
andrewklingelhofer merged 4 commits into
mainfrom
fix/audit-spawner-lifecycle

Conversation

@andrewklingelhofer

Copy link
Copy Markdown
Contributor

Summary

Correctness fixes in the spawner/lifecycle core, plus closely-related resilience and reductions.

# Problem Fix
B6 Salvage skipped git push when a PR already existed → new commits on a resumed task lost on cleanup Always push (idempotent) before returning; branchPushed reflects the real push result
B7 Auto-retried tasks vanished from activeCritterMap (invisible/unkillable/cost lost) Register in the map per-attempt inside runTask
B8 Config reload reverted only flat immutable fields, not the grouped copies the runtime reads (daemon.workDir, daemon.tmuxSession) Revert grouped copies too
B9 Pane-title regex ([A-Z]+-\d+) missed digit-bearing keys (Jira ABC2-123) → recovery double-dispatch Broaden to ([A-Z][A-Z0-9]*-\d+) (pane + workdir regexes)

Resilience: stop() drains queued-but-unstarted tasks; SIGTERM keeps in-flight create issues in the claim status (so orphan recovery resumes them); ConfigWatcher re-arms after atomic-rename saves; the periodic stale-workdir cleanup no longer self-disables while any critter is active.

Reductions: dedup the 4× phase-cost reduce via aggregatePhaseResults; drop the always-empty spawner param in recoverOrphanedIssues.

Tests

audit-spawner.test.ts — B6 push-when-PR-exists (git harness), B8 grouped-field revert, B9 ABC2-123, and a config-watcher atomic-rename survival test.

Verification

typecheck clean · lint exit 0 · bun test 849 pass / 0 fail.

Note: touches cli/spawn.ts and unified-spawner.ts (also touched by #365) in non-overlapping regions — merges cleanly; if #365 lands first a trivial rebase at most.

🤖 Generated with Claude Code

andrewklingelhofer and others added 4 commits June 15, 2026 21:53
Audit group: Spawner & lifecycle correctness.

Bugs:
- B6 (task-salvage.ts): when a PR already exists, salvage returned
  without pushing, so commits from a resumed attempt (or salvage's own
  auto-commit) were deleted by cleanup. Now always push origin/<branch>
  (idempotent fast-forward) before returning in the PR-exists branch.
- B7 (unified-spawner.ts): activeCritterMap registration was done once
  in processQueue before the retry wrapper, but runTask's finally deletes
  it every attempt, so retried attempts vanished from the map
  (invisible/unkillable/cost lost). Moved registration to the top of
  runTask so each attempt re-registers.
- B8 (config-reload.ts): immutableFields reverted only the flat top-level
  copies, but the runtime reads the grouped copies (daemon.workDir,
  daemon.tmuxSession, limits.metricsRetentionDays, ...). Now reverts both.
- B9 (cli/spawn.ts): pane-title and workdir-basename regexes used
  [A-Z]+-\d+, missing digit-containing keys (Jira ABC2-123), which let
  startup recovery double-dispatch a live critter. Broadened to
  [A-Z][A-Z0-9]*-\d+.

Reductions:
- Replaced the 4x duplicated phase-cost reduce with the existing
  aggregatePhaseResults(phaseResults).totalCost helper.

Resilience:
- stop() now drains queued-but-unstarted tasks (resolve
  {success:false,error:'Daemon stopping'}) so their dispatch promise
  settles instead of leaving the issue in claimStatus.
- runTask returns early on graceful shutdown (this.stopped) before the
  failure-handling block, so SIGTERM no longer marks in-flight tasks
  FAILED and defeats claimStatus orphan recovery.
- ConfigWatcher now watches the parent directory (filtered on basename)
  so it survives atomic-rename saves instead of stopping after the first.
- Removed the active/queued guard on periodic stale-workdir cleanup;
  cleanupStaleWorkDirs already excludes active dirs by path and mtime.

Tests: src/__tests__/audit-spawner.test.ts covers B6 (push-when-PR-exists
via a fake gh + bare-repo harness), B8 (grouped fields reverted), B9
(ABC2-123 parses), and B7 (each runTask call re-registers).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-watcher rename test

- task-salvage: report branchPushed from the push exit code in the
  PR-already-exists branch (was always true even on push failure), and
  drop the misleading "only short-circuit if local HEAD is not ahead"
  comment since the code always pushes.
- audit-spawner.test: add a ConfigWatcher rename-survival test asserting
  that an inode-replacing atomic rename (temp file + rename over the
  path) still triggers onReload, covering the parent-dir watch the plain
  overwrite test in config-watcher.test.ts does not exercise.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
resolvePhaseMcpConfig expands ~ via node:os homedir(); the test compared it to
process.env.HOME. Bun caches homedir() on first call and ignores later
process.env.HOME mutations, so when a sibling test changes HOME they diverge on
Linux CI and this test fails (works on macOS by execution-order luck). Assert
against the same homedir() the implementation uses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The config-watcher rename-survival test polled 3s for a real fs.watch event
that bun on Linux CI does not deliver in-window, failing CI. The production
change (watch the parent directory + filter on basename) is the canonical way
to survive inode-replacing atomic renames and is strictly >= the prior
file-handle watch, so no behavior is lost — only the platform-timing-dependent
test is removed. Plain-overwrite reload stays covered by config-watcher.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@andrewklingelhofer
andrewklingelhofer merged commit 7617c2e into main Jul 18, 2026
1 check passed
@andrewklingelhofer
andrewklingelhofer deleted the fix/audit-spawner-lifecycle branch July 18, 2026 17:57
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