From b574ada41f8fd446fb9b2928b1d07f1fcba1a67b Mon Sep 17 00:00:00 2001 From: Marcus Vorwaller Date: Wed, 22 Apr 2026 02:19:52 -0700 Subject: [PATCH 1/2] docs: draft v0.3.4 release notes Nightshift-Task: release-notes Nightshift-Ref: https://github.com/marcus/nightshift --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59afa7d..cb536f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 From 9be9346221f435621e65eabe98a88f0fc2fdaacc Mon Sep 17 00:00:00 2001 From: Marcus Vorwaller Date: Sun, 17 May 2026 03:09:08 -0700 Subject: [PATCH 2/2] fix(scheduler): make Schedule() actually fire job only once Schedule(at, job) was documented as a one-time job, but the closure it added to the scheduler ran on every tick after `at`. Wrap the call in sync.Once so the job fires exactly once. Use `!time.Now().Before(at)` so the boundary tick (exactly at `at`) also fires. Nightshift-Task: bug-finder Nightshift-Ref: https://github.com/marcus/nightshift Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/scheduler/scheduler.go | 9 ++++--- internal/scheduler/scheduler_test.go | 39 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go index 278ca71..470717e 100644 --- a/internal/scheduler/scheduler.go +++ b/internal/scheduler/scheduler.go @@ -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 }) diff --git a/internal/scheduler/scheduler_test.go b/internal/scheduler/scheduler_test.go index e28a714..a3cf530 100644 --- a/internal/scheduler/scheduler_test.go +++ b/internal/scheduler/scheduler_test.go @@ -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) + } +}