diff --git a/CLAUDE.md b/CLAUDE.md index 7e598a7..9f43a23 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -78,7 +78,15 @@ This project uses speckit for feature specification and planning. Available comm Templates are stored in `.specify/templates/` and project constitution in `.specify/memory/constitution.md`. -**Important:** After implementing a spec, always update `README.md` to reflect the new functionality. +## Post-Implementation Checklist + +After completing any feature implementation or significant work: + +1. **Update README.md** - Ensure all new commands, options, and functionality are documented +2. **Update spec status** - Mark the spec as "Implemented" in the spec.md file +3. **Verify examples** - Ensure README examples still work with any changes + +This checklist ensures documentation stays in sync with the codebase. ## Active Technologies - Go 1.21+ (per IC-001) + Standard library only (os/exec for tmux, encoding/json for JSONL) (001-agent-mail-structure) diff --git a/README.md b/README.md index 31a0743..d219286 100644 --- a/README.md +++ b/README.md @@ -228,16 +228,21 @@ agentmail status offline ### mailman -Start the mailman daemon to monitor mailboxes and notify agents. +Start or stop the mailman daemon to monitor mailboxes and notify agents. ```bash agentmail mailman [--daemon] +agentmail mailman stop ``` **Flags:** - `--daemon` - Run in background (daemonize) +**Subcommands:** + +- `stop` - Stop the running daemon gracefully + **Behavior:** - Uses file watching (fsnotify) for instant notification on mailbox changes @@ -245,7 +250,7 @@ agentmail mailman [--daemon] - Sends notifications to agents with `ready` status that have unread mail - Notifications sent via tmux: `tmux send-keys -t "Check your agentmail"` - Stores PID in `.agentmail/mailman.pid` -- Gracefully shuts down on SIGTERM/SIGINT +- Gracefully shuts down on SIGTERM/SIGINT or when `stop` command is issued **Examples:** @@ -255,14 +260,22 @@ agentmail mailman # Run as background daemon agentmail mailman --daemon + +# Stop the running daemon +agentmail mailman stop ``` -**Exit codes:** +**Exit codes (start):** -- `0` - Daemon started/stopped successfully +- `0` - Daemon started successfully - `1` - Error (failed to start, PID file error, etc.) - `2` - Daemon already running +**Exit codes (stop):** + +- `0` - Stop signal sent successfully +- `1` - Error (stop already pending or filesystem error) + ### onboard Output AI-optimized onboarding context about AgentMail. diff --git a/internal/cli/mailman_stop.go b/internal/cli/mailman_stop.go index f6d9880..86181da 100644 --- a/internal/cli/mailman_stop.go +++ b/internal/cli/mailman_stop.go @@ -17,6 +17,10 @@ type MailmanStopOptions struct { // MailmanStop implements the agentmail mailman stop command. // Creates a .stop file to signal the daemon to shut down. // +// The function attempts to find the repository root via FindGitRoot, +// falling back to os.Getwd if not in a git repository. If both fail, +// repoRoot will be empty and file creation will fail with a clear error. +// // Exit codes: // - 0: Success (stop signal sent) // - 1: Error (file exists or filesystem error) diff --git a/internal/cli/mailman_stop_test.go b/internal/cli/mailman_stop_test.go index 6df4d6a..60cb096 100644 --- a/internal/cli/mailman_stop_test.go +++ b/internal/cli/mailman_stop_test.go @@ -4,6 +4,7 @@ import ( "bytes" "os" "path/filepath" + "strings" "testing" "agentmail/internal/daemon" @@ -157,7 +158,7 @@ func TestMailmanStop_FilesystemError_ReturnsError(t *testing.T) { // Verify error message contains expected prefix expectedPrefix := "Failed to send stop signal:" - if len(stderr.String()) < len(expectedPrefix) || stderr.String()[:len(expectedPrefix)] != expectedPrefix { + if !strings.HasPrefix(stderr.String(), expectedPrefix) { t.Errorf("Expected stderr to start with %q, got %q", expectedPrefix, stderr.String()) } diff --git a/specs/012-mailman-stop/contracts/cli.md b/specs/012-mailman-stop/contracts/cli.md index d76f281..91ff770 100644 --- a/specs/012-mailman-stop/contracts/cli.md +++ b/specs/012-mailman-stop/contracts/cli.md @@ -54,12 +54,14 @@ Failed to send stop signal: ### Fire-and-Forget Pattern The stop command creates a signal file and immediately exits with code 0. It does NOT: + - Wait for the daemon to terminate - Verify the daemon is running - Verify the daemon received the signal - Delete any files The daemon is responsible for: + - Detecting the `.stop` file via file watcher - Removing the `.stop` file - Removing the `.pid` file @@ -76,6 +78,7 @@ The stop mechanism uses file creation as inter-process communication: 5. Daemon initiates shutdown and cleans up files This approach: + - Requires no process validation - Works cross-platform - Uses existing file watcher infrastructure diff --git a/specs/012-mailman-stop/data-model.md b/specs/012-mailman-stop/data-model.md index 14c44d4..4cf35e6 100644 --- a/specs/012-mailman-stop/data-model.md +++ b/specs/012-mailman-stop/data-model.md @@ -19,6 +19,7 @@ This feature introduces a file-based signaling mechanism for stopping the daemon **Purpose**: Acts as an inter-process communication (IPC) mechanism between the CLI stop command and the running daemon. **Operations**: + | Operation | Actor | Trigger | |-----------|-------|---------| | Create | CLI (stop command) | User runs `agentmail mailman stop` | diff --git a/specs/012-mailman-stop/plan.md b/specs/012-mailman-stop/plan.md index 9e9b510..52751fd 100644 --- a/specs/012-mailman-stop/plan.md +++ b/specs/012-mailman-stop/plan.md @@ -31,6 +31,7 @@ Add `agentmail mailman stop` subcommand to gracefully terminate the mailman daem | IV. Standard Library | ✅ PASS | Uses only stdlib (os package for file operations) | **Quality Gates Required**: + 1. `gofmt -l .` - no output 2. `go mod verify` - pass 3. `go vet ./...` - pass diff --git a/specs/012-mailman-stop/quickstart.md b/specs/012-mailman-stop/quickstart.md index 5de6109..29917bf 100644 --- a/specs/012-mailman-stop/quickstart.md +++ b/specs/012-mailman-stop/quickstart.md @@ -135,11 +135,13 @@ mailmanCmd := &ffcli.Command{ ### Step 6: Write Tests #### internal/cli/mailman_stop_test.go + - Test success case (file created) - Test "stop already pending" (file exists) - Test filesystem error (permissions) #### internal/daemon/watcher_test.go (extend existing) + - Test stop file detection triggers shutdown ## Key Files to Modify diff --git a/specs/012-mailman-stop/research.md b/specs/012-mailman-stop/research.md index ecf2b1b..078dfbc 100644 --- a/specs/012-mailman-stop/research.md +++ b/specs/012-mailman-stop/research.md @@ -12,6 +12,7 @@ **Decision**: Create an empty `.stop` file in `.agentmail/` directory. Daemon detects via existing fsnotify watcher. **Rationale**: + - File creation is a simple, cross-platform IPC mechanism - The daemon already has fsnotify watching `.agentmail/` for mailbox changes - No need for Unix signals, process validation, or syscall dependencies @@ -33,12 +34,14 @@ **Decision**: Use `os.OpenFile` with `O_CREATE|O_EXCL` flags to atomically create the file only if it doesn't exist. **Rationale**: + - `O_EXCL` flag causes the open to fail if file exists - This is atomic at the filesystem level - No race conditions between check and create - Go's `os.OpenFile` supports this directly **Implementation**: + ```go // Atomic create - fails if file exists f, err := os.OpenFile(stopFilePath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) @@ -59,12 +62,14 @@ return "Stop signal sent" **Decision**: Extend the existing `FileWatcher` to detect CREATE events for `.stop` file in `.agentmail/` directory. **Rationale**: + - The daemon already watches `.agentmail/` and `mailboxes/` directories - fsnotify provides CREATE events for new files - No additional polling or infrastructure needed - Detection is nearly instant (< 100ms typically) **Implementation Notes**: + - The watcher's `Run()` function receives all events - Filter for `fsnotify.Create` events where filename is `.stop` - When detected, trigger graceful shutdown sequence @@ -74,6 +79,7 @@ return "Stop signal sent" **Question**: What's the correct order for daemon shutdown? **Decision**: Follow this sequence: + 1. Detect `.stop` file 2. Remove `.stop` file (acknowledge receipt) 3. Close file watcher (stops notification loop) @@ -82,11 +88,13 @@ return "Stop signal sent" 6. Exit with code 0 **Rationale**: + - Removing `.stop` first prevents stale signal files - Closing watcher before PID removal ensures clean state - Matches existing signal-based shutdown sequence in `runForeground()` **Existing Code Reference** (`internal/daemon/daemon.go:249-271`): + ```go // Wait for shutdown signal or test stop <-sigChan diff --git a/specs/012-mailman-stop/spec.md b/specs/012-mailman-stop/spec.md index eb9b4d2..9348951 100644 --- a/specs/012-mailman-stop/spec.md +++ b/specs/012-mailman-stop/spec.md @@ -53,7 +53,7 @@ An agent operator runs the stop command when a previous stop is already pending - **FR-001**: When `agentmail mailman stop` is invoked, the CLI shall attempt to create the file `.agentmail/.stop`. - **FR-002**: When the `.stop` file is created successfully, the CLI shall output "Stop signal sent" to stdout and exit with code 0. - **FR-003**: If the `.stop` file already exists, then the CLI shall output "Stop already pending" to stderr and exit with code 1. -- **FR-004**: If the `.stop` file cannot be created due to a filesystem error, then the CLI shall output "Failed to send stop signal: " to stderr and exit with code 1. +- **FR-004**: If the `.stop` file cannot be created due to a filesystem error, then the CLI shall output "Failed to send stop signal: \" to stderr and exit with code 1. **Daemon Stop File Detection:** diff --git a/specs/012-mailman-stop/tasks.md b/specs/012-mailman-stop/tasks.md index aa31d67..ba7d1f3 100644 --- a/specs/012-mailman-stop/tasks.md +++ b/specs/012-mailman-stop/tasks.md @@ -16,6 +16,7 @@ ## Path Conventions Based on plan.md structure (Go CLI project): + - `cmd/agentmail/main.go` - CLI entry point - `internal/daemon/` - Daemon package (existing + modifications) - `internal/cli/` - CLI handlers (existing + new) @@ -75,7 +76,7 @@ Based on plan.md structure (Go CLI project): ### Implementation for User Story 2 - [x] T017 [US2] Add os.IsExist error handling for "Stop already pending" message in internal/cli/mailman_stop.go -- [x] T018 [US2] Add generic filesystem error handling "Failed to send stop signal: " in internal/cli/mailman_stop.go +- [x] T018 [US2] Add generic filesystem error handling "Failed to send stop signal: \" in internal/cli/mailman_stop.go - [x] T019 [US2] Run tests to verify US2: `go test -v ./internal/cli/... -run MailmanStop` **Checkpoint**: Both user stories complete - full stop functionality with error handling @@ -103,7 +104,7 @@ Based on plan.md structure (Go CLI project): ### Phase Dependencies -``` +```text Phase 1 (Setup) → No dependencies Phase 2 (US1) → Depends on Phase 1 Phase 3 (US2) → Depends on Phase 2 (shares MailmanStop function)