Skip to content

feat(inbox): agent-actionable — mark-read + rich JSON metadata (0.13.0)#26

Merged
muqsitnawaz merged 2 commits into
mainfrom
inbox-actions
Jul 19, 2026
Merged

feat(inbox): agent-actionable — mark-read + rich JSON metadata (0.13.0)#26
muqsitnawaz merged 2 commits into
mainfrom
inbox-actions

Conversation

@muqsitnawaz

Copy link
Copy Markdown
Contributor

What

Makes linear inbox actionable for agents (follow-up to #25).

Actions

  • linear inbox --read <id> — mark notification(s) read (repeatable)
  • linear inbox --read-all — mark everything read

Richer --json — each notification now carries enough to follow the ticket/thread and act:

  • issue { identifier, url, state } — to open/comment on the ticket
  • comment { id, url, body } + parentComment — the thread
  • actor, notification url/inboxUrl, type, readAt

Comment back uses the existing surface: an agent reads the issue.identifier from --json, then linear update <identifier> --comment "...". The plain listing now prints this recipe.

Verified

  • python3 -m unittest — 24/24 pass (added a mark-read test asserting one notificationUpdate per id).
  • Live: --json returns the full metadata set; inbox --read <id> marked a notification read (tested non-destructively on an already-read one); --read/--read-all in --help.

Backed by notificationUpdate / notificationMarkReadAll (schema-introspected). 0.12.0 -> 0.13.0.

Actions: --read <id> (repeatable) marks notification(s) read via
notificationUpdate; --read-all via notificationMarkReadAll. Richer --json:
each notification now carries issue{identifier,url,state}, comment{id,url,body},
parentComment (thread), actor, and the notification url/inboxUrl — enough for an
agent to follow the ticket/thread and act (reply via 'linear update <id>
--comment'). Listing prints the follow-up recipe. 0.12.0 -> 0.13.0; adds a
mark-read unit test.
@prix-cloud

prix-cloud Bot commented Jul 19, 2026

Copy link
Copy Markdown

Code Reviewer

Verdict: Changes requested (minor)

Build: N/A — single-file Python script, no build step. python3 -c "import ast; ast.parse(open('linear').read())" passes, ./linear inbox --help shows the new --read/--read-all flags, ./linear --version reports linear-cli 0.13.0.
Tests: python3 -m unittest test_linear -v — 24/24 passed, including the new test_cmd_inbox_read_marks_each_given_notification.

No AGENTS.md/CLAUDE.md/GEMINI.md exist anywhere in this repo (checked root and touched dirs), so there were no repo-specific build/test overrides to defer to — README/skill.md don't document anything beyond what's above.

Changes that work well

  • --json metadata expansion (issue.state.type, comment.id/url, parentComment, notification url/inboxUrl) is a clean, additive GraphQL query change — no existing field was removed or renamed, so it's backward compatible for any existing consumer of --json.
  • The "actions take precedence over listing" short-circuit (linear:2800-2816) is easy to follow and the new test exercises the multi-id path correctly (test_linear.py:97-119).

Issues that need attention

1. _mark_notifications_read swallows the actual GraphQL error message (linear:2774-2787)

success = (((data.get("data") or {}).get("notificationUpdate") or {}).get("success"))
(ok if success and "errors" not in data else failed).append(nid)

Every other per-item mutation loop in this file calls check_errors(data) before checking success, which prints the underlying Error: <message> to stderr (e.g. the issueRelationCreate loop at linear:1704-1715, or projectDelete at linear:2437-2442). This new function reimplements the "did it fail" check inline and never surfaces data["errors"][0]["message"]. I reproduced it directly:

ok, failed = _mark_notifications_read('key', ['bad-id'])   # gql returns {'errors': [{'message': 'Notification not found'}]}
# ok=[] failed=['bad-id']
# caller prints: "Failed to mark read: bad-id"
# -> "Notification not found" is never shown anywhere

An agent (or human) hitting a bad id, a permissions error, or a rate limit gets no signal why the mutation failed — just the bare id back. Suggest reusing check_errors(data) per iteration (or at least appending the message) instead of the ad hoc "errors" not in data check.

2. --read-all failure doesn't set a non-zero exit code, unlike --read (linear:2801-2807 vs 2809-2816)

if getattr(args, "read_all", False):
    data = gql(api_key, "mutation { notificationMarkReadAll { success } }")
    if check_errors(data):
        return
    ok = (((data.get("data") or {}).get("notificationMarkReadAll") or {}).get("success"))
    print("Marked all notifications read." if ok else "Failed to mark all read.")
    return

If the mutation returns success: false without a top-level GraphQL error, this prints "Failed to mark all read." to stdout and returns with exit code 0. Compare to the --read path a few lines down, which on failure prints to stderr and does sys.exit(1) (linear:2814-2815), and to the established convention elsewhere in the file, e.g. projectDelete:

if check_errors(data) or not (data.get("data") or {}).get("projectDelete", {}).get("success"):
    print("Project remove failed.", file=sys.stderr)
    sys.exit(1)

Since the whole point of this PR is to make inbox actionable for agents, a silent success: false that still exits 0 is the one case most likely to bite an automated caller scripting off the exit code. Minor, but worth aligning with the rest of the file (stderr + sys.exit(1) on a success: false result).

Neither of these is covered by the new test — it only exercises the all-succeed path for --read and doesn't touch --read-all at all.

Things to verify manually

  • The PR description says the live --read test was run "non-destructively on an already-read one" — worth confirming notificationUpdate is idempotent against an already-read notification in your Linear workspace (it presumably is, given the test result, but I can't hit the live API from this sandbox).

Reviewed by Code Reviewer — actually ran the build and tests on this branch.

… --read-all failure

1. _mark_notifications_read now calls check_errors(data) per iteration so a bad
   id / permission / rate-limit error prints 'Error: <message>' to stderr,
   matching the other per-item mutation loops (was swallowing data['errors']).
2. --read-all now prints failure to stderr and sys.exit(1) on success:false,
   consistent with --read and the projectDelete convention.
@prix-cloud

prix-cloud Bot commented Jul 19, 2026

Copy link
Copy Markdown

Code Reviewer

Verdict: Ready to merge

Build: Green — python3 -c "import py_compile; py_compile.compile('linear', doraise=True)" compiles clean; linear --version reports 0.13.0, matching the bump in linear:40 and CHANGELOG.md:8.
Tests: Green — python3 -m unittest → 24/24 pass, including the new test_cmd_inbox_read_marks_each_given_notification (test_linear.py:97-119).

No repo-level AGENTS.md/CLAUDE.md/GEMINI.md exists (checked root and all directories touched by the diff), so there's no documented non-default build/test process to defer to — plain python3 + stdlib unittest is correct and matches the repo's normal CI (.github workflow already runs unittest, confirmed via gh pr checks 26).

Changes that work well

  • _mark_notifications_read (linear:2773-2788) follows the exact not check_errors(data) and data[...]["success"] idiom used everywhere else in this file (e.g. linear:1763, 1795, 1834), and correctly tracks per-id success/failure so a partial failure still reports which ids succeeded.
  • The local import datetime inside _mark_notifications_read (linear:2775) shadows the module-level from datetime import date, datetime, timezone (linear:35) only within that function's scope. I was suspicious of this at first glance, so I loaded the module directly and called the function with a faked gql — it resolves datetime.datetime.now(...)/datetime.timezone.utc correctly against the module binding and produces a valid ISO timestamp (2026-07-19T02:00:02.000Z in my run). No bug.
  • --read-all and --read both exit non-zero on failure and print to stderr (linear:2807-2809, 2818-2820) — this is exactly what the prior commit (45e9d3e) in this same PR fixed, and it's still correct here.
  • Action-vs-listing precedence (linear:2803 comment, --read-all then --read then fall through to list) is a sensible, clearly-commented structure for a single command that now does double duty.
  • --json enrichment (issue.state.type, comment.id/url, parentComment, actor, notification url/inboxUrl) is a straightforward additive schema change with no removed fields, so it's non-breaking for any existing consumer of the old JSON shape.

Things to verify manually

  • I don't have Linear credentials in this sandbox (linear inbox --json → "No API key configured"), so I could not independently re-run the live notificationUpdate/notificationMarkReadAll calls the author already verified. The GraphQL shapes look correct against check_errors/gql conventions used elsewhere, but a live schema mismatch (e.g. readAt input field name) would only surface at runtime.
  • There's no test for the --read-all path or for the partial-failure branch of --read (both ids fail, or one succeeds/one fails) — worth a follow-up test given the prior commit in this same PR was specifically about getting that exit-code/error-surfacing behavior right, but I wouldn't block on it given the rest of the file's test coverage is similarly targeted rather than exhaustive.

Reviewed by Code Reviewer — actually ran the build and tests on this branch.

@muqsitnawaz
muqsitnawaz merged commit fd1adff into main Jul 19, 2026
2 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