Repository navigation
feat(jira): show and edit story points on the issue detail - #122
Merged
Merged
Conversation
The host's field catalogue and the issue detail's edit form both have to answer "is this field the story-points field", and only the host answered it. Moving the rule into shared/jira-field-rendering.ts lets the renderer reuse it when it reads Jira's own editmeta, so a site's field id is found by one rule rather than by a second, drifting copy. Behaviour is unchanged: Greenhopper's own field type, or the float custom field a site names "Story points".
Story points were readable — the host resolves the site's points field from the field catalogue, and the list view offers a Points column — but no issue view displayed them, so the value only existed behind a column most people never enable. The detail's metadata bar now carries the points beside status, priority, assignee and sprint: the current value, and a popover that sets or clears it. The right-sidebar Issue pane gets the same value as one of its property rows. Both share one editor, and both write through the custom-field path the "Edit fields" form already uses, with the field's own id taken from Jira's editmeta, so nothing is keyed to one site and Jira still decides what is writable: an issue whose points are not writable shows them but offers no editor. Story points join the fields the optimistic-patch bookkeeping protects, so the value does not snap back to the old one while the write is in flight and returns to it if Jira refuses.
The list view's planning columns are the list half of "show story points": the column exists and the host resolves the value, but nothing asserted the cells render it, so the claim rested on code reading alone. These cover the value, the unset dash, and the sprint cell beside it.
Five findings from the adversarial review of the control, all of them about a value that is easy to get subtly wrong: - The points box was an `<input type="number">`, which reports an empty value for text it cannot parse, so a draft of "5e" or "1." read as an empty one and saving cleared the field. It is now a text box parsed here: a draft that is not a number disables Save and says so, and only an actually empty box clears. - A save through the popover left the "Edit fields" form holding the value Jira no longer had, because that form seeds its drafts from the same editmeta read. A custom-field write through the shared mutation path now re-reads that read, so the two forms agree whichever one wrote. - A site can expose more than one story-points field, and the write went to the first one editmeta listed while the view showed another. The editor now prefers the field holding the value the view displays. - Save was disabled only for this field's own write, so a save attempted while another field was in flight was refused by the mutation guard with an error this form has nowhere to show; the whole form is now disabled for any pending write, as the row's own popover already was. - The detail chip named its value (`8 pts`) rather than showing a bare number with only a tooltip to say what it was; the labelled Issue pane row keeps the number. Tests cover the unparseable draft, the blocked save, the two-field choice, that a save re-reads editmeta, that a refusal rolls the chip back and reaches a toast, and that the list cells render points.
…ot name The inferred type of a bare vi.fn() pair needs a vitest-internal Procedure type the renderer project cannot reference (TS2883), so the spies are typed by their call signature.
…und-2 notes
The second review round passed the change but left three minor findings and a
handful of accessibility notes, all of which are cheaper to fix than to argue
about:
- The read/write pairing still had a hole. The control preferred the editable
field holding the value on screen, but fell back to a different points field
when none held it, so on a site with two points fields a save could land where
the view never reads and appear to do nothing. The issue read now carries the
id of the field its points came from (`storyPointsFieldId`), so the editor
writes that field and offers no editor at all when Jira will not let this user
write it. A host that predates the field omits it, and the client falls back to
matching the value.
- The detail chip read "1 pts" on a one-point issue. It names the field instead
("Points: 1" / "No points"), which needs no plural rules in any language.
- The test that pins the post-write `editmeta` re-read only asserted "more than
one call"; it now pins one read on open and exactly two after a write, and that
a refused write reads nothing.
- Accessibility: the chip's accessible name is now its visible text (it was
"Story points: 8" beside a visible "8 pts"), the input is described by its own
error message, and it is read-only rather than disabled while a write is in
flight so a refusal does not drop the caret.
- A value Jira will not let this user write is now a disabled property row in the
Issue pane, matching how the pane's own "Fields" row reads when nothing is
editable.
…cribed-by The pane mounts the row itself, so the wiring deserved an assertion of its own: a value Jira will let this user write now shows up in the pane. The input also points at its message only when there is one, rather than at an empty element on an issue with no points.
…leared keys Round 3's findings, one of which is a regression this branch introduced: - `mapStoryPoints` returned an object without the points keys when an issue had none, where the inline mapping it replaced always set them. A refresh is spread onto the issue on screen, so an omitted key left points that Jira no longer had visible until the next full read. Both keys are now always present. The old tests could not catch this: `toEqual` treats a missing key and an undefined one as the same, so the mapping has a case that asserts the keys exist. - Jira's reason for refusing a write appeared only in a toast, which disappears while the user is still looking at the value that needs correcting. The form now shows it beside the field (and as its `aria-describedby` description), so the write is submitted with `reportsInline` and no toast competes with it. - The trigger was disabled during its own field's write, so pressing Esc while a save was in flight left focus with nowhere to go; it is only disabled while another field is writing now. - Three helpers that nothing imports are no longer exported, and the tests cover the `aria-describedby` wiring and the inline refusal.
Round 4 found that the previous commit's inline reporting introduced a worse failure than the one it fixed: with `reportsInline` suppressing the toast, a save whose popover was dismissed before Jira answered — Esc, a click outside, or the issue being switched — showed its reason on a form nobody could see, then lost it on reopen. On the revision before, a toast still reported it. The editor now tracks whether its form is up: the reason goes inline while it is, and to the toast when the form has gone. It is also dropped as soon as the draft changes, so a refusal can no longer mark a value that was never sent. Two more from that round: the Issue pane's row was still disabled during its own save, so Escape there had nowhere to put focus (the chip had been fixed), and the chip's enabled-while-writing behaviour had no test. Both are covered now.
Round 5's remaining case: the popover's open state was only tracked through its own dismissals, so a component that unmounted mid-write — the Issue pane's remount, a worktree switch — left the form marked up, and the refusal was written to a form that no longer existed. Clearing that on unmount sends the reason to a toast instead, which is the one place still on screen.
The board's cards showed the key, title, status, assignee, attached workspace and priority, but not the number a scrum board is planned by — the one field the board is named for. It now sits in the card's top corner beside the status pill as Jira's own `3sp` shorthand, from a translation with the value interpolated so the abbreviation travels with the locale. A card Jira holds no points for carries no chip rather than a dash; zero is a value and reads as `0sp`. Nothing is editable from the card — the detail's control is still the only writer.
The card chip was dead on arrival: `listBoardIssues` requested only the standard issue fields and mapped the page without the site's agile field ids, so every board issue arrived with `storyPoints` undefined and nothing could ever render. The read now resolves the field catalogue (shared and cached per site, answering empty when it cannot be read) and maps the discovered ids, exactly as the list and detail reads already do. Found while checking the card's own test against a real board payload rather than a fixture that set the value directly; the new host test fails without this.
The board-card chip's name is built from the card's text, so a screen reader read the shorthand as "3 s p"; it now carries its own label while still showing `3sp`. The "no chip" case queries the chip itself instead of looking for the letters "sp" anywhere in a card's text.
Story points are what a scrum board is planned by, and this branch now shows them in the issue detail and on the board's cards — but the list still made every reader find the Columns picker first, because upstream ships all planning columns hidden. Points joins the default set; the other planning columns stay opt-in, and anyone with a saved column choice keeps it, so only a fresh list changes shape. This is a deliberate divergence from upstream's default in `jira-list-columns.ts`, recorded in the unit test's name and comment and in the e2e spec's own toggling so the two do not disagree about what is already visible.
With story points in the default column set, the md and lg panel steps spend 72px of fixed tracks on it, so the title lands on the grid's own minmax(180px, 1fr) floor there. The width test asserted the 200px margin it was calibrated on, which that no longer meets, so it now asserts the floor at those two steps and keeps the 200px margin where there is room. Measured in the real app, headless, at the four panel widths the test drives: 337px at 500, 180 at 780, 185 at 1050, 367 at 1320. The guard's other halves (the header's fixed tracks matching the row's, the mobile sort controls, and the assignee column's tier) are untouched.
The previous commit's threshold change was measured but its reasoning was partly wrong, and it only asserted the title floor — which the grid's own minmax(180px, 1fr) already guarantees — so it could not see the real cost of the wider default row: at panel widths just above the md and lg breakpoints the row no longer fits and scrolls sideways (measured: 63px of overflow at 780, 8px at 1037, none from 843 and 1050). The md step now measures 860px, still in the md tier and wide enough for the default set, and the test asserts the row does not overflow at the two ends and at that step. The comment records the measured numbers, the two bands that do scroll, and why the original 200px expectation was already wrong here at 780: with upstream's columns on a machine with a stable scrollbar gutter the title is 189px, not 200.
Round 11 measured every panel width and found the previous comment's numbers wrong: the md band that scrolls is 770-844 (73px at its worst, not 63), the lg band is 1026-1044, and 1045 upwards fits — 1050 has 5px to spare rather than none, which was the stated reason for skipping its fit check. The title is 197px at 860, not on the 180px floor. The lg step now measures 1060 (still the lg tier, 13px of slack) so the row's fit is asserted in every tier rather than left to the title floor in one, and the comment carries the measured bands and slack.
Five commits from the fork's line, one of which (#121, matching the board's Me filter on any role field) rewrote the same board issue read and agile-field discovery this branch touches. The reconciliation, by file: - `jira-agile-fields.ts`: one cached `/field` read per site already answered the agile planning fields and now also the user-picker role fields, each reader with its own patience. Kept that structure and the shared `isJiraStoryPointsField` rule for the planning half; dropped the two field-type constants the shared rule replaced and the module-level caches the per-patience readers superseded. - `jira-board-queries.ts`: the board performs one catalog read and uses both halves of it — role ids within the request budget for "Me", the agile ids for the story points a card shows — and maps each issue with both. - `jira-issue-mapping.ts`: `mapJiraIssue` keeps #121's `JiraIssueFieldIds` and its `roleAssignees`, with this branch's `mapStoryPoints` helper as its points half. - `jira-board-queries.test.ts`: #121's tests kept as they were; this branch's points case rewritten in the same style, on the real catalog read, so it also pins that one read answers both halves. Verified on the merge: `pnpm test jira` 185 files / 1616 tests, cold `tc:node` and `tc:web` clean, and the two Jira e2e specs pass headless.
6 of 7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ELI5
Story points live in a Jira field that Orca could read but almost nowhere showed. The task list had an optional "Points" column you had to switch on, and no issue view displayed the value at all — the only way to change it was to know it was hiding inside the "Edit fields" dialog. Now the value sits in the issue's own detail, next to status and priority, and you can set or clear it right there.
What Changed
The issue detail now shows story points and edits them in place.
Before: opening a Jira issue (task detail, or the linked-issue pane in the worktree sidebar) showed status, priority, assignee, sprint and labels. Story points were nowhere, and changing them meant knowing they were one of the fields in the "Edit fields" form.
After: the detail's metadata bar carries the points beside the other properties —
Points: 8, or "No points" when the issue has none — and clicking it opens a small form that sets or clears the value; Enter saves, Esc backs out. The right-sidebar pane gets the same value as one of its property rows. Where Jira says the field is not writable (or reports no story-points field for that issue at all), the value is still shown but there is nothing to click.The board's cards show them too. The card in the sprint/backlog board previously showed the key, title, status, assignee, attached workspace and priority — the plan's own number was missing. It now sits in the card's top-right corner beside the status pill as Jira's
3spshorthand. Nothing is editable from the card (the detail's control is the only writer); a card Jira holds no points for carries no chip, and zero reads as0sprather than as an absence. This needed the board's own issue read to ask Jira for the site's points field — it did not, so the chip would have been dead on arrival; that is fixed here and pinned by a host test.The list shows the Points column by default. The list already rendered story points, but upstream ships every planning column hidden (stablyai#28615), so the number a scrum board is planned by sat behind the Columns picker. Points now joins the default columns: a fresh list — and one whose column choice was never saved — shows it, while anyone who has saved a column choice keeps exactly that choice. The other planning columns (Parent, Sprint, Estimate, Remaining) stay opt-in. This is a deliberate divergence from upstream's default, recorded in
jira-list-columns.ts, pinned by the unit test's expectation, and reflected in the e2e spec's own toggling; if upstream later changes its planning-column defaults, this one line is the conflict.Mechanism, in short. The field id is not hardcoded: it comes from Jira's own
editmetaread — the same read the "Edit fields" form is built from, which already lists the story-points custom field — and the write goes through the already-shippedcustomFieldsupdate path. The issue also carries the id of the field its value was read from, so a site with two points fields cannot have one shown and another written. No new RPC parameter or stream message exists, so an older host or a paired client is unaffected rather than silently no-oping. The control carries the value optimistically and puts it back if Jira refuses, keeps Jira's reason beside the field while the form is open (a toast for a refusal that lands after it has gone, so a failure is never reported nowhere), and the shared mutation path re-readseditmetaafter a custom-field write so the "Edit fields" form cannot end up showing a value Jira no longer has.Why
Story points are the one number on a Jira issue that a team plans by, and Orca had it in hand without displaying it: the host already discovers the site's points field for the list column and the agile reads. The narrow gap was the detail view, so that is where the control went, reusing the field-editing plumbing from #98/#112 rather than adding a parallel write path.
Design decisions worth knowing about:
Points is on by default in the list, where upstream hides it, and the cost is measured rather than described. The column costs 72px of the row (60px plus its gap), so:
Measured in the real app, headless, at eleven panel widths; the e2e spec now asserts both the title floor and that the row fits where the default set should. For reference, the original 200px expectation already failed at 780 on this machine with upstream's columns (189px, because a stable scrollbar gutter takes 11px), so that step now measures 860 instead. If the wider minimum matters more than seeing points, the flag is one word in
jira-list-columns.ts.The write reuses
customFields, with the field id fromeditmeta, rather than adding astoryPointsfield toJiraIssueUpdate. A new update field would need its own capability read: the RPC schema strips fields it does not declare, so an older host would report success while writing nothing — the exact silent no-op feat(task-page): reliable Jira lane drops and custom-field editing #112 had to fix for sprint assignment.customFieldsis already declared and already keyed tocustomfield_*, and theeditmetaread itself is the capability check (a host predating it returns no editable fields, so no editor is offered).One candidate field beats a hardcoded id. Jira's field catalogue can hold more than one story-points field (a classic float beside a next-gen estimate), so the issue now carries the id of the field its value was read from (
storyPointsFieldId, an added optional read field) and the editor writes that one. When Jira does not offer that field as writable on the issue, no editor is offered at all — writing a different points field would leave the value on screen unchanged. A host that predates the field omits it, and the client falls back to matching the value, which is the best a client can do alone.Both detail surfaces, not one. feat(task-page): reliable Jira lane drops and custom-field editing #112 put the field-edit form in the task detail and the sidebar pane; story points follows the same pair, from one shared editor.
The card uses Jira's
3spshorthand, and cannot edit. A board card is a compact, draggable surface with a fixed corner; the number goes there and editing stays in the detail, so a drag can never turn into a field write. The abbreviation is a translated string with the value interpolated, so a locale can spell it out.Clearing is explicit. An empty box submits
nullrather than omitting the key, because the update path only writes what it is given and a Jiranullis what clears a field.The chip says
Points: 8. The pane row has a label beside it, the detail bar's chip does not, so the chip names the field instead of showing a bare number with only a tooltip to explain it. A label rather than aptssuffix also keeps one point from reading as "1 pts" in English and needs no plural rules in the other locales.Linked Issue
Fixes #118
Visual Proof
N/A — no Jira site is configured in this environment, so there is no connected board or issue to capture a before/after against. The control, its failure paths and both detail surfaces are covered by component and integration tests instead (list below). A reviewer with a connected site can see it in ten seconds: open any issue in a scrum project, click the points chip in the metadata bar, type a value, press Enter.
Testing
Run from the worktree, on the final revision.
pnpm tc— exit 0. Also run cold, with a fresh--tsBuildInfoFileso no incremental cache could hide a result:tc:web,tc:node,tc:clieach exit 0.pnpm test jira— 185 files, 1616 tests passed (after the base merge). Every Jira test in the repo, which is the whole surface this change touches.pnpm test src/renderer/src/components/task-page src/renderer/src/store src/renderer/src/i18n src/renderer/src/runtime src/renderer/src/components/right-sidebar— 1102 files, 9445 tests passed (1 pre-existing skip): the task views, the store the optimistic patch writes to, the catalogs, the runtime clients, and the pane.pnpm run check:code-quality:changed— passed, 0 findings across 26 changed files, measured against the merged base (including the design-system, React Doctor and type-aware passes).translatekey resolves inen.jsonand its fallback matches (checked with the catalog verifier's own extractor);verify:localization-extractionandaudit-localization-coverage --checkboth pass. The repo-wideverify-localization-catalogreports one pre-existing dotted key,settings.appearance.chat.title, which is present atnplez1/mainand unrelated to this change.pnpm lintruns green through repo-wide oxlint, the anti-slop and code-quality audits, the reliability and max-lines ratchets, and then stops atcheck:ts-nocheck-ratchetonsrc/main/runtime/workspaces/workspace-process-listing.ts— a file this diff does not touch and which carries the same suppression atnplez1/main. That is a pre-existing red, not a failure of this branch; the gates that cover this change (check:code-quality:changed, its type-aware and design-system passes) are green.src/renderer/src/components/jira-issue-story-points-control.test.tsx— writes a value, clears one, refuses a draft that is not a number instead of reading it as a clear, blocks a save while another field is being written (and keeps a refused save's focus), shows Jira's refusal beside the field, describes the input with its own message (and with none when there is none), writes the field the issue names, offers no editor when Jira will not write that field, matches the value for a host that does not name it, shows points Jira will not let you write, renders nothing when there is neither a value nor an editor.src/renderer/src/components/JiraIssueWorkspace.story-points.test.tsx— through the real workspace: the write's payload, exactly oneeditmetaread on open and two after a write, none after a refused write, the rollback, and that a refusal is reported beside the field instead of in a toast.src/renderer/src/components/right-sidebar/jira-story-points-row.test.tsxand a case in.../right-sidebar/JiraLinkedIssuePane.test.tsx— the pane's row and the pane mounting it: value, editor, read-only when Jira will not write it, disabled while another field is pending.src/renderer/src/components/jira-issue-workspace-hydration.test.ts— the optimistic patch is tracked, rolled back, and protected from a stale fetch.src/renderer/src/components/task-page-jira-issue-cells.test.tsx— pins the list's Points and Sprint cells, andsrc/renderer/src/components/jira-list-columns.test.tspins the new default set (story points in, the other planning fields out). The e2e spec that exercises the planning columns was updated for the same default.src/renderer/src/components/task-page/jira/Board.test.tsxandsrc/main/jira/jira-board-queries.test.ts— the card shows3sp,0spfor zero and no chip when unset, and the board's issue read asks for the site's discovered points field and maps it onto every issue it returns.src/shared/jira-field-rendering.test.tsandsrc/main/jira/jira-agile-fields.test.ts— the shared "is this the story-points field" rule, that the mapped issue names the field its points came from, and that an issue whose points are unset still carries both points keys (a refresh is spread onto the issue on screen, so an omitted key would leave a value Jira no longer has).nplez1/mainwas merged in (59ca23f716) — five commits, one of which (feat(task-page): match the Jira board's Me filter on any role field #121, the board's Me filter matching any role field) had rewritten the same board issue read and the same agile-field discovery. The board read now takes one catalog read and uses both halves of it: the role-field ids within the request budget, and the agile ids so a card can show story points.pnpm test jirapasses on the merge, coldtc:node/tc:webare clean, and the two Jira e2e specs pass.ORCA_BACKGROUND_LAUNCH=1 npx playwright test … --project=electron-headless, hidden renderers, no focus stolen):tests/e2e/jira-panel-layout.spec.tsandtests/e2e/jira-tasks-settings-link.spec.ts— 3 passed. The panel-width spec was re-measured in the real app rather than reasoned about, and now asserts the title's floor where the default column set fills the row plus that the row does not overflow at the widths where it should fit (500, 860 and 1320; 1050 sits exactly on the lg minimum, so its fit is left to the floor).pnpm testsuite (it has a known runtime trap on this machine, a separate hunt is on it), the fullpnpm lintchain past the pre-existing ratchet failure above, andpnpm build(left to CI).typecheck:e2ecannot pass on this machine for reasons outside this diff (thecloud/workspace is not installed, and 500-odd pre-existing spec errors); the two Jira specs this change touches typecheck with no errors. No Jira site, Windows, Linux or SSH runtime was exercised locally.AI Disclosure
Implemented by an AI coding agent (pi) using DeepSeek and MiMo models for research and edits, with the review rounds below.
Review
Claude Opus 5.5 (
claude/opus, the Claude Code CLI backend) at high reasoning, tool-enabled, adversarial, over the full diff againstnplez1/main.54e07b5212) found five issues and returned NOT SHIP-READY: a number input reported an unparseable draft (5e,1.) as empty and saving cleared the field; a popover save left the "Edit fields" form holding a stale value; the control could show one story-points field and write another; a save during another field's write was refused with an error this form had nowhere to show; and the chip showed a bare number with only a tooltip for a label. All five were fixed, with tests for each.abec452b1c) returned SHIP-READY with three minor findings worth closing anyway: the read/write field pairing still fell back to a different field when nothing held the displayed value,1 ptshad no singular, and the test that pins theeditmetare-read only asserted "more than one call". All three were closed, along with its accessibility notes (the chip's accessible name now contains its visible text, the input is described by its own error, and the input is read-only rather than disabled so a refused save does not drop focus).814f6a6ec6) returned SHIP-READY but flagged one real regression it wanted fixed before the PR: the newmapStoryPointsleft the points keys off an issue with no points, where the inline mapping it replaced always set them, so a refresh spread onto the issue on screen could keep showing points Jira no longer had. Fixed in671fae1095, along with its other notes (Jira's refusal now shows beside the field instead of only in a toast; the chip stays focusable during its own save; three unused exports dropped).671fae1095) returned NOT SHIP-READY: the previous fix had moved Jira's reason inline and turned the toast off, so a save whose popover was dismissed before Jira answered reported the failure on a form nobody could see and then lost it. Fixed inc16512ad66: the reason goes inline while the form is up and to a toast once it is gone, it is dropped as soon as the draft changes, and the Issue pane's row no longer disables itself during its own save.c16512ad66) returned SHIP-READY and asked for one further line: the ref that decides whether a refusal can be shown inline was not cleared when the editor unmounted — the pane's own remount or a worktree switch — so a refusal could still land on a form that no longer existed. Fixed inc1f5e65995.c1f5e65995) returned SHIP-READY with no blocking or major findings and nothing it required to be fixed. Its one note was optional (a test for the unmount path above, which is why that fix has no test of its own); it was left out deliberately so that the reviewed revision is the shipped revision, and it is the first thing to add if that cleanup is ever touched.f9873719bb, the board card) returned NOT SHIP-READY with a BLOCKER: the board's issue read never asked Jira for the points field, so the new chip could never render. The read now resolves the site's field catalogue (the same cached read the list and detail use) and maps the discovered ids, with a host test that fails without the fix (f32014b922), and the chip's shorthand is announced as "3 story points" rather than "3 s p" (e5717ccb0b).2031fe8735, the Points column by default) returned NOT SHIP-READY with a MAJOR: the default column set spends 72px of fixed tracks at the narrow panel steps, so the issue title lands on the grid's 180px floor and the upstream e2e guard that asserted a 200px margin would fail. Fixed in507c301c55by measuring the four widths in the real app and asserting the floor where it now applies; the reviewer's predecessor revision could not be verified by running e2e locally at the time.507c301c55) returned SHIP-READY with two MINORs worth fixing before merge: the reworked assertion could not see the real consequence of the wider default row (panels just above the md and lg breakpoints now scroll sideways), and its comment overstated one number and blamed the points column alone for a failure that the stable scrollbar gutter already caused at 780. Both were fixed in0cf59ae4bdby measuring the app directly and asserting the fit.0cf59ae4bd) returned SHIP-READY with two MINORs, both fixed in47230e2aa2: several measured numbers in the new step's comment were wrong (the md band that scrolls is 770–844 at up to 73px, the lg band is 1026–1044, and 1050 has 5px to spare rather than none), and the lg tier's fit was not asserted anywhere. The lg step now measures 1060 so every step asserts both the title floor and that the row fits.59ca23f716, the base merge) returned SHIP-READY. It verified the reconciliation point by point: one catalog read per board page taken before the request-queue slot, the request budget applied to the role fields alone, both halves handed tomapJiraIssuein the shape it expects, nothing lost from either side (the once-per-site warning survives), and the deleted test mock having weakened nothing. Its two notes were optional and are recorded here rather than folded in, so the reviewed revision is the shipped one: the branch's board test now covers the backlog scope where it used to cover the sprint scope (thefieldslist is built before that split, so nothing differs), andmergeJiraIssueHydrationtakesstoryPointsFieldIdfrom the fetched issue rather than the locally edited one, which on a multi-points-field site can only make the editor fall back to matching by value — the field it would still match.e5717ccb0b) returned SHIP-READY with nothing it required to be fixed. It confirmed every board read path (IPC, runtime RPC, web preload, both scopes, Cloud and Server/DC, paging) reaches the fixed read, that the optimistic board move keeps the value, that the field catalogue is shared and cached per site so no board page pays a new request, and that a discovery failure still lets the board load. Its optional notes — a test for the call ordering, an exact rather than substringfieldsassertion, and ansr-onlyspan instead ofaria-labelon the chip — are recorded here rather than folded in, so that the reviewed revision is the shipped revision.Agent skill upstream boundary
Notes
customfield_*; Jira still enforces the user's own permissions. Theeditmetaread is read-only. Nothing is written that Jira did not report as writable for that issue.jira.listEditFields, which a host predating feat(task-page): reliable Jira lane drops and custom-field editing #112 reports as empty — the value is then shown read-only rather than offering a write that would silently no-op. The addedstoryPointsFieldIdis an optional read field: an older host omits it and the client falls back to matching the value against the editable fields. One known limit, shared with sprint, parent and assignee: a remote JSON transport dropsundefinedissue fields, so an issue whose points were cleared on Jira can keep showing the old value until the next full read, locally only.customFieldsis an existing optional field;storyPointsFieldIdis a new optional read field that an older host simply omits. No new stream opcode or RPC parameter.editmetaread that already runs with the issue's side data — plus oneeditmetare-read after a custom-field write, which is what keeps the two forms agreeing. One extra request there was chosen over two surfaces disagreeing about the value.inputMode="decimal", so touch keyboards still get a numeric keypad. The control uses the same primitives and chrome as the neighbouring status, priority, assignee and sprint controls.Checklist
N/Awith reasonpnpm buildleft to CI;pnpm tcpasses locally, with the focused suites and changed-lines gate recorded above