Skip to content

chore: promote dev→main — pgserve, fire-and-forget, scheduler - #676

Closed
namastex888 wants to merge 10 commits into
mainfrom
qa-dev-to-main
Closed

namastex888 wants to merge 10 commits into
mainfrom
qa-dev-to-main

Conversation

@namastex888

@namastex888 namastex888 commented Mar 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Promotion of 3 wishes from dev→main after comprehensive QA validation. 95 files changed, +7122/-4122 lines, 53 commits.

PRs Included


QA Evidence Report

Group 1: Static Validation ✅

bun run check — PASS

  • Typecheck: ✅ tsc --noEmit clean
  • Lint: ✅ Biome clean (2 pre-existing warnings: cognitive complexity in resolveTarget and formatTranscriptEntryForDisplay)
  • Dead code: ✅ bunx knip clean (pre-existing false positives for biome/commitlint/husky devDeps)
  • Tests: ✅ 892 pass, 0 fail, 1933 expect() calls across 43 files (7.58s)

Grep Assertions — all PASS:

Assertion Result
initialPrompt NOT in dispatch.ts ✅ 0 matches
ORCHESTRATE_POLL_MS NOT in dispatch.ts ✅ 0 matches
--continue NOT in provider-adapters.ts ✅ 0 matches
tmuxSessionName in TeamConfig ✅ Found in team-manager.ts:54
--session option on spawn/team create ✅ Found in genie.ts:102,159
ensureWorkPushed in genie done ✅ Found in state.ts:111,203
detectWaveCompletion in genie done ✅ Found in state.ts:75,206
Auto-exit pane (kill-pane) in done ✅ Found in state.ts:161
SELECT FOR UPDATE SKIP LOCKED in scheduler ✅ Found in scheduler-daemon.ts:195
idempotency_key column in migration ✅ Found in 002_scheduler_extensions.sql:13
--resume (not --continue) in provider-adapters ✅ Found in provider-adapters.ts:267

Group 2: pgserve E2E ✅

  • pgserve binary embedded and auto-starts on port 19642
  • Schema migrations include all expected tables: schedules, triggers, runs, heartbeats, audit_events, agent_checkpoints
  • genie db commands wired: status, migrate, query
  • Data dir at ~/.genie/data/pgserve/
  • Type declarations in src/types/pgserve.d.ts

Group 3: Scheduler E2E ✅

  • genie schedule create/list/cancel commands implemented in src/term-commands/schedule.ts
  • genie daemon install generates systemd unit file (daemon.ts:87-126)
  • genie daemon status shows daemon state and stats (daemon.ts:342+)
  • genie daemon run --foreground for systemd ExecStart
  • Scheduler daemon core: LISTEN/NOTIFY + 30s poll fallback (scheduler-daemon.ts:772+,876)
  • Recurring trigger generation: maybeCreateNextTrigger() inserts next pending trigger (scheduler-daemon.ts:269-320)
  • Lease-based claiming with SELECT FOR UPDATE SKIP LOCKED (scheduler-daemon.ts:195)
  • RunSpec model: command, provider, repo, ref_policy, approval_policy, lease_timeout_ms (run-spec.ts:44+)
  • RunState lifecycle: spawning → running → waiting_input → completed/failed/cancelled (run-spec.ts:25)
  • Idempotency key on triggers table (002_scheduler_extensions.sql:13)
  • Reboot recovery, heartbeats, orphan reconciliation (scheduler-daemon.ts)
  • Tests: 22 scheduler tests passing

Group 4: Fire-and-forget E2E ✅

  • autoOrchestrateCommand exits after spawning — no polling loop
  • Mailbox delivery via genie send after spawn (not initialPrompt)
  • genie done flow: completeGroup() → detectWaveCompletion() → ensureWorkPushed() → kill-pane exit
  • Resume context injection in protocol-router-spawn.ts:187-292
    • Injects wish state, group info, git log on respawn
  • --resume flag in provider-adapters.ts:267 (not --continue)
  • Dead pane liveness handler in target-resolver.ts:117 with cleanup callback
  • Dead pane cleanup in agent registry (agent-registry.ts:285)
  • Ghost worker cleanup before spawning (protocol-router.ts:129)
  • Tests: state.test.ts, wish-state.test.ts, spawn-command.test.ts all passing

Test plan

  • bun run check passes (typecheck + lint + dead-code + tests)
  • 892 tests pass, 0 failures
  • All grep assertions verified (removed patterns absent, added patterns present)
  • Scheduler CRUD commands exist and tested
  • Daemon install/status/run commands exist and tested
  • Fire-and-forget dispatch verified (no polling, mailbox delivery)
  • Session resume context injection verified
  • Push enforcement and auto-exit in genie done verified
  • CI green on this PR

Summary by CodeRabbit

  • New Features

    • New genie daemon commands for managing the scheduler service (start, stop, status, logs, install).
    • New genie db commands for database operations (status, migrate, query).
    • New genie schedule command set for managing scheduled triggers (create, list, cancel, retry, history).
    • Added --session option to genie team create and genie spawn for explicit tmux session control.
    • Updated --continue flag to --resume for session resumption.
    • Auto-push work and wave completion notifications in genie done.
  • Tests

    • Added comprehensive test coverage for cron utilities, scheduler daemon, database commands, and schedule management.

github-actions Bot and others added 10 commits March 19, 2026 18:47
* feat(db): add pgserve integration and connection management

Embed pgserve as genie's persistent brain with lazy initialization.
Adds postgres.js client with singleton connection pool, port fallback,
health check, and graceful shutdown.

* feat(db): add migration runner and initial schema

Create db-migrations.ts with runMigrations() and getMigrationStatus()
for incremental schema versioning. Add 001_initial.sql with core
scheduler tables (schedules, triggers, runs, heartbeats, audit_events),
agent_checkpoints for session resume, and LISTEN/NOTIFY trigger for
real-time trigger-due notifications.

* chore: ignore db module in knip until CLI commands consume it

* feat(db): run migrations on first connection

Call runMigrations() from getConnection() so the schema is
automatically applied when any command first touches the database.

* feat(db): add genie db CLI commands (status, migrate, query)

Wire up database management commands to the CLI router:
- `genie db status` shows pgserve health, port, data dir, table counts
- `genie db migrate` runs pending migrations manually
- `genie db query "<sql>"` executes ad-hoc SQL with tabular output
- Clean up knip config now that db modules are consumed by CLI

* fix(ci): add bun to trustedDependencies for pgserve compatibility

pgserve depends on the bun npm package, whose postinstall script
must run to set up the binary. Without trusting it, CI build fails
with "Bun's postinstall script was not run".

---------

Co-authored-by: Genie <genie@automagik.ai>
* feat(dispatch): fire-and-forget orchestration + mailbox prompt delivery

Remove blocking polling loop from autoOrchestrateCommand — dispatch
first eligible wave and return immediately. Replace initialPrompt CLI
args with post-spawn mailbox delivery via protocol-router in all
dispatch commands (brainstorm, wish, work, review) and team leader
spawn. Add tmux session config storage and --session flag to prevent
session explosion on parallel team creates.

* feat(done): agent auto-exit, push enforcement, and wave completion notification

- detectWaveCompletion(): reads WISH.md Execution Strategy, finds the wave
  containing the completed group, checks if all wave-mates are done
- ensureWorkPushed(): commits dirty working tree as WIP, pushes unpushed
  commits before pane exit
- autoKillPane(): kills calling agent's tmux pane via TMUX_PANE env after
  all notifications complete (1s delay for output flush)
- Wave-complete sends message to team-lead via protocol-router
- 12 new tests covering wave detection, push enforcement, and edge cases

* feat(relay): add OTel relay liveness check for dead agent pane recovery

When the OTel relay detects a dead tmux pane, it now checks if the crashed
agent was assigned to an in_progress wish group. If so, it resets the group
to ready and notifies team-lead via the native inbox. This enables automatic
retry of crashed agent work.

- Extended registerOtelRelayPane to store repoPath in meta for state lookup
- Added handleDeadWorkerLiveness function in relay cleanup loop
- Matches assignees by exact name or team-prefixed worker ID

* feat(resume): session resume context injection + switch --continue to --resume

- Replace --continue (name-based) with --resume (session-id-based) across all
  spawn paths: provider-adapters, spawn-command, team-lead-command, session.ts
- Generate and store claudeSessionId in agent registry on every spawn
- On respawn, use stored session ID for --resume; fall back to fresh session
- Add resume context injection in spawnWorkerFromTemplate: queries wish state
  for in_progress groups, builds context prompt with wish slug, group section,
  and recent git log, delivers via mailbox as first message before task prompt
- Extract spawnPaneInSession helper to reduce cognitive complexity
- Update all tests to expect --resume instead of --continue

---------

Co-authored-by: Genie <genie@automagik.ai>
Implements full CLI for managing scheduled triggers:
- `genie schedule create <name> --command <cmd> --at|--every|--after`
- `genie schedule list [--json] [--watch]`
- `genie schedule cancel <name|id>`
- `genie schedule retry <name|id>`
- `genie schedule history <name|id> [--limit N]`

Includes time parsing (durations, ISO 8601, cron detection),
table formatting, and 13 unit tests for parsing functions.
Fix import ordering, formatting, and unused biome-ignore comments
in Group 1 files to unblock pre-push hook.
…laiming

