Skip to content

feat: horizon policy dates every created issue, and linear goals (PHNX-4291) - #62

Merged
muqsitnawaz merged 2 commits into
mainfrom
agents/linear-cli-goals-v4
Oct 9, 2026
Merged

muqsitnawaz merged 2 commits into
mainfrom
agents/linear-cli-goals-v4

Conversation

@muqsitnawaz

Copy link
Copy Markdown
Contributor

Refs PHNX-4291. linear create now gives every issue a due date, cycle and milestone by rule. A new linear goals command reads and writes the company, week and day goals that agi-menu consumes.

Why

Over the last 60 days, 3% of tickets had a due date and 5% had a milestone. create put every issue in the active cycle and left the date empty.

Build 1: horizon policy in create (single and --from-file)

  • One horizon per issue (plan_horizon, a pure function covering the rule table):
    • now (default): due date by priority from policy.sla. Urgent and high are due today, with reminders at +15m and +1h. Medium is due the next working day, low at the active cycle's last day. Active cycle, and the project's next open milestone.
    • milestone (--milestone M): due on M's target date (urgent and high keep an earlier SLA date). Active cycle only when that date falls inside it.
    • backlog (--backlog): Backlog state, no cycle, no due date. The milestone must be a future one, defaulting to policy.backlogMilestone ("Q1 2027"). If that doesn't resolve, create refuses.
  • Explicit flags win: --due-date, --cycle and --status. --cycle no longer defaults to active.
  • Project: --project, else resolve_cwd_project (the linear tasks auto-scope path). With neither, create refuses and lists the projects.
  • Milestone: --milestone, else the earliest-target milestone under 100% (never the backlog one). If there's none, create refuses with the catalog.
  • Errors teach. Both refusals list the three horizons in three lines. The bulk form stays on one TSV-safe line.
  • One stderr line names every applied default.
  • --bug adds label Bug and implies backlog plus low priority. --bug --blocker implies horizon now, high priority, and the next milestone.
  • Delegate: cfg["agent"] becomes the delegate when --delegate isn't passed. The API-key owner stays the assignee.
  • --skip-milestone is the escape hatch and prints a warning.
  • policy block in ~/.linear-cli/config.json, merged per key over the defaults. Documented in the README under "Horizons".
  • Dates use the team's timezone (team.timezone). The fleet boxes run in UTC, and Linear's cycle endsAt is the exclusive start of the next cycle, so the "last day" is the day before that instant in the team's zone.
  • milestones set-target-date moves every open issue in the milestone that was due on the old target to the new one (cascade_due_dates), and prints the count.

Build 2: linear goals

  • linear goals --json follows the shared contract: company.initiative, logoUrl, milestones, week.cycle, week.goal, me and team. Goal objects carry their to-dos.
    • goals_state makes four requests and build_goals is pure.
    • week.goal is null for an empty name or one matching ^Cycle \d+$.
    • team is the other active, non-app, non-guest users.
  • Human view (no --json): company goal with days left, the week goal, my week, my day with to-dos, then each teammate's day and week goal.
  • Write commands, each with --json:
    • goals set week|day "<text>" [--milestone M] creates the goal or updates its title. A day goal is parented to the week goal.
    • goals set company-week "<text>" renames the active cycle.
    • goals todo "<text>" creates a sub-issue of today's goal, due today.
    • goals todo --done <ID> marks one done.
    • The default milestone is the Active initiative's next open milestone.
  • Key-result milestones: Linear has no field for an initiative's key results, so company.milestones is the open milestones (under 100%) of the initiative's projects with a target on or before the initiative's. Today that's the three named key results plus "0.1.4 — Agent Profiles" (Oct 30, 82%).

Verification

python3 -m unittest: Ran 265 tests, OK (skipped=4). The same suite passes under Python 3.9 through uv run --python 3.9. New test classes:

  • SlaTableTest: the default table, config overrides, +Nwd skipping a weekend, cycle last day in America/Los_Angeles.
  • PlanHorizonTest: each horizon, urgent/high keeping an earlier date, explicit due winning.
  • CreateHorizonTest: now, milestone, backlog, bug, bug+blocker, no project, no open milestone, explicit flags, delegate, a bad policy rule.
  • MilestoneRetargetCascadeTest.
  • GoalsJsonTest: the contract shape, startDate fallback, unnamed cycle, newest duplicate, human output.

Real runs against the getrush workspace using the branch's linear. Every throwaway (PHNX-4296 to 4306) was cancelled afterwards, and the throwaway milestone was deleted.

