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
16 changes: 16 additions & 0 deletions .gitmessage
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Commit format:
# <type>(<optional-scope>): <imperative summary>
#
# Examples:
# fix(tasks): standardize commit prompt handling
# docs: add commit message guide
#
# Allowed types:
# build, chore, ci, docs, feat, fix, perf, refactor, release, revert, style, test
#
# Keep the subject under 72 characters and do not end it with a period.
# Add a body only when it helps explain why or captures tradeoffs.
#
# When Nightshift creates the change, include both trailers:
# Nightshift-Task: <task-id>
# Nightshift-Ref: https://github.com/marcus/nightshift
9 changes: 6 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,13 @@ help:
@echo " check - Run tests and lint"
@echo " install - Build and install to Go bin directory"
@echo " calibrate-providers - Compare local Claude/Codex session usage for calibration"
@echo " install-hooks - Install git pre-commit hook"
@echo " install-hooks - Install git hooks and commit template"
@echo " help - Show this help"

# Install git pre-commit hook
# Install git hooks and commit template
install-hooks:
@ln -sf ../../scripts/pre-commit.sh .git/hooks/pre-commit
@echo "✓ pre-commit hook installed (.git/hooks/pre-commit → scripts/pre-commit.sh)"
@ln -sf ../../scripts/commit-msg.sh .git/hooks/commit-msg
@git config commit.template "$$(pwd)/.gitmessage"
@echo "✓ hooks installed (.git/hooks/pre-commit, .git/hooks/commit-msg)"
@echo "✓ commit template configured (.gitmessage)"
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,18 +258,33 @@ Each task has a default cooldown interval to prevent the same task from running

## Development

### Pre-commit hooks
### Git Hooks And Commit Template

Install the git pre-commit hook to catch formatting and vet issues before pushing:
Install the local git hooks and commit template:

```bash
make install-hooks
```

This symlinks `scripts/pre-commit.sh` into `.git/hooks/pre-commit`. The hook runs:
This installs:

- `scripts/pre-commit.sh` into `.git/hooks/pre-commit`
- `scripts/commit-msg.sh` into `.git/hooks/commit-msg`
- `.gitmessage` as the local `commit.template`

The commit-message standard for this repo is:

```text
<type>(<optional-scope>): <imperative summary>
```

See `docs/guides/commit-messages.md` for examples and the Nightshift trailer rules.

The hooks run:
- **gofmt** — flags any staged `.go` files that need formatting
- **go vet** — catches common correctness issues
- **go build** — ensures the project compiles
- **commit-msg** — validates conventional-style subjects and Nightshift trailers

To bypass in a pinch: `git commit --no-verify`

