You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add optional scheduled_at and due_at DateTime fields to issues, enabling task scheduling workflows. scheduled_at marks when an issue becomes actionable; due_at is the hard deadline. The crosslink next command filters out not-yet-scheduled issues, boosts overdue issues by +100, and prints a warning when a deadline is within 1 day. Both fields propagate through the full event-sourced pipeline: CLI flags, events, shared writer, compaction, materialization, hydration, and SQLite.
Requirements
REQ-1: Add scheduled_at: Option<DateTime<Utc>> and due_at: Option<DateTime<Utc>> fields to the Issue data model across all representation layers (Issue struct, IssueFile, CompactIssue, HydratedIssue, SQLite schema).
REQ-2: Add a SchedulingUpdated T2 event variant to the Event enum for post-creation scheduling changes, following the same pattern as StatusChanged (always overwrites both fields).
REQ-3: Add optional scheduled_at and due_at fields to the IssueCreated event variant so scheduling can be set atomically at creation time (matching the existing pattern where IssueCreated carries initial field values like labels).
REQ-4: Add --scheduled and --due CLI flags to create, quick, and update commands, accepting YYYY-MM-DD or full RFC 3339 datetime input.
REQ-5: Add --no-scheduled and --no-due CLI flags to update for clearing previously-set dates.
REQ-6: crosslink next must filter out issues whose scheduled_at is in the future (not yet actionable).
REQ-7: crosslink next must boost overdue issues (due_at < now) by +100 in the scoring algorithm.
REQ-8: crosslink next must print a warning alongside any recommended issue whose due_at is within 1 day.
REQ-9: crosslink show must display Scheduled and Due fields when set, formatted as YYYY-MM-DD.
REQ-10: Full backward compatibility: existing issues without scheduling dates, existing event logs without scheduled_at/due_at fields, and existing checkpoint state must continue to work via #[serde(default)].
REQ-11: Date parsing convention: YYYY-MM-DD input for --scheduled is parsed to T00:00:00Z (start of day); YYYY-MM-DD input for --due is parsed to T23:59:59Z (end of day). Full RFC 3339 datetime input (e.g. 2026-03-20T14:00:00Z) is parsed directly, bypassing the start/end-of-day convention.
REQ-12: Subissue constraint: if --scheduled or --due is provided together with --parent, the CLI must reject with an error: "Scheduling dates apply to parent issues, not subissues." Scheduling fields are a property of the deliverable (parent issue), not the implementation breakdown (subissues).
Acceptance Criteria
AC-1: crosslink create "title" --scheduled 2026-03-20 --due 2026-03-25 creates an issue with scheduled_at stored as 2026-03-20T00:00:00Z and due_at stored as 2026-03-25T23:59:59Z. (REQ-3, REQ-4, REQ-11)
AC-2: crosslink quick "title" -p high --due 2026-03-25 creates an issue with due date set and scheduled_at as None. (REQ-3, REQ-4, REQ-11)
AC-3: crosslink update <id> --scheduled 2026-03-20 sets scheduled_at without changing due_at. A SchedulingUpdated event is emitted carrying both fields (current due_at preserved via read-before-write). (REQ-2, REQ-4)
AC-4: crosslink update <id> --no-due clears due_at to None. Passing --due X and --no-due simultaneously is rejected by clap conflicts_with. (REQ-5)
AC-5: crosslink show <id> on an issue with scheduled_at = 2026-03-20T00:00:00Z and due_at = 2026-03-25T23:59:59Z prints Scheduled: 2026-03-20 and Due: 2026-03-25. (REQ-9)
AC-6: crosslink show <id> on an issue with no scheduling dates does not print Scheduled/Due lines. (REQ-9)
AC-7: crosslink next with an issue whose scheduled_at is tomorrow does not include that issue in the results. (REQ-6)
AC-8: crosslink next with a medium overdue issue and a medium non-overdue issue ranks the overdue issue higher (score 300 vs 200). (REQ-7)
AC-9: crosslink next with an issue due in 6 hours prints a warning line like Due in 6 hours alongside the recommendation. (REQ-8)
AC-10: A SchedulingUpdated event written to the event log is correctly processed by compaction: the CompactIssue state reflects the new dates, and materialized issue.json contains the fields. (REQ-2)
AC-11: Scheduling dates survive a full round-trip: create with dates, compact, materialize to JSON, hydrate to SQLite, query via db.get_issue() — all dates match. (REQ-1, REQ-10)
AC-12: Deserializing an existing IssueCreated event (without scheduled_at/due_at) succeeds with both fields defaulting to None. Existing checkpoint state without these fields deserializes correctly. (REQ-10)
AC-13: If both --scheduled and --due are provided and scheduled > due, the CLI prints a warning (not an error) before proceeding. (REQ-4, REQ-11)
AC-14: crosslink create "subtask" --parent 1 --due 2026-03-25 is rejected with an error message stating that scheduling dates apply to parent issues, not subissues. (REQ-12)
AC-15: crosslink quick "subtask" --parent 1 --scheduled 2026-03-20 is rejected with the same error. (REQ-12)
AC-16: crosslink next with an issue that has no scheduled_at and no due_at includes that issue in results (dateless issues are always eligible). (REQ-6, REQ-10)
AC-17: crosslink next with a high priority issue (no dates) and a medium priority issue (with scheduled_at in the past, due_at in the future, not overdue) recommends the high issue (scheduling dates alone do not override priority). (REQ-7, REQ-10)
AC-18: crosslink next with a dateless medium priority issue does not receive the +100 overdue boost (only issues with due_at < now get boosted). (REQ-7, REQ-10)
AC-19: An issue created before the feature (no scheduled_at/due_at columns in its original SQLite row or JSON) displays correctly in crosslink show with no Scheduled/Due lines and no errors, and appears normally in crosslink next. (REQ-10)
AC-20: crosslink create "title" --due 2026-03-20T14:00:00Z stores due_at as 2026-03-20T14:00:00Z exactly, not adjusted to T23:59:59Z. Full RFC 3339 input bypasses the start/end-of-day convention. (REQ-11)
Architecture
Date Type and Input Parsing
The fields use DateTime<Utc> to match the codebase's existing timestamp pattern (models.rs:12-14, issue_file.rs:26-29). A custom clap value_parser function accepts:
ISO 8601 date YYYY-MM-DD — parsed via NaiveDate::parse_from_str then converted to DateTime<Utc>. For --scheduled: T00:00:00Z (start of day). For --due: T23:59:59Z (end of day).
Full RFC 3339 datetime (e.g. 2026-03-20T14:00:00Z) — parsed directly, bypassing the start/end-of-day convention. This allows precise scheduling when needed.
Display in show uses %Y-%m-%d format since the time component is typically not meaningful for scheduling.
Layer 1: Event System — crosslink/src/events.rs
IssueCreated (lines 56-67) gains two optional fields with #[serde(default)] for backward compatibility:
SchedulingUpdated is a new T2 (causal) variant (#14). It follows the StatusChanged pattern: always carries the full new state of both fields. None means "not set / cleared". The CLI reads the current issue state before emitting, so unchanged fields are preserved.
insert_hydrated_issue (lines 1422-1429): add scheduled_at and due_at to the INSERT statement.
create_issue_with_parent (lines 407-436): add optional scheduled_at and due_at parameters, included in INSERT if provided.
update_issue (lines 541-595): add optional scheduled_at and due_at parameters. When provided, dynamically append to the SET clause (same pattern as title/description/priority).
get_issue and query methods: update SELECT statements and parse_issue_row() to read the two new columns.
Layer 7: Hydration — crosslink/src/hydration.rs
In the hydration loop (around line 161), convert IssueFile.scheduled_at and due_at to RFC 3339 strings and pass through HydratedIssue into insert_hydrated_issue.
create_issue (lines 366-434): accept optional scheduled_at and due_at parameters. Include in the IssueFile JSON and in the IssueCreated event fields.
New method update_scheduling: reads the current issue file, merges the requested changes (set or clear each field), writes back, emits a SchedulingUpdated event, compacts, and pushes. Follows the update_issue pattern.
Layer 9: CLI — crosslink/src/main.rs
Create (lines 433-457) and Quick (lines 460-478) structs gain:
Two parser functions: parse_scheduled_date converts YYYY-MM-DD to T00:00:00Z (start of day); parse_due_date converts to T23:59:59Z (end of day). Both fall back to DateTime::parse_from_rfc3339 for full datetime input. Both return clear error messages on failure.
Update's "at least one field required" validation (in commands/update.rs:16) is extended to also accept scheduling flags.
Layer 10: Commands
commands/create.rs: passes scheduled and due from CLI args through to shared_writer.create_issue(). If --parent is set alongside --scheduled or --due, reject with error before reaching the writer. If both scheduling flags are set and scheduled > due, prints a warning to stderr.
commands/update.rs: determines new scheduling state by reading the current issue, applying the CLI flags (set / clear / leave unchanged), then calling shared_writer.update_scheduling().
commands/show.rs (lines 54-58): after the existing Updated: line, conditionally prints:
Scheduled: 2026-03-20
Due: 2026-03-25
commands/next.rs (lines 48-157):
After the subissue and lock filters (line 76), add: if issue.scheduled_at is Some(dt) and dt > Utc::now(), skip (continue). For subissues (where issue.parent_id.is_some()), look up the parent's scheduled_at via db.get_issue(parent_id) and apply the same filter.
In the scoring block (line 87), add: if issue.due_at is Some(dt) and dt < Utc::now(), add +100 to score. For subissues, look up the parent's due_at and apply the same boost.
In the output block (lines 114-135), after the description preview: if issue.due_at (or parent's due_at for subissues) is Some(dt) and dt - Utc::now() <= Duration::days(1) and dt > Utc::now(), print a warning line with the time remaining (e.g., Due in 6 hours).
Note: crosslink next currently skips subissues entirely (line 67), so the parent lookup only matters if that filter is relaxed in the future. The implementation should still handle it correctly for forward compatibility.
The SchedulingUpdated event "always carries the full new state of both fields" where None means "cleared." Two options for the serde annotations:
Option A (current draft): #[serde(default, skip_serializing_if = "Option::is_none")] — matches the IssueCreated pattern. None fields are omitted from JSON; serde(default) restores them as None on deserialize. Round-trip is correct but the omission makes "intentionally cleared" visually indistinguishable from "field not present" in the event log.
Option B: No skip_serializing_if, just bare Option<DateTime<Utc>> — None serializes as explicit null. The event log shows {"scheduled_at": null, "due_at": "2026-03-25T23:59:59Z"}, making the intent visible. serde(default) is also unnecessary since both fields are always present in new events.
Note: this only affects SchedulingUpdated. The IssueCreated annotations (#[serde(default, skip_serializing_if = "Option::is_none")]) are correct as-is for backward compatibility with existing events.
To resolve: Developer decision during implementation. Edit this section with the chosen approach and remove the <!-- OPEN --> marker.
Out of Scope
Filtering issue list by scheduled/due dates (list always shows all matching issues regardless of scheduling)
Recurring / repeating schedule patterns
Timezone-aware scheduling (all dates are UTC; scheduled at start-of-day, due at end-of-day)
Calendar integration or external notification systems
Relative date input (e.g. +3d, tomorrow) — only ISO 8601 date strings accepted
Subissue-level scheduling dates: scheduling fields are a property of the deliverable (parent issue), not the implementation breakdown (subissues). Subissues inherit their parent's dates implicitly via parent lookup in crosslink next. No separate date fields on subissues.
Summary
Add optional
scheduled_atanddue_atDateTime fields to issues, enabling task scheduling workflows.scheduled_atmarks when an issue becomes actionable;due_atis the hard deadline. Thecrosslink nextcommand filters out not-yet-scheduled issues, boosts overdue issues by +100, and prints a warning when a deadline is within 1 day. Both fields propagate through the full event-sourced pipeline: CLI flags, events, shared writer, compaction, materialization, hydration, and SQLite.Requirements
scheduled_at: Option<DateTime<Utc>>anddue_at: Option<DateTime<Utc>>fields to the Issue data model across all representation layers (Issue struct, IssueFile, CompactIssue, HydratedIssue, SQLite schema).SchedulingUpdatedT2 event variant to the Event enum for post-creation scheduling changes, following the same pattern asStatusChanged(always overwrites both fields).scheduled_atanddue_atfields to theIssueCreatedevent variant so scheduling can be set atomically at creation time (matching the existing pattern whereIssueCreatedcarries initial field values likelabels).--scheduledand--dueCLI flags tocreate,quick, andupdatecommands, acceptingYYYY-MM-DDor full RFC 3339 datetime input.--no-scheduledand--no-dueCLI flags toupdatefor clearing previously-set dates.crosslink nextmust filter out issues whosescheduled_atis in the future (not yet actionable).crosslink nextmust boost overdue issues (due_at < now) by +100 in the scoring algorithm.crosslink nextmust print a warning alongside any recommended issue whosedue_atis within 1 day.crosslink showmust displayScheduledandDuefields when set, formatted asYYYY-MM-DD.scheduled_at/due_atfields, and existing checkpoint state must continue to work via#[serde(default)].YYYY-MM-DDinput for--scheduledis parsed toT00:00:00Z(start of day);YYYY-MM-DDinput for--dueis parsed toT23:59:59Z(end of day). Full RFC 3339 datetime input (e.g.2026-03-20T14:00:00Z) is parsed directly, bypassing the start/end-of-day convention.--scheduledor--dueis provided together with--parent, the CLI must reject with an error: "Scheduling dates apply to parent issues, not subissues." Scheduling fields are a property of the deliverable (parent issue), not the implementation breakdown (subissues).Acceptance Criteria
crosslink create "title" --scheduled 2026-03-20 --due 2026-03-25creates an issue withscheduled_atstored as2026-03-20T00:00:00Zanddue_atstored as2026-03-25T23:59:59Z. (REQ-3, REQ-4, REQ-11)crosslink quick "title" -p high --due 2026-03-25creates an issue with due date set and scheduled_at as None. (REQ-3, REQ-4, REQ-11)crosslink update <id> --scheduled 2026-03-20sets scheduled_at without changing due_at. ASchedulingUpdatedevent is emitted carrying both fields (current due_at preserved via read-before-write). (REQ-2, REQ-4)crosslink update <id> --no-dueclears due_at to None. Passing--due Xand--no-duesimultaneously is rejected by clapconflicts_with. (REQ-5)crosslink show <id>on an issue withscheduled_at = 2026-03-20T00:00:00Zanddue_at = 2026-03-25T23:59:59ZprintsScheduled: 2026-03-20andDue: 2026-03-25. (REQ-9)crosslink show <id>on an issue with no scheduling dates does not print Scheduled/Due lines. (REQ-9)crosslink nextwith an issue whosescheduled_atis tomorrow does not include that issue in the results. (REQ-6)crosslink nextwith amediumoverdue issue and amediumnon-overdue issue ranks the overdue issue higher (score 300 vs 200). (REQ-7)crosslink nextwith an issue due in 6 hours prints a warning line likeDue in 6 hoursalongside the recommendation. (REQ-8)SchedulingUpdatedevent written to the event log is correctly processed by compaction: theCompactIssuestate reflects the new dates, and materializedissue.jsoncontains the fields. (REQ-2)db.get_issue()— all dates match. (REQ-1, REQ-10)IssueCreatedevent (withoutscheduled_at/due_at) succeeds with both fields defaulting to None. Existing checkpoint state without these fields deserializes correctly. (REQ-10)--scheduledand--dueare provided andscheduled > due, the CLI prints a warning (not an error) before proceeding. (REQ-4, REQ-11)crosslink create "subtask" --parent 1 --due 2026-03-25is rejected with an error message stating that scheduling dates apply to parent issues, not subissues. (REQ-12)crosslink quick "subtask" --parent 1 --scheduled 2026-03-20is rejected with the same error. (REQ-12)crosslink nextwith an issue that has noscheduled_atand nodue_atincludes that issue in results (dateless issues are always eligible). (REQ-6, REQ-10)crosslink nextwith ahighpriority issue (no dates) and amediumpriority issue (withscheduled_atin the past,due_atin the future, not overdue) recommends thehighissue (scheduling dates alone do not override priority). (REQ-7, REQ-10)crosslink nextwith a datelessmediumpriority issue does not receive the +100 overdue boost (only issues withdue_at < nowget boosted). (REQ-7, REQ-10)scheduled_at/due_atcolumns in its original SQLite row or JSON) displays correctly incrosslink showwith no Scheduled/Due lines and no errors, and appears normally incrosslink next. (REQ-10)crosslink create "title" --due 2026-03-20T14:00:00Zstoresdue_atas2026-03-20T14:00:00Zexactly, not adjusted toT23:59:59Z. Full RFC 3339 input bypasses the start/end-of-day convention. (REQ-11)Architecture
Date Type and Input Parsing
The fields use
DateTime<Utc>to match the codebase's existing timestamp pattern (models.rs:12-14,issue_file.rs:26-29). A custom clapvalue_parserfunction accepts:YYYY-MM-DD— parsed viaNaiveDate::parse_from_strthen converted toDateTime<Utc>. For--scheduled:T00:00:00Z(start of day). For--due:T23:59:59Z(end of day).2026-03-20T14:00:00Z) — parsed directly, bypassing the start/end-of-day convention. This allows precise scheduling when needed.Display in
showuses%Y-%m-%dformat since the time component is typically not meaningful for scheduling.Layer 1: Event System —
crosslink/src/events.rsIssueCreated (lines 56-67) gains two optional fields with
#[serde(default)]for backward compatibility:SchedulingUpdated is a new T2 (causal) variant (#14). It follows the
StatusChangedpattern: always carries the full new state of both fields.Nonemeans "not set / cleared". The CLI reads the current issue state before emitting, so unchanged fields are preserved.Layer 2: Checkpoint State —
crosslink/src/checkpoint.rsCompactIssue (lines 63-87) gains:
#[serde(default)]ensures existing checkpoint files without these fields deserialize correctly.Layer 3: Compaction —
crosslink/src/compaction.rsThe
apply()function (lines 313+) gains:In the
IssueCreatedarm (line 336): setscheduled_atanddue_atfrom the event fields when constructing theCompactIssue.New arm for
SchedulingUpdated:The
compact_to_issue_file()function (lines 566-586) propagates the two new fields toIssueFile.Layer 4: Issue File —
crosslink/src/issue_file.rsIssueFile (lines 13-45) gains:
Layer 5: Models —
crosslink/src/models.rsIssue struct (lines 5-15) gains:
Layer 6: Database —
crosslink/src/db.rsSchema migration (bump
SCHEMA_VERSIONfrom 15 to 16): add two nullable TEXT columns to theissuestable:HydratedIssue (lines 81-93) gains:
insert_hydrated_issue (lines 1422-1429): add
scheduled_atanddue_atto the INSERT statement.create_issue_with_parent (lines 407-436): add optional
scheduled_atanddue_atparameters, included in INSERT if provided.update_issue (lines 541-595): add optional
scheduled_atanddue_atparameters. When provided, dynamically append to the SET clause (same pattern as title/description/priority).get_issue and query methods: update SELECT statements and
parse_issue_row()to read the two new columns.Layer 7: Hydration —
crosslink/src/hydration.rsIn the hydration loop (around line 161), convert
IssueFile.scheduled_atanddue_atto RFC 3339 strings and pass throughHydratedIssueintoinsert_hydrated_issue.Layer 8: Shared Writer —
crosslink/src/shared_writer.rscreate_issue (lines 366-434): accept optional
scheduled_atanddue_atparameters. Include in theIssueFileJSON and in theIssueCreatedevent fields.New method
update_scheduling: reads the current issue file, merges the requested changes (set or clear each field), writes back, emits aSchedulingUpdatedevent, compacts, and pushes. Follows theupdate_issuepattern.Layer 9: CLI —
crosslink/src/main.rsCreate (lines 433-457) and Quick (lines 460-478) structs gain:
Update (lines 507-520) gains:
Two parser functions:
parse_scheduled_dateconvertsYYYY-MM-DDtoT00:00:00Z(start of day);parse_due_dateconverts toT23:59:59Z(end of day). Both fall back toDateTime::parse_from_rfc3339for full datetime input. Both return clear error messages on failure.Update's "at least one field required" validation (in
commands/update.rs:16) is extended to also accept scheduling flags.Layer 10: Commands
commands/create.rs: passes
scheduledandduefrom CLI args through toshared_writer.create_issue(). If--parentis set alongside--scheduledor--due, reject with error before reaching the writer. If both scheduling flags are set andscheduled > due, prints a warning to stderr.commands/update.rs: determines new scheduling state by reading the current issue, applying the CLI flags (set / clear / leave unchanged), then calling
shared_writer.update_scheduling().commands/show.rs (lines 54-58): after the existing
Updated:line, conditionally prints:commands/next.rs (lines 48-157):
issue.scheduled_atisSome(dt)anddt > Utc::now(), skip (continue). For subissues (whereissue.parent_id.is_some()), look up the parent'sscheduled_atviadb.get_issue(parent_id)and apply the same filter.issue.due_atisSome(dt)anddt < Utc::now(), add +100 to score. For subissues, look up the parent'sdue_atand apply the same boost.issue.due_at(or parent'sdue_atfor subissues) isSome(dt)anddt - Utc::now() <= Duration::days(1)anddt > Utc::now(), print a warning line with the time remaining (e.g.,Due in 6 hours).Note:
crosslink nextcurrently skips subissues entirely (line 67), so the parent lookup only matters if that filter is relaxed in the future. The implementation should still handle it correctly for forward compatibility.Data Flow Summary
Open Questions
Q1: Serde annotations on SchedulingUpdated fields
The
SchedulingUpdatedevent "always carries the full new state of both fields" whereNonemeans "cleared." Two options for the serde annotations:Option A (current draft):
#[serde(default, skip_serializing_if = "Option::is_none")]— matches theIssueCreatedpattern.Nonefields are omitted from JSON;serde(default)restores them asNoneon deserialize. Round-trip is correct but the omission makes "intentionally cleared" visually indistinguishable from "field not present" in the event log.Option B: No
skip_serializing_if, just bareOption<DateTime<Utc>>—Noneserializes as explicitnull. The event log shows{"scheduled_at": null, "due_at": "2026-03-25T23:59:59Z"}, making the intent visible.serde(default)is also unnecessary since both fields are always present in new events.Note: this only affects
SchedulingUpdated. TheIssueCreatedannotations (#[serde(default, skip_serializing_if = "Option::is_none")]) are correct as-is for backward compatibility with existing events.To resolve: Developer decision during implementation. Edit this section with the chosen approach and remove the
<!-- OPEN -->marker.Out of Scope
issue listby scheduled/due dates (list always shows all matching issues regardless of scheduling)+3d,tomorrow) — only ISO 8601 date strings acceptedcrosslink next. No separate date fields on subissues.