$ linear create "throwaway …: now urgent" --project Rush --priority urgent
horizon now · urgent → due 2026-10-08, reminder 23:10 · milestone: 0.1.0 — Interactive cloud run (next for Rush) · cycle: Launch gate green on all three harnesses · delegate: claude
Created PHNX-4296: …  [Launch gate green on all three harnesses | Muqsit | delegate: Claude]
PHNX-4296 -> reminder: 2026-10-09T06:10:34.970817Z

$ linear create "throwaway …: milestone" --project Rush --milestone "Ready to charge"
horizon milestone · milestone target → due 2026-10-30 · cycle: none · delegate: claude

$ linear create "throwaway …: backlog" --project Rush --backlog
horizon backlog · no due date · milestone: Q1 2027 (backlog) · cycle: none · delegate: claude

$ linear create "throwaway …: bug" --project Rush --bug
horizon backlog · no due date · bug → priority low · milestone: Q1 2027 (backlog) · cycle: none · delegate: claude

$ linear create "throwaway …: bug blocker" --project Rush --bug --blocker
horizon now · high → due 2026-10-08, reminder 23:56 · bug → priority high · milestone: 0.1.0 — Interactive cloud run (next for Rush) · cycle: Launch gate green on all three harnesses · delegate: claude

$ cd ~/src/…/agents-cli && linear create "throwaway …: cwd" --priority high
horizon now · high → due 2026-10-08, reminder 23:56 · project: AGI (cwd) · milestone: Q4 2026 (next for AGI) · cycle: Launch gate green on all three harnesses · delegate: claude

$ cd /tmp && linear create "throwaway …: no project"      # exit 1
Refusing to create an issue without a project: 'throwaway …: no project'
  Pass --project <name>, or run from a repo `agents projects` binds to one.

  Every issue lands in one horizon:
    now (default): due by priority (urgent/high today, medium next working day, low at cycle end), active cycle, the project's next open milestone
    --milestone M: due on M's target date (urgent/high keep an earlier SLA date), active cycle only when that date falls inside it
    --backlog: Backlog state, no cycle, no due date, a future milestone (default 'Q1 2027')

  Projects:
    Rush
    AGI
    …

What Linear stored, read back with GraphQL:

PHNX-4299 Low    due null        Backlog  cycle null  Q1 2027          labels [Bug]  assignee Muqsit  delegate Claude
PHNX-4298 Medium due null        Backlog  cycle null  Q1 2027
PHNX-4297 Medium due 2026-10-30  Todo     cycle null  Ready to charge
PHNX-4296 Urgent due 2026-10-08  Todo     cycle 32    0.1.0 — Interactive cloud run

Cascade, run on a throwaway milestone:

$ linear milestones create --project Rush --name "throwaway PHNX-4291 cascade" --target 2026-12-01
$ linear create "throwaway …: cascade" --project Rush --milestone "throwaway PHNX-4291 cascade"
horizon milestone · milestone target → due 2026-12-01 · cycle: none · delegate: claude
$ linear milestones set-target-date "throwaway PHNX-4291 cascade" 2026-12-08 --project Rush
Set 'throwaway PHNX-4291 cascade' target date -> 2026-12-08.
Moved 1 open issue(s) due 2026-12-01 -> 2026-12-08.
$ linear tasks PHNX-4302
  Due:      2026-12-08

Goals. The team array and avatar URLs are omitted here because the repo is public:

$ linear goals --json
  "company": {"initiative": {"name": "Rush Cloud - first paying customers", "startDate": "2026-08-02", "targetDate": "2026-12-31", …},
              "logoUrl": null,
              "milestones": [{"name": "0.1.0 — Interactive cloud run", "project": "Rush", "targetDate": "2026-10-16", "progress": 92},
                             {"name": "0.1.4 — Agent Profiles", "targetDate": "2026-10-30", "progress": 82, …},
                             {"name": "Ready to charge", "targetDate": "2026-10-30", "progress": 0, …},
                             {"name": "First design partner live", "targetDate": "2026-11-20", "progress": 18, …}]},
  "week": {"cycle": {"number": 32, "name": "Launch gate green on all three harnesses", …},
           "goal": "Launch gate green on all three harnesses"},

$ linear goals set day "test" --milestone "Ready to charge" --json
{"identifier": "PHNX-4303", "title": "test", "dueDate": "2026-10-08",
 "milestone": {"name": "Ready to charge", "targetDate": "2026-10-30"}, "done": 0, "total": 0, "todos": []}