Expand Down
9 changes: 2 additions & 7 deletions cmd/nightshift/commands/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,13 +369,8 @@ func runScheduledTasks(ctx context.Context, cfg *config.Config, database *db.DB,
projectTaskTypes = append(projectTaskTypes, string(scoredTask.Definition.Type))

// Create task instance
taskInstance := &tasks.Task{
ID: fmt.Sprintf("%s:%s", scoredTask.Definition.Type, projectPath),
Title: scoredTask.Definition.Name,
Description: scoredTask.Definition.Description,
Priority: int(scoredTask.Score),
Type: scoredTask.Definition.Type,
}
taskInstance := taskInstanceFromDef(scoredTask.Definition, projectPath)
taskInstance.Priority = int(scoredTask.Score)

// Mark as assigned
st.MarkAssigned(taskInstance.ID, projectPath, string(scoredTask.Definition.Type))
Expand Down
9 changes: 2 additions & 7 deletions cmd/nightshift/commands/preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,13 +359,8 @@ func buildPreviewResult(cfg *config.Config, database *db.DB, projects []string,
}
projectResult.Tasks = make([]previewTask, 0, len(selected))
for idx, scored := range selected {
taskInstance := &tasks.Task{
ID: fmt.Sprintf("%s:%s", scored.Definition.Type, project),
Title: scored.Definition.Name,
Description: scored.Definition.Description,
Priority: int(scored.Score),
Type: scored.Definition.Type,
}
taskInstance := taskInstanceFromDef(scored.Definition, project)
taskInstance.Priority = int(scored.Score)
prompt := orch.PlanPrompt(taskInstance)
minTokens, maxTokens := scored.Definition.EstimatedTokens()

Expand Down
9 changes: 2 additions & 7 deletions cmd/nightshift/commands/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -717,13 +717,8 @@ func executeRun(ctx context.Context, p executeRunParams) error {
projectTaskTypes = append(projectTaskTypes, string(scoredTask.Definition.Type))

// Create task instance
taskInstance := &tasks.Task{
ID: fmt.Sprintf("%s:%s", scoredTask.Definition.Type, projectPath),
Title: scoredTask.Definition.Name,
Description: scoredTask.Definition.Description,
Priority: int(scoredTask.Score),
Type: scoredTask.Definition.Type,
}
taskInstance := taskInstanceFromDef(scoredTask.Definition, projectPath)
taskInstance.Priority = int(scoredTask.Score)

// Mark as assigned
p.st.MarkAssigned(taskInstance.ID, projectPath, string(scoredTask.Definition.Type))
Expand Down
11 changes: 6 additions & 5 deletions cmd/nightshift/commands/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,11 +307,12 @@ func taskInstanceFromDef(def tasks.TaskDefinition, projectPath string) *tasks.Ta
id = fmt.Sprintf("%s:%s", def.Type, projectPath)
}
return &tasks.Task{
ID: id,
Title: def.Name,
Description: def.Description,
Priority: 0,
Type: def.Type,
ID: id,
Title: def.Name,
Description: def.Description,
AgentInstructions: def.PromptText(),
Priority: 0,
Type: def.Type,
}
}

Expand Down
26 changes: 23 additions & 3 deletions cmd/nightshift/commands/task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,10 @@ func TestCategoryShort(t *testing.T) {

func TestTaskInstanceFromDef(t *testing.T) {
def := tasks.TaskDefinition{
Type: "lint-fix",
Name: "Linter Fixes",
Description: "Fix lint errors",
Type: "lint-fix",
Name: "Linter Fixes",
Description: "Fix lint errors",
AgentInstructions: "Inspect lint output and apply safe fixes",
}

// Without project path
Expand All @@ -156,6 +157,12 @@ func TestTaskInstanceFromDef(t *testing.T) {
if task.Title != "Linter Fixes" {
t.Errorf("Title = %q", task.Title)
}
if task.Description != def.Description {
t.Errorf("Description = %q, want %q", task.Description, def.Description)
}
if task.AgentInstructions != def.AgentInstructions {
t.Errorf("AgentInstructions = %q, want %q", task.AgentInstructions, def.AgentInstructions)
}

// With project path
task = taskInstanceFromDef(def, "/tmp/proj")
Expand All @@ -164,6 +171,19 @@ func TestTaskInstanceFromDef(t *testing.T) {
}
}

func TestTaskInstanceFromDefFallsBackToDescription(t *testing.T) {
def := tasks.TaskDefinition{
Type: "docs-backfill",
Name: "Docs",
Description: "Generate missing documentation",
}

task := taskInstanceFromDef(def, "")
if task.AgentInstructions != def.Description {
t.Errorf("AgentInstructions = %q, want fallback %q", task.AgentInstructions, def.Description)
}
}

func TestAgentByNameUnknown(t *testing.T) {
_, err := agentByName(nil, "unknown-provider")
if err == nil {
Expand Down
20 changes: 16 additions & 4 deletions docs/guides/adding-tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,25 @@ TaskMyNewTask: {
Type: TaskMyNewTask,
Category: CategoryPR, // See categories below
Name: "My New Task",
Description: "What the agent should do, written as instructions",
Description: "Short summary shown in task lists",
AgentInstructions: `Optional richer instructions for the agent.
Use this when the CLI summary should stay concise but the prompt
needs more concrete scope or guardrails.`,
CostTier: CostMedium, // Estimated token usage
RiskLevel: RiskLow, // Risk of unintended changes
DefaultInterval: 72 * time.Hour, // Minimum time between runs per project
},
```

The `Description` field is what the agent sees as its task instructions. Write it as a clear directive.
Use `Description` for the concise user-facing summary shown in task lists, previews, and JSON metadata. Add `AgentInstructions` when the agent needs richer directions; if it is omitted, Nightshift falls back to `Description`.

`commit-normalize` is the reference example for this split: it stays a short "Standardize commit message format" entry in the catalog, while the agent prompt makes the scope explicit:

- treat it as a PR task for future commit-message consistency
- inspect existing repo conventions before choosing a format
- avoid rewriting published history
- preserve Nightshift's required commit trailers
- prefer small, explainable enforcement or documentation changes

### Step 3: Update the Completeness Test

Expand Down Expand Up @@ -115,7 +126,7 @@ tasks:

### How It Works

Custom tasks register into the same task registry as built-in tasks. They participate in the same scoring, cooldown, budget filtering, and plan-implement-review orchestration cycle. The `description` field becomes the agent's task prompt.
Custom tasks register into the same task registry as built-in tasks. They participate in the same scoring, cooldown, budget filtering, and plan-implement-review orchestration cycle. For custom tasks, the `description` field still becomes the agent's task prompt.

Custom tasks appear in `nightshift task list` with a `[custom]` label and in JSON output with a `"custom": true` field.

Expand All @@ -134,7 +145,8 @@ tasks:

### Tips

- Write descriptions as direct instructions to the agent — they become the task prompt
- For built-in tasks, keep `Description` short and add `AgentInstructions` only when the agent needs extra guardrails
- For custom tasks, write `description` as direct instructions to the agent — it becomes the task prompt
- Use `nightshift preview --explain` to verify custom tasks appear in the run plan
- Run `nightshift run --task my-custom-type --dry-run` to test a specific custom task
- Custom task types must not match any built-in task name
48 changes: 48 additions & 0 deletions docs/guides/commit-messages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Commit Message Guide

Nightshift now standardizes future commits around a small conventional format that matches most recent history:

```text
<type>(<optional-scope>): <imperative summary>
```

Examples:

- `fix(tasks): standardize commit message template`
- `feat(run): add preflight summary display`
- `docs: add commit message guide`

## Rules

- Use one of: `build`, `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `release`, `revert`, `style`, `test`
- Keep the subject under 72 characters
- Use an imperative summary
- Do not end the subject with a period
- Add a body only when it adds useful context

## Nightshift Trailers

When a change is made by Nightshift or one of its agents, include both trailers together:

```text
Nightshift-Task: <task-id>
Nightshift-Ref: https://github.com/marcus/nightshift
```

Leave a blank line before the trailers.

## Local Setup

Run:

```bash
make install-hooks
```

This installs:

- `.git/hooks/pre-commit` for formatting, vet, and build checks
- `.git/hooks/commit-msg` for commit subject and Nightshift trailer validation
- `.gitmessage` as the local commit template for this repository

The hook is intentionally forward-looking. It does not rewrite or validate old history.
6 changes: 3 additions & 3 deletions internal/orchestrator/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -741,7 +741,7 @@ Description: %s
"files": ["file1.go", "file2.go", ...],
"description": "overall approach"
}
`, task.ID, task.Title, task.Description, branchInstruction, task.Type)
`, task.ID, task.Title, task.PromptText(), branchInstruction, task.Type)
}

func (o *Orchestrator) buildImplementPrompt(task *tasks.Task, plan *PlanOutput, iteration int) string {
Expand Down Expand Up @@ -783,7 +783,7 @@ Description: %s
"files_modified": ["file1.go", ...],
"summary": "what was done"
}
`, task.ID, task.Title, task.Description, plan.Description, plan.Steps, iterationNote, branchInstruction, task.Type)
`, task.ID, task.Title, task.PromptText(), plan.Description, plan.Steps, iterationNote, branchInstruction, task.Type)
}