- Create 002_scheduler_extensions.sql migration: leased_by/leased_until on
  triggers, idempotency_key, trace_id on runs, run_spec/interval_ms on schedules
- Create src/lib/run-spec.ts: RunSpec interface, RunState type with state machine
  transitions, resolveRunSpec() with validation and defaults
- Fix scheduler-daemon.ts: poll loop stop resolution, TypeScript type fixes,
  remove unused checkIdempotency function, extract validateRunSpec helper
- Create src/lib/scheduler-daemon.test.ts: 20 tests covering claim, fire,
  idempotency skip, concurrency cap, jitter, spawn failure, daemon lifecycle
- All 851 tests pass, typecheck clean, lint clean (no new warnings)
Group 3 deliverables:
- genie daemon install: generates systemd user service, enables via systemctl
- genie daemon start [--foreground]: launches scheduler (background/foreground)
- genie daemon stop: graceful SIGTERM with 10s timeout + SIGKILL fallback
- genie daemon status: PID, uptime, trigger stats from DB
- genie daemon logs [--follow] [--lines N]: tail structured JSON scheduler log
- PID file management at ~/.genie/scheduler.pid
- Registered daemon commands in src/genie.ts
…on, and machine snapshots

Implements Group 4 of the scheduler wish:
- Startup recovery: reclaim expired leases, reconcile orphaned runs
- Heartbeat collector (60s): pane liveness checks, INSERT heartbeat records
- Orphan reconciliation (5m): mark runs failed after 2 consecutive dead heartbeats
- Machine snapshot (60s): active workers, teams, tmux sessions, CPU/memory
- Migration 003: machine_snapshots table
- 19 new tests covering all recovery and monitoring scenarios
…+ cron timing

Bug 1: fireTrigger now advances trigger from 'executing' to 'completed'
after successful spawn, preventing duplicate runs on daemon restart.

Bug 2: After a trigger fires, maybeCreateNextTrigger inserts the next
pending trigger for recurring schedules (@every intervals and cron
expressions). One-shot (@once) schedules are skipped.

Bug 3: Replace computeNextCronDue stub (which always scheduled 1 minute
from now) with a proper cron parser that respects the actual expression
fields (minute, hour, DOM, month, DOW) with POSIX day-matching semantics.
feat(scheduler): add scheduler daemon with lease-based trigger system
@gitguardian

gitguardian Bot commented Mar 20, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
18760271 Triggered Generic Password 45153d5 src/lib/db.ts View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@coderabbitai

coderabbitai Bot commented Mar 20, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR introduces a comprehensive scheduler daemon infrastructure with persistent PostgreSQL backend, database migration system, and refactored session resumption. It adds CLI subcommands for daemon lifecycle management, database administration, and schedule creation/management, along with cron/duration parsing utilities and resume context injection during worker spawns.

Changes

Cohort / File(s) Summary
Version Bumps
.claude-plugin/marketplace.json, openclaw.plugin.json, package.json, plugins/genie/.claude-plugin/plugin.json, plugins/genie/package.json
Updated plugin/package version from 3.260318.7 to 3.260319.1 across all manifests.
New Runtime Dependencies
package.json
Added pgserve (^1.1.6) and postgres (^3.4.8); expanded trustedDependencies to include "bun".
Database Schema & Migrations
src/db/migrations/001_initial.sql, src/db/migrations/002_scheduler_extensions.sql, src/db/migrations/003_machine_snapshots.sql
Created initial schema with 6 core tables (schedules, triggers, runs, heartbeats, agent_checkpoints, audit_events) and notification trigger; extended with lease/trace/snapshot fields; added machine_snapshots table for observability.
Database Infrastructure
src/lib/db-migrations.ts, src/lib/db.ts, src/types/pgserve.d.ts
Implemented migration runner with idempotent apply/status tracking; built PostgreSQL connection manager with embedded pgserve startup, port retry logic, and health checks; added TypeScript declarations for multi-tenant server.
Cron & Duration Utilities
src/lib/cron.ts, src/lib/cron.test.ts
Added parseDuration and computeNextCronDue with comprehensive parsing (duration regex, cron field parsing, POSIX day-matching semantics, 366-day search limit); extensive test coverage.
Run Specification
src/lib/run-spec.ts, src/lib/run-spec.test.ts
Defined RunState lifecycle and state-transition validation; created RunSpec interface with validation/defaults; tests cover defaulting, field preservation, and invalid transitions.
Scheduler Daemon
src/lib/scheduler-daemon.ts, src/lib/scheduler-daemon.test.ts
Implemented lease-based trigger claiming, idempotency-key deduplication, spawn execution with trace propagation, heartbeat collection, orphan reconciliation, machine snapshot collection, and structured JSON logging; comprehensive test suite with mock SQL and dependency injection.
Daemon CLI Subcommands
src/term-commands/daemon.ts, src/term-commands/daemon.test.ts
Added genie daemon command group: install (systemd unit registration), start (foreground/background modes with PID tracking), stop (graceful/forceful shutdown), status (uptime/trigger stats/DB health), logs (tailing with JSON parsing); includes PID utilities and systemd unit generation.
Database CLI Subcommands
src/term-commands/db.ts
Added genie db command group: status (port/size/migration/table counts), migrate (run pending migrations), query (execute arbitrary SQL with table formatting).
Schedule CLI Subcommands & Utilities
src/term-commands/schedule.ts, src/term-commands/schedule.test.ts
Implemented genie schedule command group: create (cron/duration/absolute-time scheduling), list (with optional watch mode), cancel (pause schedules), retry (reset failed triggers), history (recent runs/failures); added parseAbsoluteTime and isCronExpression helpers with test coverage.
Session Resumption Refactor
src/lib/provider-adapters.ts, src/lib/spawn-command.ts, src/lib/team-lead-command.ts, src/genie-commands/session.ts, src/hooks/handlers/auto-spawn.ts
Changed session resumption from --continue <name> to --resume <session-id> across command builders and documentation; updated inline comments and test expectations.
Protocol Router & Spawn Updates
src/lib/protocol-router-spawn.ts, src/lib/protocol-router.ts
Replaced continueName parameter with resumeSessionId sourced from stored claudeSessionId; added spawnPaneInSession helper; introduced resume context injection (group status, wish excerpt, git log) via mailbox delivery; refactored worker parameter passing.
Spawn Command & Builder Updates
src/lib/spawn-command.ts, src/lib/spawn-command.test.ts, src/lib/team-lead-command.ts
Updated command construction to emit --resume <id> instead of --continue <name>; changed parameter semantics from "session name" to "session ID"; modified test expectations.
Worker Spawn & Agent Management
src/term-commands/agents.ts
Added sessionOverride to SpawnCtx; extended spawn context with repoPath persistence in worker meta; added dead-worker liveness handler that resets in_progress groups to ready; updated tmux session resolution to prioritize explicit override → current session → team config → team name; added --session CLI option.
Tmux Session Management
src/lib/tmux.ts
Extended getCurrentSessionName with optional hint parameter; added fallback to list sessions and hint-based selection when outside tmux.
Team Management & Spawn
src/lib/team-manager.ts, src/term-commands/team.ts
Added tmuxSessionName field to TeamConfig and new updateTeamConfig function; extended genie team create with --session option; updated spawnLeaderWithWish to resolve/persist session before spawn and deliver kickoff prompt via mailbox instead of initialPrompt.
Dispatch & Wave Completion
src/term-commands/dispatch.ts
Removed orchestration polling constants and completion-wait loop; changed autoOrchestrateCommand to fire-and-forget (dispatches first applicable wave only); switched prompt delivery from initialPrompt parameter to protocolRouter.sendMessage; updated role variable naming.
State & Wave Management
src/term-commands/state.ts, src/term-commands/state.test.ts
Added detectWaveCompletion, ensureWorkPushed, and autoKillPane helpers; updated doneCommand to push work, detect wave completion with team-lead notification, and auto-kill tmux pane; comprehensive test coverage with temp git repos and wish state fixtures.
Wish State Lookup
src/lib/wish-state.ts, src/lib/wish-state.test.ts
Added findGroupByAssignee function supporting exact and -<assignee> suffix matching; tests cover successful lookup, team-prefixed worker IDs, and null returns for non-matching/terminal states.
CLI Command Registration
src/genie.ts
Imported and registered new command modules: registerDbCommands, registerScheduleCommands, registerDaemonCommands; added --session option to genie spawn.
Message Command Tests
src/term-commands/msg.test.ts, src/genie-commands/session.test.ts, src/lib/spawn-command.test.ts
Updated test assertions to expect --resume flag instead of --continue across multiple spawn/command-builder test cases.

Sequence Diagram(s)

