Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,24 +228,29 @@ 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
- Includes 60-second safety timer that runs alongside watching
- Sends notifications to agents with `ready` status that have unread mail
- Notifications sent via tmux: `tmux send-keys -t <window> "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:**

Expand All @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions internal/cli/mailman_stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion internal/cli/mailman_stop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"

"agentmail/internal/daemon"
Expand Down Expand Up @@ -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())
}

Expand Down
3 changes: 3 additions & 0 deletions specs/012-mailman-stop/contracts/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,14 @@ Failed to send stop signal: <error message>
### 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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions specs/012-mailman-stop/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
1 change: 1 addition & 0 deletions specs/012-mailman-stop/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions specs/012-mailman-stop/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions specs/012-mailman-stop/research.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion specs/012-mailman-stop/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <error>" 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: \<error\>" to stderr and exit with code 1.

**Daemon Stop File Detection:**

Expand Down
5 changes: 3 additions & 2 deletions specs/012-mailman-stop/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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: <error>" in internal/cli/mailman_stop.go
- [x] T018 [US2] Add generic filesystem error handling "Failed to send stop signal: \<error\>" 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
Expand Down Expand Up @@ -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)
Expand Down