func (o *Orchestrator) buildReviewPrompt(task *tasks.Task, impl *ImplementOutput) string {
Expand Down Expand Up @@ -814,7 +814,7 @@ Description: %s
}

Set "passed" to true ONLY if the implementation is correct and complete.
`, task.ID, task.Title, task.Description, impl.Summary, impl.FilesModified)
`, task.ID, task.Title, task.PromptText(), impl.Summary, impl.FilesModified)
}

// prURLPattern matches standard GitHub pull request URLs.
Expand Down
32 changes: 32 additions & 0 deletions internal/orchestrator/orchestrator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,38 @@ func TestBuildPrompts(t *testing.T) {
}
}

func TestBuildPromptsUseAgentInstructions(t *testing.T) {
o := New()
task := &tasks.Task{
ID: "prompt-test",
Title: "Build Prompts",
Description: "Short summary",
AgentInstructions: "Detailed agent-only instructions",
}

plan := &PlanOutput{
Steps: []string{"step1"},
Description: "test plan",
}
impl := &ImplementOutput{
FilesModified: []string{"file1.go"},
Summary: "test implementation",
}

for name, prompt := range map[string]string{
"plan": o.buildPlanPrompt(task),
"implement": o.buildImplementPrompt(task, plan, 1),
"review": o.buildReviewPrompt(task, impl),
} {
if !strings.Contains(prompt, task.AgentInstructions) {
t.Errorf("%s prompt missing agent instructions\nGot:\n%s", name, prompt)
}
if strings.Contains(prompt, "Description: "+task.Description) {
t.Errorf("%s prompt should use agent instructions instead of short summary\nGot:\n%s", name, prompt)
}
}
}

func TestExtractPRURL(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading
Loading