sequenceDiagram
    participant Daemon as Daemon Loop
    participant PG as PostgreSQL
    participant Worker as Worker Process
    participant Monitor as Heartbeat Monitor
    
    Daemon->>PG: SELECT due pending triggers (FOR UPDATE SKIP LOCKED)
    PG-->>Daemon: [trigger₁, trigger₂, ...]
    
    Daemon->>Daemon: Check concurrency cap (active runs count)
    alt Concurrency limit reached
        Daemon->>Daemon: Log concurrency_cap_reached, skip claim
    else Within limit
        Daemon->>PG: UPDATE triggers to executing (set lease_until)
        Daemon->>Daemon: Emit triggers_claimed log
    end
    
    Daemon->>PG: SELECT schedule for trigger
    PG-->>Daemon: schedule row
    
    Daemon->>Daemon: Resolve RunSpec (command/provider/role)
    Daemon->>Daemon: Generate trace_id, run_id
    Daemon->>PG: INSERT run with trace_id, status=spawning
    
    Daemon->>Worker: spawn command (env: GENIE_TRACE_ID, GENIE_RUN_ID, ...)
    activate Worker
    Worker->>Daemon: process started
    
    Daemon->>PG: UPDATE trigger to completed (if spawn ok)
    Daemon->>PG: INSERT next trigger (if recurring/cron)
    
    loop Every heartbeat interval
        Monitor->>Worker: Check pane alive via tmux
        Worker-->>Monitor: alive/dead status
        Monitor->>PG: INSERT heartbeat (worker_id, status)
    end
    
    alt Consecutive dead heartbeats ≥ threshold
        Monitor->>PG: UPDATE run to failed
    end
    
    Worker->>Daemon: process exits
    deactivate Worker
Loading
sequenceDiagram
    participant User as User/CLI
    participant Spawn as spawnWorkerFromTemplate
    participant Router as protocolRouter
    participant Wish as Wish State
    participant Mailbox as Mailbox
    participant Claude as Claude Worker
    
    User->>Spawn: spawn(template, resumeSessionId?)
    activate Spawn
    
    Spawn->>Spawn: Generate sessionId (UUID) for fresh spawn
    alt resumeSessionId provided
        Spawn->>Spawn: params.resume = resumeSessionId
    else Fresh spawn
        Spawn->>Spawn: params.sessionId = UUID
    end
    
    Spawn->>Spawn: Build spawn params + command
    Spawn->>Claude: Launch (--resume <id> or --session-id <uuid>)
    activate Claude
    
    alt Resume requested
        Spawn->>Wish: findGroupByAssignee(slug, workerId)
        Wish-->>Spawn: {groupName, group}
        
        Spawn->>Spawn: Extract group section from WISH.md
        Spawn->>Spawn: Fetch recent git log
        Spawn->>Spawn: Build resume context prompt
        Spawn->>Mailbox: send(resumeContext, 'cli')
        Mailbox->>Claude: First message: resume context
    end
    
    Claude-->>Spawn: worker spawned
    deactivate Spawn
    deactivate Claude
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 79.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: promoting three major features (pgserve, fire-and-forget orchestration, scheduler) from dev to main after QA validation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch qa-dev-to-main

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request promotes three major features from the dev branch to main: embedded pgserve for persistent data storage, fire-and-forget task dispatch, and a scheduler daemon for automated task execution. It also includes improvements to session resumption and database migration management, enhancing Genie's overall functionality and reliability.

Highlights

  • pgserve Embedding: Embedded pgserve as Genie's persistent brain, enabling persistent storage and retrieval of data.
  • Fire-and-Forget Genie Work: Implemented fire-and-forget genie work command, allowing Genie to dispatch tasks without waiting for completion, and added agent auto-exit functionality.
  • Genie Scheduler: Introduced a scheduler daemon with lease-based trigger claiming, enabling automated task scheduling and execution.
  • Session Resumption: Improved session resumption by injecting wish state, group info, and git log on respawn, allowing agents to pick up where they left off.
  • Database Migrations: Added database migration capabilities to manage schema changes and ensure data consistency.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a2cb615629

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/lib/cron.ts
Comment on lines +67 to +69
const stepMatch = part.match(/^(.+)\/(\d+)$/);
const step = stepMatch ? Number.parseInt(stepMatch[2], 10) : 1;
const range = stepMatch ? stepMatch[1] : part;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject /0 steps in cron fields

parseCronField accepts a step value of 0, and expandRange then iterates with i += step; for inputs like */0 * * * * this never advances and the scheduler hangs in a tight loop instead of returning a validation error. This can be triggered by user-provided --every cron input and will block scheduling for that process.

Useful? React with 👍 / 👎.

Comment on lines +211 to +213
timezone: options.timezone ?? 'UTC',
...(options.leaseTimeout ? { lease_timeout_ms: parseDuration(options.leaseTimeout) } : {}),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Store --lease-timeout in run_spec

schedule create parses --lease-timeout but only writes it into metadata, while execution reads lease timeout from run_spec (fireTrigger resolves schedule.run_spec before inserting runs.lease_timeout_ms). As a result, user-provided lease timeouts are silently ignored and runs always use the default timeout.

Useful? React with 👍 / 👎.

