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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@

All notable changes to nightshift are documented in this file.

## [v0.3.4] - 2026-02-28

### Features
- **Configurable agent timeouts** — add `--timeout` to `nightshift run` and `nightshift daemon`, with daemon re-exec forwarding the flag (#27)
- **Expanded PII scanner guidance** — add detailed instructions for detecting hardcoded PII, leaked env files, unsafe storage, and related exposure patterns in the built-in task (#34)

### Fixes
- **Timeout handling and diagnostics** — preserve partial output on timeout, terminate full process groups, and surface partial logs from failed plan/implement/review steps (#33)
- **Copilot CLI integration** — improve binary resolution, permission gating, and CLI flag handling for Copilot runs (#39)
- **Provider config YAML serialization** — write provider settings with the correct YAML keys during setup (#43)
- **Configured run limits and budget fallback** — honor `schedule.max_projects` and `schedule.max_tasks`, improve budget calibration at day and week boundaries, and preserve Codex fallback permissions in headless runs (#42)

### Other
- **Task reference docs** — add a comprehensive reference page for all 59 built-in tasks and refresh related task documentation (#30)
- **Docs cleanup** — remove auto-generated implementation docs from the repository (#40)
- **Low-risk cleanup** — resolve Copilot helper lint warnings and replace `WriteString(fmt.Sprintf(...))` patterns with `fmt.Fprintf` in reporting and setup code (#38, #41)

## [v0.3.3] - 2026-02-19

### Features
Expand Down
9 changes: 6 additions & 3 deletions internal/scheduler/scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -473,11 +473,14 @@ func (s *Scheduler) ScheduleInterval(d time.Duration, job func()) error {
return nil
}

// Schedule adds a one-time job to run at the specified time.
// Schedule adds a one-time job to run at or after the specified time.
// The job fires exactly once on the first scheduler tick at or after `at`;
// subsequent ticks are no-ops.
func (s *Scheduler) Schedule(at time.Time, job func()) {
var once sync.Once
s.AddJob(func(ctx context.Context) error {
if time.Now().After(at) {
job()
if !time.Now().Before(at) {
once.Do(job)
}
return nil
})
Expand Down
39 changes: 39 additions & 0 deletions internal/scheduler/scheduler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -579,3 +579,42 @@ func TestScheduleInterval_Legacy(t *testing.T) {
t.Errorf("len(jobs) = %d, want 1", len(s.jobs))
}
}

func TestSchedule_RunsExactlyOnce(t *testing.T) {
s := New()
ctx := context.Background()

var count int32
// Schedule for a time already in the past so the job is eligible.
s.Schedule(time.Now().Add(-time.Minute), func() {
atomic.AddInt32(&count, 1)
})

// Simulate multiple scheduler ticks; the job must only run once.
for i := 0; i < 5; i++ {
s.runJobs(ctx)
}

if got := atomic.LoadInt32(&count); got != 1 {
t.Errorf("Schedule one-time job ran %d times, want 1", got)
}
}

func TestSchedule_NotBeforeTargetTime(t *testing.T) {
s := New()
ctx := context.Background()

var count int32
// Schedule for a time in the future; the job must not fire yet.
s.Schedule(time.Now().Add(time.Hour), func() {
atomic.AddInt32(&count, 1)
})

for i := 0; i < 3; i++ {
s.runJobs(ctx)
}

if got := atomic.LoadInt32(&count); got != 0 {
t.Errorf("Schedule job ran %d times before target, want 0", got)
}
}
Loading