feat(create): refuse to create an unassigned issue without --force - #44
Conversation
An issue with no assignee is nobody's job — it lands in the board's
"No assignee" bucket and nobody picks it up. `linear create` now fails
closed with a message that says why it refused and how to fix it,
instead of silently producing an unowned ticket.
The default path is unchanged: with no --assign, the API key owner is
still the assignee. Two paths now error where they previously did not:
- `--assign none`
- an `--assign` value matching no human, which used to warn
"leaving unassigned" and create the issue anyway
`--force` is the deliberate escape hatch. The check applies per row to
`--from-file` bulk create; `--force` waives it for the whole file, or a
row can carry "force": true. Bulk error text stays on one line so the
tab-separated record is still parseable.
|
VERDICT: APPROVE Code ReviewerVerdict: Ready to merge Build: N/A — single-file Python CLI (no build step) Instruction files readNone found — the repo has no Changes that work well
Issues that need attentionNone. The diff is focused, well-structured, and every claim in the PR description is backed by code or tests. Things to verify manually
Reviewed by [Code Reviewer] — actually ran the tests on this branch. 139/139 pass. |
Three findings from the non-author review of #44: 1. The refusal message and skill.md offered `--delegate <agent>` as a standalone fix. It isn't one — delegate sets delegateId, never assigneeId, so following that advice got you refused again with the identical message. Both now say delegate rides alongside --assign rather than standing in for it. Covered by a new test. 2. The unresolvable-assignee reason interpolated the caller's --assign value raw. In bulk --from-file that value lands in a tab-separated record, so a tab or newline in it split the record into phantom columns or rows — breaking the exact single-line invariant this change claims to hold. Now interpolated with !r, which escapes both. Covered by a new test over tab / newline / CRLF values. 3. CHANGELOG, skill.md, and README each said only two paths newly error. There are three: an unresolvable API key owner also refuses now, and that case previously created an unowned issue with no warning at all. Documented on all three surfaces.
|
VERDICT: APPROVE Code ReviewerVerdict: Ready to merge Build: Not applicable — single-file Python CLI script, no build step (syntax-checked both Tests: 141 passed, 0 failed, 0 skipped (0.024s) Operating instructions read: none exist in this repo (no Changes that work well
Issues that need attentionNone. The diff is well-scoped, correctly implemented, and thoroughly tested. Things to verify manually
Reviewed by Code Reviewer — actually ran the build and tests on this branch. |
Reviewing the assign-value escaping turned up the same defect one field
over: `title_hint` and the created issue's title were interpolated into
the tab-separated bulk record raw. A title with a tab added a phantom
column; a title with a newline split one row into two. Any consumer
doing line.split("\t") — the shape the README and skill.md document —
silently gets garbage.
Adds tsv_field() and applies it to both title fields, so the
single-line-record invariant this branch documents actually holds for
the whole row rather than just the reason column.
Predates the owner check; fixing it here because the CHANGELOG entry
added on this branch is what claims the record stays parseable.
Non-author review — verdict: READY TO MERGEReviewed by the Round 1 findings — all closedBLOCKER — BLOCKER — TSV injection through the SHOULD — docs undercounted the refusal paths. There are three, not two: an unresolvable API key owner also refuses now, and that case previously created an unowned issue with no warning at all. Documented in Adjacent defect found and fixedThe reviewer flagged that Real bulk row with a hostile title — 4 fields, 1 line: State at merge
Filtered as non-defects across both rounds: Known gap, deliberately out of scope: |
Three findings from the non-author review of #44: 1. The refusal message and skill.md offered `--delegate <agent>` as a standalone fix. It isn't one — delegate sets delegateId, never assigneeId, so following that advice got you refused again with the identical message. Both now say delegate rides alongside --assign rather than standing in for it. Covered by a new test. 2. The unresolvable-assignee reason interpolated the caller's --assign value raw. In bulk --from-file that value lands in a tab-separated record, so a tab or newline in it split the record into phantom columns or rows — breaking the exact single-line invariant this change claims to hold. Now interpolated with !r, which escapes both. Covered by a new test over tab / newline / CRLF values. 3. CHANGELOG, skill.md, and README each said only two paths newly error. There are three: an unresolvable API key owner also refuses now, and that case previously created an unowned issue with no warning at all. Documented on all three surfaces.
|
VERDICT: APPROVE Code ReviewerVerdict: Ready to merge — clean design, thorough tests, all 145 pass. Build: N/A (dependency-free Python script — no build step) Operating instructions readNo Changes that work well
Issues that need attentionNone. Notes
Reviewed by Code Reviewer — actually ran the build and tests on this branch. |
Why
The board fills with tickets nobody owns. Grouping the active cycle by assignee shows a "No assignee" bucket that just sits there — an unassigned issue is nobody's job, so nobody picks it up.
linear createhad two paths that produced unowned tickets, one of them silently:--assign none— created it unassigned, no comment--assign <value>that matched no human — printedWarning: ... leaving unassigned.and created the issue anyway (linear:3116before this change)What changed
createnow fails closed when the issue would land with no assignee, and says why.The unresolvable-assignee path gets the same treatment, with its own reason line:
--forceis the deliberate escape hatch:The default path is unchanged — with no
--assign, the API key owner is still the assignee, so ordinarylinear create "..."never trips the guard:Both smoke tickets (PHNX-3980, PHNX-3981) were canceled immediately after.
Bulk (
--from-file)The check applies per row. Bulk output is a tab-separated record, so the reason is rendered on one line there rather than as the multi-line block — a newline would corrupt the record.
--forcewaives the check for the whole file; a single row can also carry"force": true.Tests
7 new cases in
CreateRequiresOwnerTest, covering all three unowned reasons, the--forcebypass, the named-assignee and default-owner happy paths, and the single-line invariant for bulk error text.Docs
README.md(usage line + "For humans and agents" bullet),skill.md(new "Every issue needs an owner" section — this is what agents read),CHANGELOG.mdunder Unreleased, and both--assign/--forcehelp strings.