Repository navigation
Releases: kmoneil/jr
Release list
v0.20.2
Take this one if you reopen issues, or write to one issue more than once
under --if-unchanged. A sequence can now open with its move, so a reopen
and the changes that go with it are one plan (issue 265), and a guarded write
can hand back the precondition for the next one (issue 266).
Nothing changed for anybody who does not use the new spellings, except that
issue edit, issue move and issue assign report their new schema version
in v: a consumer pinning v on those three has a version to add.
A reopen is one sequence. issue sequence carried one move, as its last
step, which is a close. A reopen runs the other way round: the move first, then
the edits that belong to the reopened ticket, which a closed status can refuse
outright (the classic workflow's Closed sets jira.issue.editable=false).
[["issue", "move", "ENG-101", "Reopen Issue"],
["issue", "edit", "ENG-101", "--summary", "Flaky on ARM"],
["issue", "comment", "add", "ENG-101", "Reopening: it came back on the ARM runners."]]- One move, as the first step or the last. A move in the middle, or a second
move, is stillTRANSITION_NOT_LAST. - Every step is still dry-run when the plan is written, so a field, a value, a
person or a sprint that could never work refuses the plan before anything is
sent. The checks that depend on the status (the permission, the edit screen,
the assignee) wait for the move, and each later step's checks say
after step 1: .... - The apply sends the move, runs those checks against the reopened issue, then
sends the rest. One that fails stops the run with the issue moved and nothing
else sent, and that step isfailedwith the check's own code. Re-applying
the same file once it is fixed sends the rest. - Measured against Jira Cloud. A status with
jira.issue.editable=falsehas
not been measured on either deployment; the tests stand in for one with a
constructed issue.
The next precondition, from the write. A write moves the issue's
updated, so the token that guarded it is stale once it lands, and a chain of
guarded writes cost an issue get between every two.
$ p=$(jr issue move ENG-101 Open --if-unchanged "$p" --precondition \
--format json | jq -r '.issue["next-precondition"]')
$ jr issue edit ENG-101 --summary 'Reopened' --if-unchanged "$p"--preconditiononissue edit,issue moveandissue assignreads the
issue back after the write and reports its token asnext-precondition, with
or without--if-unchanged. It costs one request, so it is asked for.- It is the issue as read after the write, not the version the write made. A
change landing in between, such as an automation rule the write fired, is
inside it, as it would be in anissue getafter the write. - If the write lands and the read fails, the result still says the write
happened and the exit is the read's. The error says not to send it again and
is never retryable: retrying a move sends a second transition. - A token after
--preconditionis refused asPRECONDITION_TAKES_NO_VALUE,
because it belongs to--if-unchanged. Beside--dry-run,--plan-outor
--applythe flag isPRECONDITION_NEEDS_A_WRITE. - Measured against Jira Cloud. On Data Center it is covered by constructed
fixtures and has not been run against a live server.
--tz on a live Data Center. The 0.20.1 notes said --tz had not been run
against a live Data Center. It has since, on 10.4.0: issue get, issue list,
issue activity and issue changes under a named zone, local and account,
JIRA_TZ, and a printed value given back to --updated-after. Nothing needed
to change.
Documentation
- docs/output-contract.md: what a write's
next-preconditionis and what it
is not; a sequence's first move and when the checks after it run; an
archived project on Data Center answers a search with no rows. - docs/recipes.md: reopening an issue and changing it, as one sequence; two
guarded writes to one issue. - docs/troubleshooting.md:
PRECONDITION_TAKES_NO_VALUEand
PRECONDITION_NEEDS_A_WRITE;TRANSITION_NOT_LASTwith a move first or
last; an archived Data Center project that reads as empty. - The skill:
--preconditionon the first of two writes to one issue, and a
sequence that reopens.
Internal
- Recordings from Jira Cloud: a guarded edit, then a move guarded by the
token the edit handed back; a reopen sequence's plan and its apply. A
recording from Data Center of one unknown project, with its manifest row. - golang.org/x/sys 0.49.0 and golang.org/x/term 0.47.0.
Output contract
issue.editv3,issue.movev4,issue.assignv3: an optional
next-preconditionattribute onissue, present only under
--precondition.- New error codes
PRECONDITION_TAKES_NO_VALUEand
PRECONDITION_NEEDS_A_WRITE, exit 2, reached only through--precondition.
On a sequence step the flag isSTEP_TAKES_NO_PLAN_FLAG. TRANSITION_NOT_LASTno longer refuses a move as the first step.
issue.sequence.planandissue.sequencedid not move: a first move's
waiting checks are entries in thecheckslist the plan already had.- Priced a patch. Each change is additive by its row. A first move was
refused and now answers, and so does--precondition, which was an unknown
flag: accepting an input that used to be refused. Three kinds gain an
optional attribute. The two new codes are reached only through the new flag,
so no invocation that worked before answers with one. No command makes more
requests than it did unless--preconditionasks it to.
v0.20.1
Take this one if you read jr's timestamps anywhere but UTC, or if a
context or a script of yours can name a project that is gone. --tz writes
timestamps in a zone you name (issue 218). And on Jira Cloud an empty answer
over a project Jira does not know is now refused, where through 0.20.0 it was
reported as an empty result at exit 0.
One consequence worth knowing before you upgrade: on Jira Cloud, a search
over one project that reads no issue now costs a second request. A
--max-requests that was exactly enough for that answer is one short, and the
command stops at the budget, REQUEST_BUDGET_EXCEEDED at exit 3, where it
answered at exit 0. --max-requests 1 on issue list is the plain case: make
it 2.
Timestamps in a zone you choose. Every timestamp jr printed was UTC,
while every date it is given is read in the Jira account's zone, so a write-up
for people in one place meant converting each one by hand.
$ jr issue get ENG-101 --tz Europe/Berlin | grep -E 'created|updated'
<created>2026-05-12T01:30:00+02:00</created>
<updated>2026-05-12T11:10:00+02:00</updated>--tztakesutc(the default, and the same bytes as before),account
(the zone on your Jira profile, for one GET to/myself),local(the
machinejrruns on), or an IANA name such asAmerica/Chicago. The value
keeps its offset, so it is still one instant, still RFC 3339, and still goes
straight back into any date flag.- It can be asked for in three places, and the nearest wins:
--tz, then
JIRA_TZ, then a context'stz, whichjr context create --tzandjr context edit --tzset and--unset tzclears.jr context showreports the
one in effect. - The flag exists only on a command that writes a timestamp.
jr schema <command>lists it there, andjr version --tz localis an unknown flag.
JIRA_TZand a context'stzare ignored by a command with nothing to
respell. - Over MCP a tool call takes
tzon the same commands, and one that names none
falls back to the zonejr mcp servewas started with. - The zone is settled before the command runs. A write under
--tz account
that cannot learn the zone is refused before anything is sent, so it cannot
change Jira and then fail to say so. - What stays in UTC:
jr doctor, which has no--tz, because underaccount
its zone would be a request made with the credential it is there to
diagnose; every instant in a warning or an error on stderr, thereachedof
a walk that was cut short and the bounds inEMPTY_RESULTamong them; and
version'sbuilt. Adatehas no zone and does not move. - A zone that names nothing is
INVALID_TZat exit 2, and the message says
whether it was read from--tz,JIRA_TZor the context. An empty--tzis
refused and not read as UTC.JIRA_TZis checked on every command, as
JIRA_FORMATis. - Sorting a timestamp column as text sorts it by time only in UTC. Under a zone
with daylight saving, use--sortor parse the values. - Measured against Jira Cloud. On Data Center it is covered by recorded
fixtures and has not been run against a live server.
An unknown project is not an empty result. Jira Cloud answers a search over
a project it has never heard of with HTTP 200 and no rows, which is exactly
what it says of a real project with nothing in it, and jr passed both on as
EMPTY_RESULT at exit 0. A context whose project had been deleted or renamed
went on reporting that there was nothing to do.
- When
issue list,issue activityorissue changesreads no issue at all
over a scope of one project, it asks Jira about the project, and refuses with
UNKNOWN_PROJECTat exit 5, in Jira's own words, when Jira does not know it.
Nothing has reached stdout at that point, in any format, and an MCP caller
gets the same refusal. - The scope is asked about wherever it came from:
--project,JIRA_PROJECT,
or the context. - A project that exists and holds nothing that matches is still an empty result
at exit 0. - The question costs one request, and only when no issue came back. An answer
with rows is never asked about. A poll ofissue changesthat finds nothing
updated pays it on every tick. - Jira Cloud only. Data Center refuses such a search itself, as
BAD_REQUEST
at exit 2 (measured on 10.4.0), and that has not changed. - TSV writes its header with the first row, or at the end of a collection that
has none, so a command that reads an empty page and then refuses has written
nothing. Output from a command that succeeds is the same bytes.
Documentation
- docs/output-contract.md: a section for
--tz, with what moves and what does
not;EMPTY_RESULTsays when a project is asked about and what that costs;
UNKNOWN_PROJECTcovers one project as well as a set. The stability policy
gains a row for a command that makes more requests than it did. - docs/troubleshooting.md: entries for
INVALID_TZand forUNKNOWN_PROJECT
on one project. - docs/recipes.md: writing timestamps in a zone.
- The skill tells an agent to ask for a zone and not convert by hand, and that
no rows over a project that does not exist is an error.
Internal
- A sweep runs every command the test harness can answer under a zone, on both
deployments' fixtures, and fails on any timestamp left in UTC. It is the
check for an instant built into a field declared a string, which every other
check would pass. It found none. - Two recordings from Jira Cloud: the empty answer over a project that does not
exist and over one that does, which the search answers identically.
Output contract
context.getv3 andcontext.listv3: an optionaltzattribute,
absent when no zone is set.timestampis RFC 3339, in UTC unless a zone was asked for, and then with
that zone's offset in the value. No kind that holds one moved, and with no
zone asked for every document is the bytes it was.- New error code
INVALID_TZ, exit 2.NO_ACCOUNT_TIMEZONEand
UNKNOWN_ACCOUNT_TIMEZONE, exit 2, are also reached by--tz account. UNKNOWN_PROJECT, exit 5, is also reached by one project that answers empty
on Jira Cloud, where the result wasEMPTY_RESULTat exit 0.- On Jira Cloud an answer with no issues over one project costs one more
request than it did. - Priced a patch. Each change is additive by its row.
--tzwas an unknown
flag and now answers: accepting an input that used to be refused. Two kinds
gain an optional attribute.INVALID_TZis reached only through--tz,
JIRA_TZor a context'stz, none of whichjrread before. An unknown
project reported as empty was a successful answer that was wrong, and an
error code for that condition is additive. The extra request is priced by a
row this release writes: a command that makes more requests than it did is
additive and is listed, which is what 0.18.0 did with the request it added
to everyissue activityrun. The reader that row costs something is the
one named at the top of this section, whose budget was exactly enough. JIRA_TZis a namejrdid not read before, so no setting made forjris
refused. A shell that already exports it for something else now has it read,
and refused on every command if it is not a zone.- No exit code changed meaning.
v0.20.0
Take this one if you ask about more than one project, and read it first if
anything you run passes --project twice. --project now repeats where the
answer is a union over projects, and is refused where it is not (issue 216).
Through 0.19.8 a second --project was accepted on every command and the last
one won, without a word about the others.
$ jr issue list --project OPS --project NET --project DO --updated-after -3dissue list,issue activity,issue changesandjql explaintake
several, and searchproject IN ("OPS", "NET", "DO")with every key quoted. A
key that is a reserved JQL word, asDOis, needs nothing from you. The
spelling this replaces,--all-projects --jql 'project in (...)', still
works, and still leaves the quoting and the scope to the caller.- The result names the set. The envelope carries
projects="OPS NET DO", a
list in JSON and YAML, and noproject. One project is written asproject,
exactly as before, so check for either when you need to know an answer was
scoped.EMPTY_RESULTsaysprojects=OPS NET DO, andSCOPE_MISMATCH
compares a--jqlagainst the whole set. - A key Jira does not know is refused,
UNKNOWN_PROJECTat exit 5, in Jira's
own words. Jira Cloud's search leaves an unknown key out ofproject in (...)and answers with the rest, so a mistyped key would have shortened the
answer and said nothing. The check costs one request, and only when there are
two or more keys. - Every other command refuses a second
--project, asPROJECT_REPEATED
at exit 2, before any request.jr board list --project OPS --project NET
used to list NET's boards and say nothing about OPS. Run such a command once
per project.jr schema <command>reportsrepeatableon--projectper
command, so a caller can ask before it sends. - A member of a repeated
--projecthas to be a project key
(INVALID_PROJECT_KEYotherwise), and a key given twice is one key. One
--projectis still passed to Jira as typed.JIRA_PROJECTand a context's
projectstill hold one key each. jr mcp servestarted with several projects follows the same rule: a tool
call for a command that reads one project is refused.
auth login reports the credential it stored as site-scoped. Its result
said site-scoped="false" for a credential it had just stored keyed by the
site's host, where auth status for the same site said true. Only the
login's own result was wrong. How a credential is found and where it is sent
did not change.
Documentation
- docs/output-contract.md:
repeatableon--projectis per command, the
envelope'sprojects, the three codes a repeated--projectcan meet, and
jql.explainv3. The stability policy's row for an answer that contradicted
the command's own account now covers a value as well as rows. - docs/troubleshooting.md: entries for
PROJECT_REPEATEDand for
UNKNOWN_PROJECTon a set, and the reserved-word section teaches repeating
--projectinstead of hand-writtenproject in (...). - The skill tells an agent to repeat
--projectrather than write the JQL.
Internal
- A registry sweep holds every command's declaration that it takes several
projects to the accessor the command calls, with a negative control. A
command that declared the set and read one project would be handed nothing
and answer for the whole site. - Three recordings: a two-project listing on Cloud whose third page crosses
from one project to the other, and the refusal of an unknown key on Cloud and
on Data Center 10.4.0. On Data Center a set of projects is walked by offset
and never by issue key, because a key bound does not compare across
projects: measured there, over two projects, to the last row.
Output contract
jql.explainv3: an optionalprojectslist, written in place of
projectwhen the scope is several projects.- The envelope gains an optional
projectsattribute, written in place of
projectfor a set. It moves no kind's version: the envelope is outside
every kind's shape. - New error code
PROJECT_REPEATED, exit 2.UNKNOWN_PROJECT, exit 5, and
INVALID_PROJECT_KEY, exit 2, are also reached by a repeated--project. auth.status:site-scopedon a login's result istruewhere it was
false. The kind did not move.- Priced a minor, on one row. A second
--projecton a command that reads
one project was accepted, the last key winning, and now exits 2 with
PROJECT_REPEATED: refusing an input that used to be accepted. The rest is
additive. On the four commands that take a set, the same input now answers
for every key and not the last, and a set holding an unknown key is an error
where it was an answer about fewer projects than were named: both are a
wrong answer corrected, with nothing on a command line to edit. So is the
login'ssite-scoped, under the row this release widens from rows to a
value. - No exit code changed meaning.
v0.19.8
Take this one whichever commands you use. Nothing jr does changes: no
flag, no output, no exit code. The binaries are built with Go 1.26.9, where
0.19.7 and every release back to 0.13.3 were built with 1.26.8, and 1.26.9
fixes nine advisories in the standard library that govulncheck reports
reachable from this code:
GO-2026-6603,net/http: HTTP/2 server memory exhaustion due to Trailer
headers.GO-2026-6605,net/http: HTTP/1 client connection desynchronization after
CONNECT rejection.GO-2026-6607,crypto/tls: reject malformed ECH outer extension
references.GO-2026-6608,net/textproto: memory limit bypass when parsing MIME
headers.GO-2026-6610,net/http: HTTP/2 transport accepts malformed
framing-related headers.GO-2026-6611,net/http: excessive CPU consumption from repeated initial
window changes.GO-2026-6612,net/http: double flow control refund on HTTP/2 server
streams.GO-2026-6613,net/http: HTTP/1 server connection desynchronization after
a 2xx CONNECT response.GO-2026-6617,net/http: HTTP/2 server crash due to an HPACK encoder race.
Those are the advisories' own titles. "Reachable" is the scanner's word and
means the call graph gets there. Nobody has judged here which of the nine a
Jira server, or a proxy in front of one, could use against jr. What the
titles do say: four name an HTTP server (6603, 6612, 6613, 6617), and jr
listens on no socket in any profile. Two name the client (6605, 6610), and
three do not say whose side they are on.
Internal
internal/rendergains the two tests the weekly mutation sweep asked for
(issue 252). The truncation warning'stotal, and itssort,orderand
reached, were asserted only frominternal/cli, and the sweep measures a
package against its own tests. The package's count of surviving mutants
falls from 12 to 11.
Output contract
- No kind moved, no code was added, and no invocation is answered differently.
- Priced a patch. A toolchain is nothing a caller sends and nothing a
consumer parses, so no row of the stability policy is touched and the
default position moves.
v0.19.7
Take this one if you work against more than one Jira site, or read
comment-heavy activity feeds. Logging in to a second site now makes its
context (issue 206), and issue activity --body-chars keeps the first part of
each body rather than all of it or none of it (issue 219).
$ printf '%s' "$TOKEN" | jr auth login --site other-company.atlassian.net \
--email you@company.com --token-stdin
$ jr --context other-company issue list --assignee currentUser- With a context already defined, a login to a site no context named stored
the credential, reportedauthenticated="true", and made nothing, so
--contextansweredUNKNOWN_CONTEXT. It now makes a context named for the
host's first label and reports it incontext. The current context never
changes. - A context already naming the site is left alone. Naming it means the same
host in any case, with the scheme and a trailing slash ignored and the path
exact, so a Data Center site under/jiraand the root of its host are two
sites. - The first label of a Data Center host is usually
jira, so a second one
often finds that name taken. Then nothing is made, the credential is still
stored, and the warningCONTEXT_NOT_CREATEDnames the context holding the
name and both ways out.--context-namenames the context yourself; a name
another site's context holds is refused asCONTEXT_NAME_TAKEN, exit 2,
before the credential is checked or stored, because making it would replace
that context.
$ jr issue activity --since -1d --body-chars 120--body-chars Nkeeps the first N characters of each comment and worklog
body, counted in code points so a cut never splits one. A cut body carries
truncated="true"and the whole body'slength; TSV gains abody-length
column afterbody, filled only where the body was cut.- A cut body does not make the result incomplete. Every event is there, and
the exit stays 0. - Refused before any request beside
--no-body(BODY_CHARS_AND_NO_BODY),
beside--raw-body(BODY_CHARS_AND_RAW_BODY, since an ADF document cut
short is not a document), and below 1 (INVALID_BODY_CHARS). - In markdown, a bounded feed stays one table instead of a section per event.
Output contract
issue.activityv2: optionaltruncatedandlengthon an event's
body, written only when--body-charscut it. No other kind carrying a
body moved.auth.status:contextis now populated on a second site's login, where it
used to be absent. The kind did not move.- New warning
CONTEXT_NOT_CREATED, fromauth login. - New error codes, each only on an invocation using one of the new flags:
CONTEXT_NAME_TAKEN,INVALID_BODY_CHARS,BODY_CHARS_AND_NO_BODY,
BODY_CHARS_AND_RAW_BODY. - Priced a patch. Two new flags, a new warning code, new optional
attributes, and an optional attribute populated where it was absent are each
additive. The new refusals refuse only invocations that use a new flag, so
nothing that worked before is refused.
v0.19.6
Take this one if you sort a list and cut it short, or read jr through its MCP
server. A list cut short under --sort now says how far down that order it
got (issue 217), and the MCP server's truncation warning now carries the
total the command line has carried since 0.19.2.
$ jr issue list --updated-after -3d --sort updated --order desc --limit 100
# stderr: RESULT_TRUNCATED, count: 100, sort: updated, order: desc,
# reached: 2026-05-09T14:20:00Z, remedy: resume with --page-token, ...reachedis the sort field's value on the last row written, as the row
renders it, so "every issue updated after 14:20" is a statement the warning
supports. Rows sharing that value can sit on either side of the cut: a walk
resumed from the token may begin on it again.sortnames the field id andorderthe resolved direction. A named field
ascends unless--ordersays otherwise, andreachedreads the opposite way
in each.- The sort field is fetched for this whether or not a column shows it, and is
not added to the rows. A--sortnaming a field outside the default set
(due,'Story Points') is resolved through the field catalogue, the cached
request--fieldmakes; sorting by the key or a default field costs nothing
more. - The three are absent where the boundary is unknown: a list-valued sort field
(labels, a multi-select, a sprint field), a sort the catalogue cannot name,
and an empty value on the last row.
The MCP server built its own copy of the truncation warning and never learned
total when 0.19.2 added it to the command line, so a model was told
"truncated" where a terminal was told "100 of 104". Both now come from one
conversion.
Output contract
- The
RESULT_TRUNCATEDwarning gains optionalsort,orderandreached
leaves on a cutissue list --sort. The warning'svstays 1, and no kind
changed. - Over MCP, the same warning now carries
totalwherever the command knows it,
as the command line's has since 0.19.2. - Priced a patch. Optional leaves on a warning, populated where they used
to be absent; nothing a consumer reads today reads differently.
v0.19.5
Take this one if your issues link outward, or your projects are filed under
categories. Two reads the API had and this tool did not, issues 214 and 220,
both measured on Cloud and Data Center before anything was built.
jr issue remotelink list returns an issue's links to things outside Jira:
the web links a person pastes in, and the links an integration writes to a
commit, a build, a document. Until now issue activity showed a link being
added and nothing could return its URL.
$ jr issue remotelink list ENG-101
id application relationship title url
10000 Deploy runbook https://wiki.example.com/runbook
10001 Example CI mentioned in Pipeline 42 https://ci.example.com/pipelines/42- A link an application wrote names it, and may carry the remote side's own
resolved state. One a person added arrives with neither: Jira sends an empty
application object for it, and that is reported by absence, so a
remotelinkwith noapplicationis a hand-added web link. - The id is the value an activity item names (
RemoteIssueLinkon Data
Center,RemoteWorkItemLinkon Cloud), so the listing resolves what the
changelog refers to. After a removal the id resolves to nothing: read the
links while they exist. issue get --with-remote-linksfolds the same set into the record. The
endpoint serves it whole in one response on both deployments, so unlike the
comment thread this container is never partial.
jr project list now reports the category a project is filed under, and can
filter by it, so "the platform team's projects" is one invocation on a site
whose categories name the owning teams:
$ jr project list --category Platform --with-category- The
categoryelement carries the server's name and id, and is absent, not
empty, on a project with none, which is how both deployments report that. --categorykeeps the projects filed under that exact name, ignoring case.
It is not a substring search, unlike--match: a category is a closed set
an administrator curates, so a fragment matches nothing rather than
guessing. A blank value is refused asEMPTY_QUERY.- The filter runs client-side on both deployments, because Cloud's search can
filter by category where Data Center's listing silently ignores the same
parameter, and a rule only one server applies cannot be held to one answer. --with-categoryappends the column; the default table keeps its four
columns, because adding one there is a breaking change.
Output contract
- New command:
issue remotelink list; new kindissue.remotelink.listv1. issue.getv12: an optionalremotelinkscontainer, present only with
--with-remote-links.project.listv3 andproject.getv3: an optionalcategoryelement,
present when the server sent one.- New flags:
issue get --with-remote-links;project list --categoryand
--with-category.--raw-fieldrefuses--with-remote-linksbeside it,
as it refuses every document-shaping flag. - New error code:
MALFORMED_REMOTE_LINKS, exit 9. - Priced a patch. Everything here is additive: a new command, a new kind,
and optional elements nothing existing looks for. The three kind versions
moved because a changed shape at an unchanged version is refused, and a
kind's version moving decides nothing about the release by itself.
v0.19.4
Take this one if you make several changes to one issue at a time. Closing a
ticket the way a team expects is a comment, a field and a transition: three
commands, and a failure at the second leaves the issue half done with nothing
saying so. jr issue sequence plans them as one document and applies them in
order (issue 157). Each step is written as the command it would have been, as
JSON, and read by that command's own flags:
$ jr issue sequence ENG-101 --steps-file close.json --plan-out close.xml
$ jr issue sequence --apply close.xml --dry-run
$ jr issue sequence --apply close.xml- Planning sends nothing. Every step is dry-run as its own command, so it is
refused where that command would be, and then checked for what the commands
leave to Jira: the permission each step needs, every edited field on the edit
screen, the assignee assignable, a linked issue readable, the sprint open. A
plan with any step that cannot run is not written, and the refusal names
every such step. - Applying sends nothing from the file. The issue's baseline is compared,
and every step is rebuilt through its own command and held byte for byte to
what the plan recorded, so an issue changed since planning (STALE_WRITE) or
a plan that drifted or was edited (PLAN_DRIFTED) runs nothing. - The first failure stops the run, and the document says which steps were
applied, which failed with what code, and which were not attempted; the exit
is the failing step's. Applying the same file again resumes. - A step is
issue edit,issue assign,issue comment add,issue link add,
sprint addorissue move, on the sequence's issue, with at most one move,
last. Create stays its own call.
Documentation
- docs/recipes.md closes a ticket as one sequence; docs/troubleshooting.md
gains the sequence's refusals; the skill's workflows reference teaches it.
Internal
- Parsing a command's flags moved from the CLI into the registry, so a step
parses exactly as the same words do at a prompt; a sweep holds the two to one
answer for every command. - A plan of six steps is recorded on each deployment and replayed, so every
read the pre-flight makes was answered by a real server.
Output contract
- New command:
issue sequence. - New kinds:
issue.sequence.planv1, what--plan-outwrites, and
issue.sequencev1, what--applyreports. - New error codes, all exit 2 unless noted:
SEQUENCE_NEEDS_A_PLAN,
NO_STEPS,STEPS_AND_STEPS_FILE,INVALID_STEPS,TOO_MANY_STEPS,
STEP_NOT_ALLOWED,GLOBAL_FLAG_IN_STEP,STEP_TAKES_NO_PLAN_FLAG,
STEP_NAMES_ANOTHER_ISSUE,STEP_READS_STDIN,TRANSITION_NOT_LAST,
FIELD_NOT_ON_SCREEN,USER_NOT_ASSIGNABLE,PERMISSION_DENIED(exit 6),
PERMISSION_NOT_REPORTED(exit 9),PLAN_DRIFTED(exit 7). - Priced a patch. A new command and two new kinds only it emits are
additive by the stability policy; no existing kind moved, nothing that ran
before is refused or answered differently. - No exit code changed meaning.
v0.19.3
Take this one if you paste a timestamp jr printed back into a date flag.
Every date flag refused the RFC 3339 instants jr prints, so asking what else
happened around a moment meant converting a UTC timestamp into the Jira
account's timezone by hand, and a conversion off by an hour is a complete,
exit-0 answer for the wrong window (issue 213). Every date flag now takes one:
2026-05-12T09:00:00Z, without seconds, with a fraction, or with a numeric
offset. JQL reads a date only to a minute of the account's clock, so jr
converts the instant into that zone, which costs one request to learn, and only
when a flag holds an instant.
- An instant with seconds is widened to the minute, outward: down for an
-afterflag and up for a-before, so the window sent holds the one you
asked for. Onissue listthe newDATE_ROUNDEDwarning names the bound
that was sent. An instant on the minute is sent exactly and nothing is said. - On the night the account's clock goes back, a minute in the repeated hour
names two instants, andjrnever sends one: the bound moves on to a minute
that names one instant, up to two hours further, and the warning says why. issue activityandissue changescompare each event to the instant
itself, so their answers are exact and they never warn.- A clock with no offset, and Jira's own
+0000spelling, are refused as
INVALID_DATE, with a hint naming the two spellings that work. On Data
Center,--worklog-afterand--worklog-beforerefuse an instant as they
refuse any time of day.
issue changes could skip an hour on the night the clocks go back. Its
query bound was the window's start truncated to the minute in the account's
zone, and inside the repeated hour that minute names two instants. A server
reading it as the later one left out an hour of changes the window still
needed. The bound now moves to a minute that names one instant, as above.
Documentation
- The recipe for meaning your own day, in docs/recipes.md,
docs/troubleshooting.md and the skill, is one command now: send your
midnight with your own offset, fromdate -Iseconds, andjrconverts it.
docs/troubleshooting.md gainsDATE_ROUNDED.
Internal
scripts/landwaits on and merges the commit its checkout holds, refuses a
branch holding commits that are not pushed, and from a second worktree says
where the base is checked out instead of pulling into the pull request's
branch. It has a test now, against a bare repository and a stand-ingh.- Two new invariants: no accepted date form reaches a query in a shape JQL
refuses, and a rounded bound moves outward and never to a minute that names
two instants. The second is a year-long sweep in eight zones, dense around
every transition, checked against a reading that does not use the code.
Output contract
- No kind moved.
- New warning code on
issue list:DATE_ROUNDED. - Priced a patch. An input that used to be refused now answers, and a
warning code is added, and the stability policy prices both as additive.
Nothing that ran before is refused or answered differently, except that
issue changesreads the hour it used to skip on the night the clocks go
back. - No exit code changed meaning.
v0.19.2
Take this one if a script or an agent reads a truncated result to decide what
to do next. The RESULT_TRUNCATED warning carried total, how many rows the
whole answer held, on two commands. It now carries it wherever jr knows the
number: on every list that reads its whole set before --limit cuts it (the
board, epic, sprint, project, component, version, status, field, transition,
create-field, link and attachment lists), on issue list against Data Center,
whose search counts every query, and on issue comment list and
issue worklog list, whose endpoints send a count. "100 of 104" and "100 of
5,000" now read differently, which is the difference between one more page and
a narrower question (issue 217). Where nothing counts the rows, issue list
and issue history on Cloud and user list, there is still no total, and
its absence still means unknown.
issue activity --until ends a window. "What changed in the half hour
before 09:30" is one command rather than a feed trimmed by hand (issue 215).
--since keeps the events at or after its instant and --until the events
before its own, so two windows that meet share no event and miss none. It
narrows the answer and not the search: every issue updated after --since is
still read. A window that holds no instant is refused as EMPTY_WINDOW.
project list --match finds projects by key or name, ignoring case, and
repeats for any of several, in place of --limit all piped to grep
(issue 220). jr applies it on both deployments, so they agree, and it costs
no request, because the listing already reads the whole catalogue. A blank
--match is refused as EMPTY_QUERY, so an unset shell variable cannot list
everything as if it had been filtered.
Documentation
- The skill's main page says that a pipe hides the exit code:
jr ... | head
reportshead's zero over a truncated result's 3 (issue 221). It was only in
reference files an agent may never open. - docs/troubleshooting.md gains
EMPTY_WINDOWandEMPTY_QUERY,
docs/recipes.md an incident-window recipe, and docs/getting-started.md finds
a project's key with--match.
Internal
- A fourth sweep, driven from the registry with a negative control and a
ledger: every paginated command is cut to one row on both deployments, and
the warning'stotalhas to be the answer's size, or the ledger has to say
why the command cannot know it. Against 0.19.1 it failed 34 runs across 17
commands. - The paging sweep's fake no longer sends a count on Cloud's enhanced search,
which the real server never does.
Output contract
- No kind moved.
- New error code, exit 2, on
issue activity:EMPTY_WINDOW, for--until
at or before--since. EMPTY_QUERY, exit 2, whichuser listgives a blank search, is also what
project listgives a blank--match.- Priced a patch. The warning's
totalis populated for inputs where it
used to be absent, which is the stability policy's row for an optional
element the schema already declared.issue activity --untiland
project list --matchare new optional flags; nothing that ran before is
refused or answered differently. - No exit code changed meaning.