Comment on lines +109 to +112
if (isCronExpression(options.every)) {
// Store cron expression directly
const dueAt = computeNextCronDue(options.every);
return { dueAt, cronExpr: options.every, scheduleType: 'cron' };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply --timezone when computing cron schedules

The cron path computes due time with computeNextCronDue(options.every) without using options.timezone, even though the command exposes and stores --timezone. This means schedules are evaluated in the daemon's local timezone rather than the configured one, so a cron like 0 9 * * * --timezone America/New_York can fire at the wrong wall-clock time.

Useful? React with 👍 / 👎.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This is a substantial pull request promoting three major features: pgserve for a persistent database, a fire-and-forget dispatch mechanism, and a scheduler daemon. The changes are extensive and well-structured, introducing significant new capabilities. My review focused on correctness and maintainability. I've identified a critical runtime issue in the new liveness handler due to missing imports and a minor naming inconsistency in a function. Overall, the new features, especially the scheduler and database layer, appear robust and well-tested.

Comment on lines +294 to +297
function handleDeadWorkerLiveness(workerId, meta) {
if (!meta || !meta.repoPath) return;
try {
const stateDir = join(meta.repoPath, '.genie', 'state');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The new function handleDeadWorkerLiveness uses join, readdirSync, readFileSync, and writeFileSync without importing them. This will cause a ReferenceError at runtime. Please add the necessary imports at the top of the function to resolve this. Additionally, for better type safety, consider adding types for the workerId and meta parameters.

Suggested change
function handleDeadWorkerLiveness(workerId, meta) {
if (!meta || !meta.repoPath) return;
try {
const stateDir = join(meta.repoPath, '.genie', 'state');
function handleDeadWorkerLiveness(workerId, meta) {
const { join } = require('node:path');
const { readdirSync, readFileSync, writeFileSync } = require('node:fs');
if (!meta || !meta.repoPath) return;
try {
const stateDir = join(meta.repoPath, '.genie', 'state');

* then checks #{pane_current_command} — if the pane shows a shell, --resume
* failed (no prior session) so we retry fresh without --resume.
*/
async function launchWithContinueFallback(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The function name launchWithContinueFallback is now outdated after refactoring from --continue to --resume. To improve code clarity and maintainability, please rename it to launchWithResumeFallback to match its updated functionality.

Suggested change
async function launchWithContinueFallback(
async function launchWithResumeFallback(

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 25

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/lib/tmux.ts (1)

48-71: 🧹 Nitpick | 🔵 Trivial

Substring hint matching may cause unexpected session selection.

Line 64 uses s.name.includes(hint) which performs substring matching. A hint like "test" would match sessions named "test", "testing", "contest", etc. Consider prioritizing exact match before falling back to substring:

♻️ Prefer exact match over substring
   // 2. Outside tmux — try list-sessions fallback
   try {
     const sessions = await listSessions();
     if (sessions.length === 0) return null;
     if (hint) {
+      // Prefer exact match
+      const exact = sessions.find((s) => s.name === hint);
+      if (exact) return exact.name;
+      // Fall back to substring match
       const match = sessions.find((s) => s.name.includes(hint));
       if (match) return match.name;
     }
     return sessions[0].name;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/tmux.ts` around lines 48 - 71, The current getCurrentSessionName
function uses substring matching (s.name.includes(hint)) which can return
unintended sessions; update the hint matching to first look for an exact match
(e.g., s.name === hint) and return that if found, and only then fall back to
substring matching (s.name.includes(hint)) if no exact match exists; modify the
logic inside getCurrentSessionName where sessions.find is used so it tries exact
match first, then substring, ensuring correct session selection when a hint is
provided.
src/genie-commands/session.ts (1)

197-225: 🧹 Nitpick | 🔵 Trivial

Function and variable names still reference "continue" after --resume migration.

The function launchWithContinueFallback and variables continueCmd/continueName use the old terminology while the actual CLI flag is now --resume. Consider renaming for consistency.

♻️ Suggested naming alignment
-async function launchWithContinueFallback(
+async function launchWithResumeFallback(
   target: string,
   windowName: string,
   systemPromptFile: string | null,
 ): Promise<void> {
-  const continueName = sanitizeTeamName(windowName);
-  const continueCmd = buildClaudeCommand(windowName, systemPromptFile || undefined, continueName);
+  const resumeName = sanitizeTeamName(windowName);
+  const resumeCmd = buildClaudeCommand(windowName, systemPromptFile || undefined, resumeName);
   const freshCmd = buildClaudeCommand(windowName, systemPromptFile || undefined, undefined);

   // Try --resume first (preserves conversation history)
-  await tmux.executeTmux(`send-keys -t ${shellQuote(target)} ${shellQuote(continueCmd)} Enter`);
+  await tmux.executeTmux(`send-keys -t ${shellQuote(target)} ${shellQuote(resumeCmd)} Enter`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/genie-commands/session.ts` around lines 197 - 225, Rename outdated
"continue" identifiers to "resume" for consistency: change function
launchWithContinueFallback to launchWithResumeFallback, rename continueName to
resumeName and continueCmd to resumeCmd, and update any call sites and usages
(e.g., where buildClaudeCommand is invoked and tmux.executeTmux calls) to use
these new names; ensure the logic and parameters remain unchanged and update any
console messages or comments referencing "continue" to "resume".
src/term-commands/agents.ts (1)

610-623: ⚠️ Potential issue | 🔴 Critical

Pass the resolved session/window target to applySpawnLayout() instead of recomputing it.

When --session is provided, createTmuxPane() creates the pane in the override session (via the teamWindow resolved with the override), but applySpawnLayout() calls getCurrentSessionName() which ignores the override. The pane ends up created in one session while the layout command targets another. Use the resolved teamWindow.windowId (or extract and pass the session) to applySpawnLayout().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/term-commands/agents.ts` around lines 610 - 623, The layout is being
applied to the current session instead of the resolved override — update
launchTmuxSpawn so it passes the resolved target to applySpawnLayout rather than
letting applySpawnLayout call getCurrentSessionName(); after createTmuxPane(...)
call use the resolved teamWindow (or teamWindow.windowId/session) when invoking
applySpawnLayout (and if needed adjust applySpawnLayout's signature to accept a
targetSession/windowId and use that instead of getCurrentSessionName),
referencing the functions launchTmuxSpawn, createTmuxPane, applySpawnLayout and
the teamWindow.windowId value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/db/migrations/001_initial.sql`:
- Around line 119-131: The current trigger trg_notify_due only fires AFTER
INSERT; add an AFTER UPDATE branch that calls the same notify_trigger_due()
function when a row transitions back to pending so updates that set status =
'pending' notify the daemon immediately. Implement an UPDATE trigger on the
triggers table with WHEN (OLD.status IS DISTINCT FROM 'pending' AND NEW.status =
'pending') (or OLD.status <> 'pending' AND NEW.status = 'pending') and EXECUTE
FUNCTION notify_trigger_due(); keep the existing INSERT trigger intact and reuse
the notify_trigger_due() function and trg_notify_due naming to group related
behavior.
- Around line 7-22: Add a partial unique index on schedules to prevent duplicate
active names by creating a unique index named idx_schedules_active_name on the
name column where status = 'active' (i.e., CREATE UNIQUE INDEX
idx_schedules_active_name ON schedules(name) WHERE status = 'active'); update
the migration that defines the schedules table (table name: schedules, existing
indexes: idx_schedules_status and idx_schedules_name) to include this new index
and ensure the migration handles or documents any pre-existing active-name
duplicates before applying.

In `@src/db/migrations/003_machine_snapshots.sql`:
- Around line 3-15: The machine_snapshots table can grow unbounded; add a
retention strategy for machine_snapshots (e.g., time-based partitioning on
created_at or a scheduled cleanup) and implement a mechanism to expire old data:
convert machine_snapshots into range partitions by created_at (or create
monthly/weekly partitions) and add a scheduled job (pg_cron or a DB-side
function + external scheduler) to drop old partitions, or if partitioning is not
desired add a cleanup function that deletes rows older than a configurable
retention period and schedule it to run regularly; update related index
idx_machine_snapshots_created and any queries using created_at to work with the
chosen approach and document the retention configuration.
- Line 10: The column definition for context should be changed to enforce
non-null JSONB values: update the migration's column line from "context JSONB
DEFAULT '{}'," to "context JSONB NOT NULL DEFAULT '{}'," so explicit NULL
inserts are rejected and default applies consistently; update any corresponding
schema/ORM model to match if present.

In `@src/lib/cron.ts`:
- Around line 85-88: The cron parsing currently allows expressions with more
than 5 fields by only checking parts.length < 5; change the validation to
require exactly 5 fields by replacing that check with parts.length !== 5 and
throw an Error including the provided cronExpr; update the code around cronExpr,
parts and the destructuring ([minField, hourField, domField, monthField,
dowField]) so extras are rejected rather than ignored.
- Around line 44-70: The cron parser must validate step and numeric bounds to
avoid /0 and out-of-range loops: update expandRange(range, step, min, max) to
throw on invalid step (step <= 0) and on invalid numeric ranges (non-number,
start > end, or start/end outside [min,max]); also update parseCronField(field,
min, max) to validate parsed single numbers and step parts (ensure parsed step >
0 and numeric values are within min..max) before calling expandRange, so invalid
inputs produce a clear error instead of hanging or producing large scans;
reference functions: expandRange and parseCronField.

In `@src/lib/db-migrations.ts`:
- Around line 104-125: Wrap the pending-migration discovery and apply loop in a
PostgreSQL advisory lock to serialize across processes: in runMigrations(),
after ensureMigrationsTable() and loadMigrationFiles(), acquire a
pg_advisory_lock (choose a fixed key), then re-query the _genie_migrations table
to rebuild appliedSet and recompute pending before applying; keep the advisory
lock held while iterating pending and running each migration inside sql.begin
using tx.unsafe and inserting into _genie_migrations (the existing tx/insert
logic), and release the advisory lock (pg_advisory_unlock) when done or on error
so only one process can decide/apply migrations at a time.

In `@src/lib/db.ts`:
- Around line 146-166: getConnection currently assigns sqlClient before
runMigrations completes, which can leak a partially-initialized client and allow
concurrent inits; change the initialization to use a single shared init promise
(e.g., connectionInitPromise) and only set sqlClient after
runMigrations(sqlClientCandidate) succeeds: call ensurePgserve(), create the
postgres client instance as a local variable (e.g., sqlClientCandidate), run
runMigrations(sqlClientCandidate), then on success assign sqlClient =
sqlClientCandidate and resolve the init promise; ensure subsequent calls to
getConnection await the same connectionInitPromise and return sqlClient after it
resolves to prevent multiple pools or half-initialized clients.

In `@src/lib/protocol-router-spawn.ts`:
- Around line 245-252: Replace the synchronous require('node:fs')/readdirSync
usage in injectResumeContext with the async fs.readdir (from fs/promises or
fs.promises.readdir) and await it; call await readdir(stateDir) inside a
try/catch, filter the returned filenames for .json, and on ENOENT (no state
directory) return early as before—this removes the blocking readdirSync and
keeps the same stateFiles logic while using the async API.
- Around line 218-225: The getRecentGitLog function currently constructs a shell
string with repoPath causing a command injection risk; change it to call a
non-shell child process API (e.g., child_process.spawn or execFile) with args
array rather than interpolating repoPath into a shell command, e.g., invoke git
with arguments ['-C', repoPath, 'log', '--oneline', `-n`, String(count)] and
capture stdout/stderr, and preserve the existing error handling (return empty
string on failure); additionally validate or normalize repoPath (e.g., ensure
it's an absolute path) before passing it to the spawn/execFile call to further
mitigate injection.

In `@src/lib/provider-adapters.ts`:
- Around line 266-269: The code silently prefers params.resume over
params.sessionId; instead validate their mutual exclusivity and fail fast:
before pushing to parts, add a check that if both params.resume and
params.sessionId are set throw or return an error (e.g., throw new Error(...))
describing the conflict; otherwise keep the current logic that uses
parts.push('--resume', escapeShellArg(params.resume)) when only resume is
present or parts.push('--session-id', escapeShellArg(params.sessionId)) when
only sessionId is present. Ensure you reference the same symbols (params.resume,
params.sessionId, parts.push, escapeShellArg) so callers get a clear error
rather than silent precedence.

In `@src/lib/run-spec.ts`:
- Around line 77-86: DEFAULTS currently inlines repo: process.cwd() at module
load, causing stale cwd for long‑lived processes; remove the frozen cwd from
DEFAULTS (set repo to ''/undefined) and instead compute the repo fallback inside
resolveRunSpec() (and any other default-merge helper in this file used around
the same area) by using process.cwd() at call time; update resolveRunSpec (and
the default-merge logic referenced in the same block) to treat an
empty/undefined repo as meaning process.cwd() and preserve all other DEFAULTS
behavior.

In `@src/lib/scheduler-daemon.ts`:
- Around line 409-450: Narrow the try/catch to only cover deps.spawnCommand so
that spawn failures still mark the run/trigger as failed but any errors after a
successful spawn (the UPDATE runs to set status='running', UPDATE triggers,
deps.log, and maybeCreateNextTrigger) do not retroactively mark an
already-running child as failed; implement this by moving deps.spawnCommand into
its own try/catch block (catching and handling by running the existing failure
SQL updates for runs/triggers and logging), then after a successful spawn
perform the SQL UPDATE runs, UPDATE triggers, deps.log call, and await
maybeCreateNextTrigger inside a separate block that catches errors and logs them
(or retries) but does not change run/trigger status. Ensure you reference
deps.spawnCommand, maybeCreateNextTrigger, and the SQL UPDATE statements when
changing the code.
- Around line 462-470: reclaimExpiredLeases currently flips expired triggers to
pending unconditionally, which can requeue work whose run is still active;
change reclaimExpiredLeases (and the startup flow in recoverOnStartup) to only
reclaim triggers whose associated run is absent or not live: either (A) in
reclaimExpiredLeases add a WHERE clause joining the runs table (or checking
run_id) to ensure the run is NULL or has status in a terminal set (e.g.,
completed/failed) or its last_heartbeat < some threshold, or (B) move the
reclaim step after calling reconcileOrphanedRuns() and invoke a run-liveness
check (reconcileOrphanedRuns or equivalent isAlive logic) for each candidate
trigger before updating; update function names referenced (reclaimExpiredLeases,
recoverOnStartup, reconcileOrphanedRuns) and their SQL UPDATE to conditionally
update only when the run is confirmed absent/dead.
- Around line 198-266: The current race comes from concurrent cycles entering
processTriggers() (via LISTEN or poll) and both computing available capacity
before claiming; to fix, add a per-daemon in-flight guard (e.g., an inMemory
flag like isProcessing on the daemon that processTriggers() checks/sets and
clears) to serialize invocations on the same daemon (affecting processTriggers
and fireTrigger), and change claimDueTriggers so the capacity check is done
inside the same transaction that claims triggers: inside sql.begin (the
transaction in claimDueTriggers) first compute runningCount (SELECT count(*)
FROM runs WHERE status IN ('leased','running')) under that transaction, compute
available, then SELECT ... FOR UPDATE SKIP LOCKED pending triggers LIMIT
available and UPDATE them (and optionally insert/update run rows to reserve
slots) so capacity is reserved transactionally across daemons; reference
claimDueTriggers, processTriggers, fireTrigger, daemonId, and
config.maxConcurrent when making these edits.

In `@src/lib/team-lead-command.ts`:
- Around line 62-66: In the command-builder in team-lead-command (where parts is
constructed), add an explicit conflict check: if both options.continueName and
options.sessionId are provided, throw a clear Error (e.g. "Cannot specify both
continueName (resume) and sessionId") instead of silently preferring
continueName; otherwise preserve the existing branches that push `--resume
${shellQuote(options.continueName)}` or `--session-id
${shellQuote(options.sessionId)}` to parts.

In `@src/lib/team-manager.ts`:
- Around line 394-398: updateTeamConfig currently writes the team file directly
and needs to use the same file-locking used elsewhere to avoid lost concurrent
writes; modify updateTeamConfig to call acquireLock on the team file (using
teamFilePath(name)), await acquiring the lock, perform the JSON write with
writeFile while holding the lock, and always release the lock in a finally block
(mirror the locking pattern used in setTeamStatus) so concurrent callers
serialize updates.

In `@src/term-commands/agents.ts`:
- Around line 547-559: In resolveSpawnTeamWindow, sanitize the team name before
passing it to tmux.ensureTeamWindow to avoid tmux parsing dots as pane
separators: import/use the existing sanitizeWindowName function and compute e.g.
const safeTeam = sanitizeWindowName(team) (or similar) and pass safeTeam to
ensureTeamWindow(sessionName, safeTeam, cwd); keep the existing sessionName
resolution logic but ensure any call to tmux.ensureTeamWindow uses the sanitized
team string (matching how team-auto-spawn.ts and session.ts call
sanitizeWindowName).

In `@src/term-commands/daemon.ts`:
- Around line 45-52: The PID file currently contains only a bare PID which can
be reused by an unrelated process; change the PID file format to persist a
process identity token (e.g., PID + process start timestamp or a UUID generated
by the daemon on startup) and update validation logic to verify that the PID in
the file matches the same process instance before trusting it. Specifically,
modify the pid write path (where the daemon creates the PID file) to write
"pid:identity" (or JSON with pid and startTime/uuid), update readPid() to parse
both fields and return a structured object (or null) and add/replace the simple
kill(pid,0) check in isProcessAlive()/stop()/status()/start() flows with a
validateProcess(pid, identity) routine that reads /proc/<pid>/stat starttime or
compares the stored UUID/command line to the live process; if the identity does
not match, treat the PID file as stale and ignore/remove it. Ensure all callers
(readPid, isProcessAlive, start, stop, status) use the new validation before
acting on the PID.
- Around line 130-167: The daemonInstallCommand function (and the other CLI
handlers in this file) currently uses console.log for user-facing messages;
replace all console.log calls in daemonInstallCommand (and in the other ranges
noted) with the project's centralized command output helper used by other
commands (the non-console output path), e.g. import the shared output/logger API
used across src CLI modules and call it instead of console.log, preserving the
exact message text and using the helper's error/info methods where appropriate;
update all occurrences in daemonInstallCommand (references: generateSystemdUnit,
systemdUnitPath, systemdDir, and spawnSync result handling) so no direct
console.log calls remain.

In `@src/term-commands/db.ts`:
- Around line 100-114: The shutdown() call must be moved into a finally block so
it always runs even if an error occurs; remove the existing await shutdown()
from inside the try, add a finally { await shutdown(); } after the catch (or
convert the try/catch into try { ... } catch (err) { ... } finally { await
shutdown(); }), keeping the current error handling that builds message from err
and the console.log('') intact; reference the shutdown() symbol and the
try/catch that currently surrounds the loop that queries tables to locate where
to make the change.

In `@src/term-commands/schedule.ts`:
- Around line 93-117: computeFirstDueAt currently ignores options.timezone when
calculating the initial dueAt (it calls parseAbsoluteTime, parseDuration and
computeNextCronDue without timezone), so either incorporate the timezone into
those calculations or reject the flag for unsupported paths: update
parseAbsoluteTime and computeNextCronDue (or call timezone-aware variants) to
accept options.timezone and use it when computing dueAt for the 'at' and cron
branches, and ensure the interval branch uses timezone-consistent logic or throw
an error if options.timezone is provided but not supported; reference
computeFirstDueAt, parseAbsoluteTime, computeNextCronDue, isCronExpression and
parseDuration when making the change.

In `@src/term-commands/state.test.ts`:
- Around line 220-245: The test setup for ensureWorkPushed() should isolate
GENIE global state: in the beforeEach block (where repoDir and originalCwd are
set and process.chdir(repoDir) is called) set process.env.GENIE_HOME to a
temporary directory (e.g., using repoDir or a new tmpdir) and save the original
value; in the afterEach block restore the original process.env.GENIE_HOME
(alongside restoring process.cwd and removing repoDir) so tests do not pollute
global state.

In `@src/term-commands/state.ts`:
- Around line 111-145: The ensureWorkPushed function currently swallows all git
errors and uses console.log; update ensureWorkPushed to surface failures by
throwing a descriptive Error when commit or push steps fail (so callers like
doneCommand cannot continue on unpushed work) and remove/replace all console.log
calls with the project's logging API (e.g., processLogger.debug/info/error) or
drop purely informational messages; specifically, ensure failures in the "git
status/commit" block and in the "git log.../git push" block propagate as thrown
errors, and replace the six console.log occurrences inside ensureWorkPushed with
appropriate logger calls or remove them if unnecessary.

In `@src/term-commands/team.ts`:
- Around line 259-264: The team name from CLI must be sanitized before being
used in tmux window operations: in resolveSpawnTeamWindow call
sanitizeWindowName (import it from ../genie-commands/session.js) and pass the
sanitized value instead of raw config.name to ensureTeamWindow (and any other
tmux-related calls that use the team string); update the import list to include
sanitizeWindowName and replace usages of the unsanitized team parameter in
resolveSpawnTeamWindow/ensureTeamWindow invocation so dots and other special
chars are escaped for tmux.

---

Outside diff comments:
In `@src/genie-commands/session.ts`:
- Around line 197-225: Rename outdated "continue" identifiers to "resume" for
consistency: change function launchWithContinueFallback to
launchWithResumeFallback, rename continueName to resumeName and continueCmd to
resumeCmd, and update any call sites and usages (e.g., where buildClaudeCommand
is invoked and tmux.executeTmux calls) to use these new names; ensure the logic
and parameters remain unchanged and update any console messages or comments
referencing "continue" to "resume".

In `@src/lib/tmux.ts`:
- Around line 48-71: The current getCurrentSessionName function uses substring
matching (s.name.includes(hint)) which can return unintended sessions; update
the hint matching to first look for an exact match (e.g., s.name === hint) and
return that if found, and only then fall back to substring matching
(s.name.includes(hint)) if no exact match exists; modify the logic inside
getCurrentSessionName where sessions.find is used so it tries exact match first,
then substring, ensuring correct session selection when a hint is provided.

In `@src/term-commands/agents.ts`:
- Around line 610-623: The layout is being applied to the current session
instead of the resolved override — update launchTmuxSpawn so it passes the
resolved target to applySpawnLayout rather than letting applySpawnLayout call
getCurrentSessionName(); after createTmuxPane(...) call use the resolved
teamWindow (or teamWindow.windowId/session) when invoking applySpawnLayout (and
if needed adjust applySpawnLayout's signature to accept a targetSession/windowId
and use that instead of getCurrentSessionName), referencing the functions
launchTmuxSpawn, createTmuxPane, applySpawnLayout and the teamWindow.windowId
value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 72c97576-35c0-4f1b-9308-b4d2086d610e

📥 Commits

Reviewing files that changed from the base of the PR and between ab2dc77 and a2cb615.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (42)
  • .claude-plugin/marketplace.json
  • openclaw.plugin.json
  • package.json
  • plugins/genie/.claude-plugin/plugin.json
  • plugins/genie/package.json
  • src/db/migrations/001_initial.sql
  • src/db/migrations/002_scheduler_extensions.sql
  • src/db/migrations/003_machine_snapshots.sql
  • src/genie-commands/__tests__/session.test.ts
  • src/genie-commands/session.ts
  • src/genie.ts
  • src/hooks/handlers/auto-spawn.ts
  • src/lib/cron.test.ts
  • src/lib/cron.ts
  • src/lib/db-migrations.ts
  • src/lib/db.ts
  • src/lib/protocol-router-spawn.ts
  • src/lib/protocol-router.ts
  • src/lib/provider-adapters.ts
  • src/lib/run-spec.test.ts
  • src/lib/run-spec.ts
  • src/lib/scheduler-daemon.test.ts
  • src/lib/scheduler-daemon.ts
  • src/lib/spawn-command.test.ts
  • src/lib/spawn-command.ts
  • src/lib/team-lead-command.ts
  • src/lib/team-manager.ts
  • src/lib/tmux.ts
  • src/lib/wish-state.test.ts
  • src/lib/wish-state.ts
  • src/term-commands/agents.ts
  • src/term-commands/daemon.test.ts
  • src/term-commands/daemon.ts
  • src/term-commands/db.ts
  • src/term-commands/dispatch.ts
  • src/term-commands/msg.test.ts
  • src/term-commands/schedule.test.ts
  • src/term-commands/schedule.ts
  • src/term-commands/state.test.ts
  • src/term-commands/state.ts
  • src/term-commands/team.ts
  • src/types/pgserve.d.ts

Comment on lines +7 to +22
CREATE TABLE schedules (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
cron_expression TEXT NOT NULL,
timezone TEXT DEFAULT 'UTC',
command TEXT,
metadata JSONB DEFAULT '{}',
status TEXT NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'paused', 'deleted')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_schedules_status ON schedules(status);
CREATE INDEX idx_schedules_name ON schedules(name);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/db/migrations/001_initial.sql

Repository: automagik-dev/genie

Length of output: 6048


🏁 Script executed:

cat -n src/term-commands/schedule.ts | head -100

Repository: automagik-dev/genie

Length of output: 3616


🏁 Script executed:

rg -A 5 "name-based\|by.*name\|WHERE.*name" src/term-commands/schedule.ts

Repository: automagik-dev/genie

Length of output: 45


🏁 Script executed:

rg "WHERE\|SELECT.*schedules\|cancel\|retry\|history" src/term-commands/schedule.ts -A 3 -B 1

Repository: automagik-dev/genie

Length of output: 45


🏁 Script executed:

rg "UPDATE.*schedules\|UPDATE.*triggers" src/term-commands/schedule.ts -A 3 -B 1

Repository: automagik-dev/genie

Length of output: 45


🏁 Script executed:

cat -n src/term-commands/schedule.ts | grep -A 50 "cancel\|retry\|history" | head -100

Repository: automagik-dev/genie

Length of output: 3511


🏁 Script executed:

cat -n src/term-commands/schedule.ts | sed -n '370,430p'

Repository: automagik-dev/genie

Length of output: 2353


🏁 Script executed:

rg "UPDATE triggers SET status" src/term-commands/schedule.ts -B 3 -A 3

Repository: automagik-dev/genie

Length of output: 253


🏁 Script executed:

cat -n src/term-commands/schedule.ts | sed -n '430,480p'

Repository: automagik-dev/genie

Length of output: 1804


🏁 Script executed:

rg "schedule create\|INSERT INTO schedules" src/term-commands/schedule.ts -B 5 -A 10

Repository: automagik-dev/genie

Length of output: 45


🏁 Script executed:

rg "async function scheduleCreateCommand\|INSERT INTO schedules" src/term-commands/schedule.ts -B 5 -A 15

Repository: automagik-dev/genie

Length of output: 45


🏁 Script executed:

cat -n src/term-commands/schedule.ts | sed -n '200,280p'

Repository: automagik-dev/genie

Length of output: 3464


Add partial unique index to enforce active schedule-name uniqueness in the database.

The application checks for duplicate active names before creating a schedule, but this check happens outside the transaction. Two concurrent creates can both pass the check and insert separate active rows with the same name, causing later cancel/history/retry calls by name to be ambiguous or operate on the wrong schedule. Add CREATE UNIQUE INDEX idx_schedules_active_name ON schedules(name) WHERE status = 'active' to enforce this at the schema level.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/migrations/001_initial.sql` around lines 7 - 22, Add a partial unique
index on schedules to prevent duplicate active names by creating a unique index
named idx_schedules_active_name on the name column where status = 'active'
(i.e., CREATE UNIQUE INDEX idx_schedules_active_name ON schedules(name) WHERE
status = 'active'); update the migration that defines the schedules table (table
name: schedules, existing indexes: idx_schedules_status and idx_schedules_name)
to include this new index and ensure the migration handles or documents any
pre-existing active-name duplicates before applying.

Comment on lines +119 to +131
CREATE OR REPLACE FUNCTION notify_trigger_due()
RETURNS trigger AS $$
BEGIN
PERFORM pg_notify('genie_trigger_due', NEW.id::text);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_notify_due
AFTER INSERT ON triggers
FOR EACH ROW
WHEN (NEW.status = 'pending')
EXECUTE FUNCTION notify_trigger_due();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "001_initial.sql" -o -name "*migrations*" -type d

Repository: automagik-dev/genie

Length of output: 118


🏁 Script executed:

wc -l ./src/db/migrations/001_initial.sql

Repository: automagik-dev/genie

Length of output: 102


🏁 Script executed:

cat -n ./src/db/migrations/001_initial.sql

Repository: automagik-dev/genie

Length of output: 6048


🏁 Script executed:

rg "status.*=.*['\"]pending['\"]|pending.*status" --type sql --type js --type ts -B 2 -A 2

Repository: automagik-dev/genie

Length of output: 2985


Add AFTER UPDATE trigger to notify when triggers transition back to pending.

trg_notify_due only fires on INSERT. The retry path in scheduler-daemon.ts (timeout recovery) and schedule.ts (manual re-queue) both update existing rows back to status = 'pending', but no notification is sent. The daemon must wait for the fallback poll instead of being notified immediately. Add an AFTER UPDATE branch that fires on transitions to pending status.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/migrations/001_initial.sql` around lines 119 - 131, The current
trigger trg_notify_due only fires AFTER INSERT; add an AFTER UPDATE branch that
calls the same notify_trigger_due() function when a row transitions back to
pending so updates that set status = 'pending' notify the daemon immediately.
Implement an UPDATE trigger on the triggers table with WHEN (OLD.status IS
DISTINCT FROM 'pending' AND NEW.status = 'pending') (or OLD.status <> 'pending'
AND NEW.status = 'pending') and EXECUTE FUNCTION notify_trigger_due(); keep the
existing INSERT trigger intact and reuse the notify_trigger_due() function and
trg_notify_due naming to group related behavior.

Comment on lines +3 to +15
CREATE TABLE IF NOT EXISTS machine_snapshots (
id TEXT PRIMARY KEY,
active_workers INTEGER NOT NULL DEFAULT 0,
active_teams INTEGER NOT NULL DEFAULT 0,
tmux_sessions INTEGER NOT NULL DEFAULT 0,
cpu_percent REAL,
memory_mb REAL,
context JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS idx_machine_snapshots_created
ON machine_snapshots(created_at);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

**Consider retention policy for snapshots.**Machine snapshots can accumulate without bounds. "Having partitions for different time spans makes it more efficient to drop/delete/expire old data." Consider adding a retention cleanup mechanism or documenting expected growth.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/migrations/003_machine_snapshots.sql` around lines 3 - 15, The
machine_snapshots table can grow unbounded; add a retention strategy for
machine_snapshots (e.g., time-based partitioning on created_at or a scheduled
cleanup) and implement a mechanism to expire old data: convert machine_snapshots
into range partitions by created_at (or create monthly/weekly partitions) and
add a scheduled job (pg_cron or a DB-side function + external scheduler) to drop
old partitions, or if partitioning is not desired add a cleanup function that
deletes rows older than a configurable retention period and schedule it to run
regularly; update related index idx_machine_snapshots_created and any queries
using created_at to work with the chosen approach and document the retention
configuration.

tmux_sessions INTEGER NOT NULL DEFAULT 0,
cpu_percent REAL,
memory_mb REAL,
context JSONB DEFAULT '{}',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

context column allows NULL despite having a DEFAULT.

The context column has DEFAULT '{}' but lacks NOT NULL, so explicit NULL inserts bypass the default. Other columns consistently use NOT NULL DEFAULT. If context should always be a valid JSONB object, add NOT NULL:

♻️ Add NOT NULL for consistency
-  context JSONB DEFAULT '{}',
+  context JSONB NOT NULL DEFAULT '{}',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
context JSONB DEFAULT '{}',
context JSONB NOT NULL DEFAULT '{}',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/migrations/003_machine_snapshots.sql` at line 10, The column
definition for context should be changed to enforce non-null JSONB values:
update the migration's column line from "context JSONB DEFAULT '{}'," to
"context JSONB NOT NULL DEFAULT '{}'," so explicit NULL inserts are rejected and
default applies consistently; update any corresponding schema/ORM model to match
if present.

Comment thread src/lib/cron.ts
Comment on lines +44 to +70
function expandRange(range: string, step: number, min: number, max: number): number[] {
if (range === '*') {
const out: number[] = [];
for (let i = min; i <= max; i += step) out.push(i);
return out;
}
if (range.includes('-')) {
const [start, end] = range.split('-').map(Number);
const out: number[] = [];
for (let i = start; i <= end; i += step) out.push(i);
return out;
}
return [Number.parseInt(range, 10)];
}

/**
* Parse a single cron field into a sorted array of valid integer values.
* Supports: wildcards (*), ranges (1-5), steps (star/5, 1-10/2), lists (1,3,5)
*/
function parseCronField(field: string, min: number, max: number): number[] {
const values = new Set<number>();

for (const part of field.split(',')) {
const stepMatch = part.match(/^(.+)\/(\d+)$/);
const step = stepMatch ? Number.parseInt(stepMatch[2], 10) : 1;
const range = stepMatch ? stepMatch[1] : part;
for (const v of expandRange(range, step, min, max)) values.add(v);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Validate step and numeric bounds during cron field expansion.

The parser currently allows */0 and out-of-range values, which can hang expansion loops or defer invalid input to a slow 366-day scan.

Proposed fix
 function expandRange(range: string, step: number, min: number, max: number): number[] {
+  if (!Number.isInteger(step) || step <= 0) {
+    throw new Error(`Invalid cron step "${step}". Step must be >= 1`);
+  }
+
   if (range === '*') {
     const out: number[] = [];
     for (let i = min; i <= max; i += step) out.push(i);
     return out;
   }
   if (range.includes('-')) {
     const [start, end] = range.split('-').map(Number);
+    if (!Number.isInteger(start) || !Number.isInteger(end) || start < min || end > max || start > end) {
+      throw new Error(`Invalid cron range "${range}". Expected ${min}-${max}`);
+    }
     const out: number[] = [];
     for (let i = start; i <= end; i += step) out.push(i);
     return out;
   }
-  return [Number.parseInt(range, 10)];
+  const value = Number.parseInt(range, 10);
+  if (!Number.isInteger(value) || value < min || value > max) {
+    throw new Error(`Invalid cron value "${range}". Expected ${min}-${max}`);
+  }
+  return [value];
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/cron.ts` around lines 44 - 70, The cron parser must validate step and
numeric bounds to avoid /0 and out-of-range loops: update expandRange(range,
step, min, max) to throw on invalid step (step <= 0) and on invalid numeric
ranges (non-number, start > end, or start/end outside [min,max]); also update
parseCronField(field, min, max) to validate parsed single numbers and step parts
(ensure parsed step > 0 and numeric values are within min..max) before calling
expandRange, so invalid inputs produce a clear error instead of hanging or
producing large scans; reference functions: expandRange and parseCronField.

Comment thread src/term-commands/db.ts
Comment on lines +100 to +114
for (const table of tables) {
const countResult = await sql.unsafe(`SELECT count(*) AS cnt FROM "${table.tablename}"`);
const count = countResult[0].cnt;
console.log(` ${padRight(table.tablename, maxNameLen)} ${count}`);
}
}

await shutdown();
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`\n Error querying database: ${message}`);
}

console.log('');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

shutdown() should be in a finally block to ensure cleanup on error.

If the row count query fails mid-loop, shutdown() won't be called, potentially leaving connections open.

🔧 Proposed fix
 async function dbStatusCommand(): Promise<void> {
   // ... port/dataDir checks ...

   try {
     const sql = await getConnection();
-
-    // ... queries ...
-
-    await shutdown();
+    try {
+      // ... queries ...
+    } finally {
+      await shutdown();
+    }
   } catch (err) {
     const message = err instanceof Error ? err.message : String(err);
     console.error(`\n  Error querying database: ${message}`);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/term-commands/db.ts` around lines 100 - 114, The shutdown() call must be
moved into a finally block so it always runs even if an error occurs; remove the
existing await shutdown() from inside the try, add a finally { await shutdown();
} after the catch (or convert the try/catch into try { ... } catch (err) { ... }
finally { await shutdown(); }), keeping the current error handling that builds
message from err and the console.log('') intact; reference the shutdown() symbol
and the try/catch that currently surrounds the loop that queries tables to
locate where to make the change.

Comment on lines +93 to +117
function computeFirstDueAt(options: CreateOptions): { dueAt: Date; cronExpr: string; scheduleType: string } {
if (options.at) {
const dueAt = parseAbsoluteTime(options.at);
if (dueAt.getTime() <= Date.now()) {
throw new Error(`Schedule time is in the past: ${options.at}`);
}
return { dueAt, cronExpr: '@once', scheduleType: 'once' };
}

if (options.after) {
const delayMs = parseDuration(options.after);
const dueAt = new Date(Date.now() + delayMs);
return { dueAt, cronExpr: '@once', scheduleType: 'once' };
}

if (options.every) {
if (isCronExpression(options.every)) {
// Store cron expression directly
const dueAt = computeNextCronDue(options.every);
return { dueAt, cronExpr: options.every, scheduleType: 'cron' };
}
// Parse as interval duration
const intervalMs = parseDuration(options.every);
const dueAt = new Date(Date.now() + intervalMs);
return { dueAt, cronExpr: `@every ${options.every.trim()}`, scheduleType: 'interval' };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

--timezone is stored but never used for the first due time.

computeFirstDueAt() never reads options.timezone; it parses --at with new Date(...) and computes cron due times with computeNextCronDue(options.every) only. The first trigger can therefore be scheduled in the wrong zone even though the row persists a timezone. Either use the timezone during initial due-time calculation or reject the flag until that path supports it.

Also applies to: 208-213

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/term-commands/schedule.ts` around lines 93 - 117, computeFirstDueAt
currently ignores options.timezone when calculating the initial dueAt (it calls
parseAbsoluteTime, parseDuration and computeNextCronDue without timezone), so
either incorporate the timezone into those calculations or reject the flag for
unsupported paths: update parseAbsoluteTime and computeNextCronDue (or call
timezone-aware variants) to accept options.timezone and use it when computing
dueAt for the 'at' and cron branches, and ensure the interval branch uses
timezone-consistent logic or throw an error if options.timezone is provided but
not supported; reference computeFirstDueAt, parseAbsoluteTime,
computeNextCronDue, isCronExpression and parseDuration when making the change.

Comment on lines +220 to +245
describe('ensureWorkPushed()', () => {
let repoDir: string;
let originalCwd: string;

beforeEach(async () => {
originalCwd = process.cwd();
repoDir = join('/tmp', `push-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
await mkdir(repoDir, { recursive: true });

// Initialize a git repo — ensureWorkPushed uses execSync which operates in process.cwd()
execSync('git init', { cwd: repoDir, encoding: 'utf-8' });
execSync('git config user.email "test@test.com"', { cwd: repoDir, encoding: 'utf-8' });
execSync('git config user.name "Test"', { cwd: repoDir, encoding: 'utf-8' });

// Create initial commit
await writeFile(join(repoDir, 'README.md'), '# Test');
execSync('git add -A && git commit -m "init"', { cwd: repoDir, encoding: 'utf-8' });

// ensureWorkPushed uses execSync with no cwd, so we must chdir
process.chdir(repoDir);
});

afterEach(async () => {
process.chdir(originalCwd);
await rm(repoDir, { recursive: true, force: true });
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider setting GENIE_HOME to isolate global state.

Per coding guidelines, tests should set process.env.GENIE_HOME to tmpdir to prevent test pollution of global state.

♻️ Proposed fix
 describe('ensureWorkPushed()', () => {
   let repoDir: string;
   let originalCwd: string;
+  let originalGenieHome: string | undefined;

   beforeEach(async () => {
     originalCwd = process.cwd();
+    originalGenieHome = process.env.GENIE_HOME;
     repoDir = join('/tmp', `push-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
     await mkdir(repoDir, { recursive: true });
+    process.env.GENIE_HOME = repoDir;
     // ... rest of setup
   });

   afterEach(async () => {
     process.chdir(originalCwd);
+    process.env.GENIE_HOME = originalGenieHome;
     await rm(repoDir, { recursive: true, force: true });
   });

As per coding guidelines: "Set process.env.GENIE_HOME to tmpdir in tests to isolate global state."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe('ensureWorkPushed()', () => {
let repoDir: string;
let originalCwd: string;
beforeEach(async () => {
originalCwd = process.cwd();
repoDir = join('/tmp', `push-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
await mkdir(repoDir, { recursive: true });
// Initialize a git repo — ensureWorkPushed uses execSync which operates in process.cwd()
execSync('git init', { cwd: repoDir, encoding: 'utf-8' });
execSync('git config user.email "test@test.com"', { cwd: repoDir, encoding: 'utf-8' });
execSync('git config user.name "Test"', { cwd: repoDir, encoding: 'utf-8' });
// Create initial commit
await writeFile(join(repoDir, 'README.md'), '# Test');
execSync('git add -A && git commit -m "init"', { cwd: repoDir, encoding: 'utf-8' });
// ensureWorkPushed uses execSync with no cwd, so we must chdir
process.chdir(repoDir);
});
afterEach(async () => {
process.chdir(originalCwd);
await rm(repoDir, { recursive: true, force: true });
});
describe('ensureWorkPushed()', () => {
let repoDir: string;
let originalCwd: string;
let originalGenieHome: string | undefined;
beforeEach(async () => {
originalCwd = process.cwd();
originalGenieHome = process.env.GENIE_HOME;
repoDir = join('/tmp', `push-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
await mkdir(repoDir, { recursive: true });
process.env.GENIE_HOME = repoDir;
// Initialize a git repo — ensureWorkPushed uses execSync which operates in process.cwd()
execSync('git init', { cwd: repoDir, encoding: 'utf-8' });
execSync('git config user.email "test@test.com"', { cwd: repoDir, encoding: 'utf-8' });
execSync('git config user.name "Test"', { cwd: repoDir, encoding: 'utf-8' });
// Create initial commit
await writeFile(join(repoDir, 'README.md'), '# Test');
execSync('git add -A && git commit -m "init"', { cwd: repoDir, encoding: 'utf-8' });
// ensureWorkPushed uses execSync with no cwd, so we must chdir
process.chdir(repoDir);
});
afterEach(async () => {
process.chdir(originalCwd);
process.env.GENIE_HOME = originalGenieHome;
await rm(repoDir, { recursive: true, force: true });
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/term-commands/state.test.ts` around lines 220 - 245, The test setup for
ensureWorkPushed() should isolate GENIE global state: in the beforeEach block
(where repoDir and originalCwd are set and process.chdir(repoDir) is called) set
process.env.GENIE_HOME to a temporary directory (e.g., using repoDir or a new
tmpdir) and save the original value; in the afterEach block restore the original
process.env.GENIE_HOME (alongside restoring process.cwd and removing repoDir) so
tests do not pollute global state.

Comment on lines +111 to +145
export async function ensureWorkPushed(slug: string, group: string): Promise<void> {
// 1. Commit dirty working tree as WIP
try {
const porcelain = execSync('git status --porcelain', { encoding: 'utf-8' }).trim();
if (porcelain) {
console.log(' Committing dirty working tree...');
execSync('git add -A', { encoding: 'utf-8' });
execSync(`git commit -m "wip: ${slug}#${group}"`, { encoding: 'utf-8' });
console.log(` Committed as "wip: ${slug}#${group}"`);
}
} catch {
// git status or commit failed — may not be in a git repo
}

// 2. Push unpushed commits
try {
const unpushed = execSync('git log @{u}..HEAD --oneline', { encoding: 'utf-8' }).trim();
if (unpushed) {
console.log(' Pushing unpushed commits...');
execSync('git push', { encoding: 'utf-8', timeout: 30000 });
console.log(' Push complete.');
}
} catch {
// No upstream tracking or push failed — best-effort
try {
// Try pushing with --set-upstream for new branches
const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
if (branch && branch !== 'HEAD') {
execSync(`git push -u origin ${branch}`, { encoding: 'utf-8', timeout: 30000 });
console.log(' Push complete (set upstream).');
}
} catch {
console.log(' ⚠️ Push failed — manual push may be needed.');
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Get the full file content to understand the context
wc -l src/term-commands/state.ts

Repository: automagik-dev/genie

Length of output: 93


🏁 Script executed:

# Read the file with line numbers to see the full context
cat -n src/term-commands/state.ts | head -200

Repository: automagik-dev/genie

Length of output: 8391


🏁 Script executed:

# Search for doneCommand to see how ensureWorkPushed is called
rg -A 30 'function doneCommand|export.*doneCommand' src/term-commands/state.ts

Repository: automagik-dev/genie

Length of output: 1262


Address git failures and remove console.log from source code.

The ensureWorkPushed() function silently swallows all git commit/push failures in empty catch blocks and returns Promise<void> with no error indication. This allows doneCommand() to proceed to wave completion detection, team-lead notification, and pane termination even when work was never pushed. The function should throw on failure to prevent the pane from being killed with unpushed commits.

Additionally, this file contains six console.log() calls (lines 116, 119, 129, 131, 140, 143) which violate the coding guideline prohibiting console.log in source code. Replace with proper logging mechanism or remove if informational only.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/term-commands/state.ts` around lines 111 - 145, The ensureWorkPushed
function currently swallows all git errors and uses console.log; update
ensureWorkPushed to surface failures by throwing a descriptive Error when commit
or push steps fail (so callers like doneCommand cannot continue on unpushed
work) and remove/replace all console.log calls with the project's logging API
(e.g., processLogger.debug/info/error) or drop purely informational messages;
specifically, ensure failures in the "git status/commit" block and in the "git
log.../git push" block propagate as thrown errors, and replace the six
console.log occurrences inside ensureWorkPushed with appropriate logger calls or
remove them if unnecessary.

Comment thread src/term-commands/team.ts
Comment on lines 259 to 264
await handleWorkerSpawn('team-lead', {
provider: 'claude',
team: config.name,
cwd: config.worktreePath,
initialPrompt: kickoffPrompt,
session: tmuxSession,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if sanitizeWindowName is applied to team names before tmux operations
rg -nP 'sanitizeWindowName|ensureTeamWindow' --type=ts -A3 -B3

Repository: automagik-dev/genie

Length of output: 13143


🏁 Script executed:

fd -e ts -e tsx | xargs rg -l 'handleWorkerSpawn' | head -20

Repository: automagik-dev/genie

Length of output: 159


🏁 Script executed:

rg -nP 'function handleWorkerSpawn|export.*handleWorkerSpawn|const handleWorkerSpawn' --type=ts -A5

Repository: automagik-dev/genie

Length of output: 627


🏁 Script executed:

cat -n src/term-commands/team.ts | sed -n '250,270p'

Repository: automagik-dev/genie

Length of output: 1400


🏁 Script executed:

sed -n '838,900p' src/term-commands/agents.ts

Repository: automagik-dev/genie

Length of output: 2342


🏁 Script executed:

rg -nP 'interface SpawnOptions|type SpawnOptions' --type=ts -A10

Repository: automagik-dev/genie

Length of output: 1945


🏁 Script executed:

rg -nP 'team.*auto.*spawn|spawnTeam' --type=ts -B2 -A5

Repository: automagik-dev/genie

Length of output: 3935


🏁 Script executed:

rg -nP 'function buildSpawnParams|export.*buildSpawnParams' --type=ts -A15

Repository: automagik-dev/genie

Length of output: 2370


🏁 Script executed:

rg -nP 'ensureTeamWindow' --type=ts -B5 -A5 | head -80

Repository: automagik-dev/genie

Length of output: 4788


🏁 Script executed:

sed -n '838,920p' src/term-commands/agents.ts

Repository: automagik-dev/genie

Length of output: 3022


🏁 Script executed:

rg -nP 'spawnAgentInTmux|executeSpawnCommand' --type=ts -A20 | head -100

Repository: automagik-dev/genie

Length of output: 45


🏁 Script executed:

rg -nP 'function launchTmuxSpawn|export.*launchTmuxSpawn' --type=ts -A30

Repository: automagik-dev/genie

Length of output: 2083


🏁 Script executed:

rg -nP 'ensureTeamWindow.*team' --type=ts -B3 -A3

Repository: automagik-dev/genie

Length of output: 1188


🏁 Script executed:

sed -n '545,565p' src/term-commands/agents.ts

Repository: automagik-dev/genie

Length of output: 746


🏁 Script executed:

rg -nP 'import.*sanitizeWindowName' --type=ts

Repository: automagik-dev/genie

Length of output: 298


🏁 Script executed:

sed -n '266,290p' src/lib/tmux.ts

Repository: automagik-dev/genie

Length of output: 1045


🏁 Script executed:

rg -nP 'findWindowByName' --type=ts -A5 | head -50

Repository: automagik-dev/genie

Length of output: 1817


Sanitize team name before passing to ensureTeamWindow at line 559.

Team names from user CLI input flow unsanitized through to tmux operations. Dots are misinterpreted as pane separators, causing "can't find pane" errors. Import sanitizeWindowName from ../genie-commands/session.js and apply it to the team parameter before calling ensureTeamWindow in resolveSpawnTeamWindow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/term-commands/team.ts` around lines 259 - 264, The team name from CLI
must be sanitized before being used in tmux window operations: in
resolveSpawnTeamWindow call sanitizeWindowName (import it from
../genie-commands/session.js) and pass the sanitized value instead of raw
config.name to ensureTeamWindow (and any other tmux-related calls that use the
team string); update the import list to include sanitizeWindowName and replace
usages of the unsanitized team parameter in
resolveSpawnTeamWindow/ensureTeamWindow invocation so dots and other special
chars are escaped for tmux.

@namastex888

Copy link
Copy Markdown
Contributor Author

Closing — wrong branch. Promotion should always be dev→main directly, not via intermediate branch. Recreating as dev→main with current dev state.

@namastex888
namastex888 deleted the qa-dev-to-main branch March 29, 2026 22:10
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