$ linear goals todo "test todo"
Added PHNX-4304: PHNX-4303 test (0/1) · due 2026-10-08 · Ready to charge
  [ ] PHNX-4304 test todo
$ linear goals todo --done PHNX-4304
Done PHNX-4304: PHNX-4303 test (1/1) · due 2026-10-08 · Ready to charge
  [x] PHNX-4304 test todo
$ linear goals set week "test week"       # → PHNX-4305, due 2026-10-12 (cycle 32's last day, LA)
$ linear goals set day "test (updated)"   # update path; re-parents the day goal under the week goal
$ linear goals
Company   Rush Cloud - first paying customers · 84 days left (target 2026-12-31)
            0.1.0 — Interactive cloud run  2026-10-16  85%
            …
Week      Launch gate green on all three harnesses · Cycle 32 · ends 2026-10-12 (4 days)
My week   PHNX-4305 test week (0/1) · due 2026-10-12 · 0.1.0 — Interactive cloud run
My day    PHNX-4303 test (updated) (1/1) · due 2026-10-08 · Ready to charge
            [x] PHNX-4304 test todo

set company-week was exercised by renaming cycle 32 to its current name, which is a no-op on real data.

Two runs predate small fixes in this same commit. The reminder line in the first output still shows microseconds; it now prints whole seconds. The human view's week line repeated the cycle name before it printed Cycle 32.

Behavior changes to note

  • Project required: create now refuses with no project. Before, --skip-milestone with no project created an orphan issue.
  • Low priority: low no longer means Backlog state, and create no longer puts every issue in the active cycle. Both follow the horizon now.
  • Default delegate: with agent configured, new issues are delegated to that agent.
  • Unrelated stale binding: ~/src/…/agents is bound to a "Prix" project that isn't in this team. create from there refuses and lists the team's projects, as designed.

The release (version bump, install.sh pin and tag) follows in a separate chore(release) PR, per the repo's convention.

🤖 Generated with Claude Code

muqsitnawaz and others added 2 commits October 9, 2026 05:58
…HNX-4291)

create resolves each issue to one horizon (now, milestone, backlog) and
prints the defaults it applied: due date from policy.sla by priority, an SLA
reminder for urgent/high, the active cycle, and the project's next open
milestone. The project comes from --project or the cwd binding; with neither,
create refuses and teaches the three horizons. --bug / --bug --blocker,
default delegate from the configured agent, a policy block in config, and
milestones set-target-date carrying its issues to the new date.

linear goals reads the company quarter, week, my week, my day with to-dos and
the team's goals; --json is the agi-menu contract. goals set week|day|
company-week and goals todo write them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
goals todo --done only closes a to-do of today's Daily goal; a non-object
policy value is a clear error, not a traceback; a failed default SLA
reminder warns without failing the create (an explicit --remind-at still
exits 1); an unavailable team timezone says so on stderr.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@muqsitnawaz

Copy link
Copy Markdown
Contributor Author

Independent review (code-reviewer subagent, non-author): APPROVE. No blocking findings.

It traced every horizon rule in plan_horizon and _build_create_input, and every field of the goals contract in build_goals and goal_object, against the spec. The suite passes, the code parses with Python 3.9 grammar, and the docs and CHANGELOG are consistent.

Its non-blocking findings and what happened to each (fixup 4f8a57e):

  • Fixed: goals todo --done <ID> could close any issue. It now only closes a to-do of today's Daily goal and refuses anything else. Live check: linear goals todo --done PHNX-4290 returns "is not a to-do of today's Daily goal (none is set)" with exit 1. Covered by a new test.
  • Fixed: a shorthand policy.sla.low: "cycle_end" raised a traceback. It is now a clear error. Covered by a new test.
  • Fixed: a failed default SLA reminder exited 1 even though the issue had been created, which invites a retry that files a duplicate. It now warns only. An explicit --remind-at still exits 1.
  • Fixed: an unavailable team timezone (missing tzdata) now prints a stderr note instead of falling back silently.
  • Partly addressed: write-path tests. The todo --done guard is now tested. set week/day is covered by the live runs in the PR body.
  • Left as is (minor): a day goal is filed under the company's next milestone, not the week goal's. This follows the spec and the README.

python3 -m unittest: Ran 267 tests, OK (skipped=4).

@muqsitnawaz
muqsitnawaz merged commit de555a5 into main Oct 9, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant