Skip to content
Open
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,8 @@ Each task has a default cooldown interval to prevent the same task from running

## Development

New to the codebase? Start with the [architecture overview](docs/architecture.md) — a map of the Go packages and how a run flows through them.

### Pre-commit hooks

Install the git pre-commit hook to catch formatting and vet issues before pushing:
Expand Down
88 changes: 88 additions & 0 deletions cmd/nightshift/commands/commit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package commands

import (
"fmt"
"io"
"os"

"github.com/marcus/nightshift/internal/commits"
"github.com/spf13/cobra"
)

var commitCmd = &cobra.Command{
Use: "commit",
Short: "Conventional Commits helpers",
Long: `Tools for working with Conventional Commits messages.

Use "commit normalize" to validate and reformat a commit message so it
follows the project's rules (type prefix, lowercase type, subject length,
and wrapped body).`,
}

var commitNormalizeCmd = &cobra.Command{
Use: "normalize [MESSAGE]",
Short: "Normalize a commit message to Conventional Commits format",
Long: `Validate and rewrite a commit message into canonical Conventional
Commits form.

The message is read from a positional argument, from a file passed via
--file (typically .git/COMMIT_EDITMSG by a commit-msg hook), or from stdin
when no argument and no --file are given.

nightshift commit normalize "feat: add login"
nightshift commit normalize --file .git/COMMIT_EDITMSG
git log -1 --pretty=%B | nightshift commit normalize

Use --check to only validate without rewriting; the exit code is non-zero
when the message does not conform.`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
check, _ := cmd.Flags().GetBool("check")
file, _ := cmd.Flags().GetString("file")

raw, err := readCommitMessage(args, file)
if err != nil {
return err
}

normalized, err := commits.Normalize(raw)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
return err
}

if check {
fmt.Fprintln(os.Stdout, normalized)
return nil
}
fmt.Fprintln(os.Stdout, normalized)
return nil
},
}

func init() {
commitNormalizeCmd.Flags().BoolP("check", "c", false, "Only validate; do not rewrite")
commitNormalizeCmd.Flags().StringP("file", "f", "", "Read the message from this file (use by the commit-msg hook)")
commitCmd.AddCommand(commitNormalizeCmd)
rootCmd.AddCommand(commitCmd)
}

// readCommitMessage resolves the message source in order: positional arg,
// --file, then stdin.
func readCommitMessage(args []string, file string) (string, error) {
if len(args) == 1 {
return args[0], nil
}
if file != "" {
b, err := os.ReadFile(file)
if err != nil {
return "", fmt.Errorf("read %s: %w", file, err)
}
return string(b), nil
}
b, err := io.ReadAll(os.Stdin)
if err != nil {
return "", fmt.Errorf("read stdin: %w", err)
}
return string(b), nil
}
8 changes: 8 additions & 0 deletions cmd/provider-calibration/main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
// Package main implements the provider-calibration utility, a standalone
// diagnostic tool that analyzes local Claude and Codex session data to
// summarize per-provider token usage distributions (primary vs. alternate
// tokens, turns per session) and derive cross-provider budget ratios.
//
// The emitted statistics inform the budget calibration heuristics used by
// the nightshift daemon. See docs/guides/provider-calibration.md for usage
// details and interpretation of the output.
package main

import (
Expand Down
93 changes: 93 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Architecture

This document maps Nightshift's Go packages and shows how a run flows through
them. For the step-by-step run lifecycle (including where logs and reports are
written), see [guides/run-lifecycle.md](guides/run-lifecycle.md).

## Overview

Nightshift is a CLI + daemon that runs AI coding agents (Claude Code, Codex,
Copilot) on maintenance tasks across your projects, on a schedule, within
token budgets. The codebase is a single Go module:

```
cmd/ executable entry points
nightshift/ the main CLI binary
commands/ cobra command definitions (one file per command)
provider-calibration/ standalone calibration analysis tool
internal/ all library packages (not importable externally)
website/ documentation site (Docusaurus)
docs/ long-form guides and reference docs
```

## Command Layer

| Package | Purpose |
|---------|---------|
| `cmd/nightshift` | CLI entry point: wires config, logging, and database into the commands. |
| `cmd/nightshift/commands` | Cobra command definitions — `run`, `preview`, `task`, `budget`, `commit`, `busfactor`, `config`, `init`, `doctor`, `daemon`, `install`/`uninstall`, `logs`, `report`, `status`, `stats`, `setup`. Thin layer: parses flags, then delegates to `internal/*`. |
| `cmd/provider-calibration` | Offline tool that summarizes per-provider token usage distributions from local session data; informs budget calibration heuristics. See [guides/provider-calibration.md](guides/provider-calibration.md). |

## Core Runtime

| Package | Purpose |
|---------|---------|
| `internal/config` | Loads, merges (global + project), and validates configuration. |
| `internal/scheduler` | Time-based job scheduling (cron or interval) used by the daemon. |
| `internal/orchestrator` | Coordinates agents working on tasks — the plan → implement → review loop. |
| `internal/agents` | Interfaces and implementations for spawning AI agent processes. |
| `internal/providers` | Provider abstraction and selection (Claude, Codex, Copilot): preference order, availability, and per-provider settings. |
| `internal/tasks` | Task registry, selection, priority scoring, and cooldown intervals. |
| `internal/projects` | Multi-project discovery, resolution, and per-project budget allocation. |
| `internal/tmux` | Scrapes tmux sessions to detect running agent processes and their usage output. |
| `internal/logging` | Structured logging with file rotation. |

## Budget & Usage Data

| Package | Purpose |
|---------|---------|
| `internal/budget` | Token budget calculation, allowance, and enforcement. |
| `internal/snapshots` | Collects and stores periodic usage snapshots from provider data files. |
| `internal/calibrator` | Tunes task budgets and scheduling from historical usage. |
| `internal/trends` | Analyzes snapshot history to surface usage patterns and anomalies. |
| `internal/stats` | Aggregate statistics over past runs (backs `nightshift stats`). |

## Persistence & Output

| Package | Purpose |
|---------|---------|
| `internal/db` | SQLite-backed storage for state and snapshots. |
| `internal/state` | Persistent run state (what ran, when, cooldowns). |
| `internal/reporting` | Run reports and morning summaries (backs `nightshift report`). |
| `internal/security` | Audit logging for nightshift operations. |
| `internal/setup` | Interactive onboarding (backs `nightshift setup`). |
| `internal/integrations` | Readers for external configuration and task sources. |

## Analysis & Tooling

| Package | Purpose |
|---------|---------|
| `internal/analysis` | Code ownership and bus-factor analysis (backs `nightshift busfactor`). |
| `internal/commits` | Conventional Commits normalization (backs `nightshift commit normalize`). |

## How a Run Flows

1. **Trigger** — cron/launchd/systemd executes `nightshift run`, or the daemon
(`internal/scheduler`) fires a scheduled tick.
2. **Setup** — `cmd/nightshift` loads config (`internal/config`) and initializes
logging (`internal/logging`), then opens the database (`internal/db`) and
loads run state (`internal/state`).
3. **Budget & provider** — `internal/budget` calculates the remaining
allowance from `internal/snapshots` data; `internal/providers` picks a
provider by preference and budget.
4. **Selection** — `internal/projects` resolves target projects;
`internal/tasks` scores and selects eligible tasks (respecting cooldowns).
5. **Execution** — `internal/orchestrator` drives the agent
(`internal/agents`) through plan → implement → review, inside tmux
(`internal/tmux`) when scraping is needed.
6. **Recording** — task and project results are written to the database;
`internal/reporting` saves the run report and (optionally) the morning
summary. `nightshift status`, `report`, and `stats` read this data back.

The sequence diagram in [guides/run-lifecycle.md](guides/run-lifecycle.md)
traces the same flow end to end.
47 changes: 47 additions & 0 deletions docs/commit-messages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Commit Messages

Nightshift uses [Conventional Commits](https://www.conventionalcommits.org/)
for all commit messages. This keeps the history readable and lets tooling
derive changelogs automatically.

## Format

```
<type>(<scope>): <subject>

<body>
```

- **type** — one of `feat`, `fix`, `docs`, `style`, `refactor`, `test`,
`chore`, `perf`, `build`, `ci`.
- **scope** — optional, e.g. `fix(api): ...`.
- **subject** — lowercase, imperative mood, no trailing period, max 72 chars.
- **body** — optional, wrapped at 72 columns, separated from the subject by a
blank line.

## The `commit normalize` command

Validate and reformat a message:

```sh
nightshift commit normalize "feat: add login screen"
nightshift commit normalize --file .git/COMMIT_EDITMSG
git log -1 --pretty=%B | nightshift commit normalize
```

Add `--check` to validate only. The command exits non-zero when a message
cannot be normalized (missing/unknown type, capitalized or overlong subject).

## commit-msg hook

To enforce the rules locally, install the hook:

```sh
make install-hooks
# or manually:
ln -sf ../../scripts/commit-msg.sh .git/hooks/commit-msg
```

The hook normalizes your message file in place before the commit is created and
rejects messages that cannot be fixed automatically. Bypass it with
`git commit --no-verify`.
Loading