Skip to content

feat(bearings): link ticket ids and PRs on every board list - #5722

Open
adithya321 wants to merge 2 commits into
kunchenguid:mainfrom
adithya321:fm/board-links
Open

adithya321 wants to merge 2 commits into
kunchenguid:mainfrom
adithya321:fm/board-links

Conversation

@adithya321

Copy link
Copy Markdown

Intent

On the /bearings lavish fleet board, an annotation on an Underway row whose title leads with a tracker ticket id asked: "can we hyperlink the ticket id to the issue tracker? similarly for github PRs in these lists". Follow-up question asked: should firstmate make ticket ids link to the tracker and PRs link to GitHub in every board list; answer: yes.

Today the board template (.agents/skills/bearings/assets/board-template.html) links a PR only on merge cards and Recently Landed rows (pr_url), renders no PR link on Underway or Charted Next rows, and never links a ticket id anywhere.

make sure no data from the employer whose board prompted this (company, repos, people, ticket ids, PR URLs, tracker workspace) goes to the firstmate repo PR

What Changed

  • The bearings board template now renders a ticket link (labelled with ticket, or "ticket" if no label is given) and a PR link (#<number>) on every Underway, Recently Landed, and Charted Next row, plus every Captain's Call item. Before this, PR links appeared only on merge cards and landed rows, and ticket ids were never linked. Links open in a new tab and render only for https:// URLs taken from the payload. No tracker or forge host is hardcoded.
  • bin/fm-bearings-board.sh accepts optional pr_url, ticket_url, and ticket fields on all four item types. It refuses a non-https or malformed URL and an empty ticket label. The payload contract comment documents the new fields.
  • The bearings SKILL.md tells the composer to fill pr_url, ticket_url, and ticket only when the task records already hold them, and never to guess or construct a URL. The render harness and the board/render tests cover the new links and the validation rules, using only placeholder data.

🤖 Generated with Claude Code

Risk Assessment

✅ Low: The change adds optional ticket and PR link fields. The validator accepts only https URLs, and the template builds each link from the payload with no hardcoded host, adding a new-tab anchor that has rel=noopener. The fix round now puts ticket links before PR links on every card and row, and the test data holds only placeholder values (ABC-, tracker.example, forge.example), with no employer data.

Testing

First I ran the existing board render test. It passed, including the new check that every list links tickets and PRs. Then I ran the real fm-bearings-board.sh build in a throwaway lab home on a fictional payload and loaded the built board in Chromium. Every card and row placed the ticket link before the PR link, used the payload URL, opened it in a new tab with rel=noopener, and labelled PRs with their number (a trailing slash was handled). A ticket with no label showed as "ticket", and the row with no URLs showed no links. The build refused all five malformed-link payloads and left the existing board byte-identical. The employer-data check was a static scan of the diff and commits, not a live run, so it is recorded as untested; the scan found only placeholder hosts (tracker.example, forge.example) and ABC-* ids. lavish-axi was replaced with a stub so the operator's shared Lavish server was never touched. That stub only arms the session and does not change what the board renders. I captured screenshot evidence, and the lab was fully torn down.

  • Live validation: ✅ go - 6 of 7 scenarios driven live against the product
Scenario Result Live Evidence
Underway row with a ticket and PR shows a ticket link to the tracker and a #N PR link ✅ pass live board-links-full.png; DOM query: DEMO-103 -> https://tracker.example.com/issue/DEMO-103, #43 -> .../pull/43
Charted Next row shows a ticket link and a PR link; a ticket with no label shows as 'ticket' ✅ pass live board-links-full.png Charted Next row: 'ticket' and '#45' links
Recently Landed row links both the ticket and the PR, and a trailing-slash PR URL still shows #44 ✅ pass live board-links-full.png Recently Landed: DEMO-104 and #44
Merge and decision cards in Captain's Call both show the ticket link before the PR link ✅ pass live DOM query order on bb-call cards: DEMO-101 then PR/41; DEMO-102 then PR/42
A row with no ticket or PR URL shows no links (no guessed or constructed URL) ✅ pass live 'Unlinked scout' Underway row has no <a> in the DOM query or the screenshot
Adversarial: a javascript:, http:, ftp: or non-string link, or an empty ticket label, is refused and the existing board is left unchanged ✅ pass live lab-refusals.log: 5 builds exit=1, board SHA unchanged
No employer data (company, repos, people, ticket ids, PR URLs, tracker workspace) in the change headed to the firstmate PR ⏸️ untested no The earlier payload checked this only with a static scan of the diff and commit messages (leak-scan.log), not a live run. That scan found only placeholder hosts and ABC-* ids, but it does not count as…
Evidence: Fictional lab payload used for the build

Source: Fictional lab payload used for the build

{
  "schema": "fm-bearings-board.v1",
  "home": "lab-home",
  "generated": "2026-09-25T00:00Z",
  "prs_live": true,
  "captains_call": [
    {
      "key": "merge-demo",
      "type": "merge",
      "id": "task-merge",
      "repo": "example-repo",
      "title": "Merge the widget refactor",
      "risk": "low",
      "detail": "CI green",
      "pr_url": "https://github.com/example-org/example-repo/pull/41",
      "ticket": "DEMO-101",
      "ticket_url": "https://tracker.example.com/issue/DEMO-101",
      "options": [
        {
          "value": "merge",
          "label": "Merge"
        },
        {
          "value": "hold",
          "label": "Hold"
        }
      ]
    },
    {
      "key": "decide-demo",
      "type": "decision",
      "id": "task-decide",
      "repo": "example-repo",
      "title": "Pick a cache strategy",
      "about": "cache",
      "decide": "which",
      "options": [
        {
          "value": "lru",
          "label": "LRU"
        },
        {
          "value": "ttl",
          "label": "TTL"
        }
      ],
      "ticket": "DEMO-102",
      "ticket_url": "https://tracker.example.com/issue/DEMO-102",
      "pr_url": "https://github.com/example-org/example-repo/pull/42",
      "recommend_value": "lru"
    }
  ],
  "underway": [
    {
      "id": "task-uw",
      "name": "DEMO-103 Wire the export button",
      "repo": "example-repo",
      "state": "running",
      "doing": "implementing",
      "kind": "ship",
      "ticket": "DEMO-103",
      "ticket_url": "https://tracker.example.com/issue/DEMO-103",
      "pr_url": "https://github.com/example-org/example-repo/pull/43"
    },
    {
      "id": "task-uw-nolink",
      "name": "Unlinked scout",
      "repo": "example-repo",
      "state": "running",
      "doing": "reading",
      "kind": "scout"
    }
  ],
  "landed": [
    {
      "id": "task-ld",
      "repo": "example-repo",
      "what": "Fix the login redirect",
      "owner": "crew",
      "ticket": "DEMO-104",
      "ticket_url": "https://tracker.example.com/issue/DEMO-104",
      "pr_url": "https://github.com/example-org/example-repo/pull/44/"
    }
  ],
  "charted": [
    {
      "id": "task-ch",
      "repo": "example-repo",
      "title": "Add dark mode",
      "reason": "queued",
      "dispatchable": true,
      "filed": "2026-09-20",
      "ticket_url": "https://tracker.example.com/issue/DEMO-105",
      "pr_url": "https://github.com/example-org/example-repo/pull/45"
    }
  ],
  "charted_more": 0,
  "charted_warning_more": 0
}
Evidence: Lab build transcript

Source: Lab build transcript

$ fm-bearings-board.sh build payload.json
board: /var/folders/wq/_h6f0f796zd6cd259vf7sm8h0000gn/T//fm-lab.6QVE8y/.lavish/bearings-board.html
session:
  status: opened
session: live
served: /var/folders/wq/_h6f0f796zd6cd259vf7sm8h0000gn/T//fm-lab.6QVE8y/.lavish/bearings-board.html
bound: lavish-fea1719b58af5249
armed: lavish-fea1719b58af5249
exit=0
Evidence: Malformed link payloads refused, board unchanged

Source: Malformed link payloads refused, board unchanged

$ build with .underway[0].ticket_url="javascript:alert(1)"
fm-bearings-board: board data does not satisfy fm-bearings-board.v1: /var/folders/wq/_h6f0f796zd6cd259vf7sm8h0000gn/T//fm-lab.6QVE8y/bad.json
exit=1
$ build with .underway[0].ticket_url="http://tracker.example.com/issue/DEMO-103"
fm-bearings-board: board data does not satisfy fm-bearings-board.v1: /var/folders/wq/_h6f0f796zd6cd259vf7sm8h0000gn/T//fm-lab.6QVE8y/bad.json
exit=1
$ build with .charted[0].pr_url="ftp://example.com/pull/45"
fm-bearings-board: board data does not satisfy fm-bearings-board.v1: /var/folders/wq/_h6f0f796zd6cd259vf7sm8h0000gn/T//fm-lab.6QVE8y/bad.json
exit=1
$ build with .landed[0].ticket=""
fm-bearings-board: board data does not satisfy fm-bearings-board.v1: /var/folders/wq/_h6f0f796zd6cd259vf7sm8h0000gn/T//fm-lab.6QVE8y/bad.json
exit=1
$ build with .captains_call[1].ticket_url=42
fm-bearings-board: board data does not satisfy fm-bearings-board.v1: /var/folders/wq/_h6f0f796zd6cd259vf7sm8h0000gn/T//fm-lab.6QVE8y/bad.json
exit=1
existing board unchanged after all refusals: 17ac9507582afc314523a064a8ed1a53d48c6b41  /var/folders/wq/_h6f0f796zd6cd259vf7sm8h0000gn/T//fm-lab.6QVE8y/.lavish/bearings-board.html
Evidence: Employer-data leak scan of the diff and commits

Source: Employer-data leak scan of the diff and commits

$ git diff f54aa00 HEAD + git log f54aa00..HEAD, case-insensitive scan for employer identifiers
138:       and (optional_string("freeform_hint"))
418:Adithya Jayasankar adithya.j@adithya321.me
421:Adithya Jayasankar adithya.j@adithya321.me
--- all URLs/hosts in added lines:
http://tracker.example/issue/ABC-1
https://forge.example/org/sample/pull/10
https://forge.example/org/sample/pull/41
https://forge.example/org/sample/pull/42
https://forge.example/org/sample/pull/43
https://forge.example/org/sample/pull/44
https://forge.example/org/sample/pull/45
https://forge.example/org/sample/pull/7
https://forge.example/org/sample/pull/8
https://forge.example/org/sample/pull/9
https://tracker.example/issue/ABC-101
https://tracker.example/issue/ABC-102
https://tracker.example/issue/ABC-103
https://tracker.example/issue/ABC-104
https://tracker.example/issue/ABC-105
https://tracker.example/issue/ABC-123
https://tracker.example/issue/ABC-124
https://tracker.example/issue/ABC-125
https://tracker.example/issue/ABC-126
https://tracker.example/issue/ABC-127
--- ticket-shaped ids in added lines:
ABC-1
ABC-101
ABC-102
ABC-103
ABC-104
ABC-105
ABC-123
ABC-124
ABC-125
ABC-126
ABC-127
Evidence: Render test output

Source: Render test output

ok - every board list links a supplied ticket and PR in a new tab, and no link without a URL
ok - an underway row leads with the task name and still reports its run status
ok - an underway identifier label is not replaced by run status
ok - charted next renders the most recently filed work first
ok - charted rows with no filed date follow the dated rows in payload order
ok - a warning row badges needs repair while queued work keeps waiting
ok - the charted next count counts queued work only, and still renders warnings
ok - a warning-only board reports nothing queued and still shows the warning
ok - omitted warnings remain separate from omitted queued work
ok - an omitted kind renders exactly as queued work always did
exit=0
Evidence: Links rendered in the browser (DOM query summary)
bb-call merge: DEMO-101 -> tracker, then PR full URL /pull/41
bb-call decision: DEMO-102 -> tracker, then PR /pull/42
bb-underway: DEMO-103 -> tracker, #43 -> PR; 'Unlinked scout' row: no links
bb-landed: DEMO-104 -> tracker, #44 -> .../pull/44/
bb-charted: 'ticket' (no label) -> tracker, #45 -> PR
all target=_blank rel=noopener

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • ℹ️ .agents/skills/bearings/assets/board-template.html:522 - Captain's Call cards show the two links in different orders: merge cards put the PR link before the ticket link (board-template.html:522-525), while decision cards put the ticket first (board-template.html:532-533). Rows always put the ticket first (appendRowLinks, :450-454). The two card branches also repeat the same link-append code. Building one list in a single order, e.g. [ticketLink(...), link(... pr_url ...)], after the type branch would make the order consistent and remove the repetition. The test at tests/fm-bearings-board-render.test.sh (the calls[] href expectation) would need its merge-card order changed to match. This only affects display order.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 6 of 7 scenarios driven live against the product
Scenario Result Live Evidence
Underway row with a ticket and PR shows a ticket link to the tracker and a #N PR link ✅ pass live board-links-full.png; DOM query: DEMO-103 -> https://tracker.example.com/issue/DEMO-103, #43 -> .../pull/43
Charted Next row shows a ticket link and a PR link; a ticket with no label shows as 'ticket' ✅ pass live board-links-full.png Charted Next row: 'ticket' and '#45' links
Recently Landed row links both the ticket and the PR, and a trailing-slash PR URL still shows #44 ✅ pass live board-links-full.png Recently Landed: DEMO-104 and #44
Merge and decision cards in Captain's Call both show the ticket link before the PR link ✅ pass live DOM query order on bb-call cards: DEMO-101 then PR/41; DEMO-102 then PR/42
A row with no ticket or PR URL shows no links (no guessed or constructed URL) ✅ pass live 'Unlinked scout' Underway row has no <a> in the DOM query or the screenshot
Adversarial: a javascript:, http:, ftp: or non-string link, or an empty ticket label, is refused and the existing board is left unchanged ✅ pass live lab-refusals.log: 5 builds exit=1, board SHA unchanged
No employer data (company, repos, people, ticket ids, PR URLs, tracker workspace) in the change headed to the firstmate PR ⏸️ untested no The earlier payload checked this only with a static scan of the diff and commit messages (leak-scan.log), not a live run. That scan found only placeholder hosts and ABC-* ids, but it does not count as…
  • bash tests/fm-bearings-board-render.test.sh (existing render test: builds through fm-bearings-board.sh build and renders under the DOM harness, including the ticket/PR link test with the ticket-then-PR order)
  • Made a throwaway lab home with bin/fm-lab-home.sh create, then ran a real bin/fm-bearings-board.sh build on a fictional payload: 2 Captain's Call cards (one merge, one decision), 2 Underway rows (one linked, one with no links), 1 Recently Landed row with a trailing-slash PR URL, and 1 Charted Next row with a ticket URL but no label
  • Served the built bearings-board.html on 127.0.0.1 and loaded it in Playwright Chromium; collected every <a> element (section, text, href, target, rel) with a DOM query and took a full-page screenshot
  • Adversarial: ran build with ticket_url=javascript:alert(1), an http:// ticket_url, an ftp:// pr_url, an empty ticket label and a numeric ticket_url; checked that each exits 1 and that the existing board's SHA is unchanged
  • Leak scan: case-insensitive grep of git diff f54aa00 HEAD and the commit messages for employer identifiers, plus a list of every URL and ticket-shaped id in the added lines
  • Teardown: stopped the http server and the lab listener, then ran rm -rf on the lab and on .playwright-mcp
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Board rows and Captain's Call cards accept optional https pr_url and
ticket_url fields plus a ticket label. The validator refuses anything
that is not an https URL, and the template renders each supplied URL
as a new-tab link without assuming any tracker or forge host.
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