feat(work-items): Jira Cloud adapter for the work-item-tracker seam - #857
Conversation
…acker seam Add a bundled `jira` adapter (tools/work-item-tracker/adapters/jira/) behind the work-item-tracker seam, alongside the shipped github and local-markdown adapters; GitHub stays the default. Scoped to the read/resolve path per the #379 maintainer decisions. Consume-only by default (issue #379 hard constraint): get-item, list-items, and capabilities are supported; every coordination write verb and list-sub-items are declared false in the manifest and exit 6 at the core gate, so no code path creates, claims, or mutates a Jira ticket. Reads use Jira Cloud REST v3 (GET /rest/api/3/issue/{key}, POST /rest/api/3/search/jql with nextPageToken pagination) over curl; Basic auth email + API token, the token referenced by env-var name only and fed to curl via a stdin config so it never enters argv. Normalization maps statusCategory to open/closed, single assignee to accountId, labels verbatim, issue type, open-only blocked_by_count under the configured blocker link type, fields.parent to parent_id, and the browse URL. The blocker link type and the exact "done" statusCategory key are configurable override seams defaulting to the documented Jira standards, so the adapter is independent of the two live-instance facts (#4, #6) deferred to the work-laptop pass. Config lives in config.jira (site, project_keys[], auth_email, auth_env), read by the adapter itself rather than by growing the shared binding.sh. Adds a jira conformance binding that runs fully offline in CI (the consume-only manifest makes every suite-exercised path pre-network) plus curl-mocked unit tests for the read path. /work-items:setup gains jira as a selectable provider; CONTRACT.md and a per-adapter README document the surface. Closes #379 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Independent review found a JQL-injection CRITICAL: list-items built the `project in (...)` and `statusCategory in (...)` clauses by string assembly, wrapping keys in quotes via jq interpolation WITHOUT escaping embedded quotes, so a --repo project key (or a configured key) containing `"` could break out of the literal and append arbitrary JQL. Fix by allowlist (reject the unexpected, safer than escaping): every value interpolated into JQL is charset-validated before assembly. Configured project_keys and done_category_keys are validated at config load (exit 3 on a bad value); the --repo override key is validated at parse time (exit 2). get-item likewise rejects an id whose project key is grammar-valid but not a real Jira key, up front (exit 2), instead of surfacing an opaque normalizer parse failure. Both allowlists reject before any HTTP call. Tests now assert the LITERAL JQL payload sent to the mocked curl (captured argv), including hostile-input cases proving --repo and config keys with embedded quotes are refused with no request made, plus the null-accountId assignee edge (-> []). Also: correct the misleading "quoted safely" comment to describe the actual validate-then-assemble mechanism, and drop a hardcoded date from the jira conformance binding in favor of the issue ref. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 5m 26s —— View job PR Review: feat(work-items): Jira Cloud adapter for the work-item-tracker seamTasks
SummarySolid implementation of the read/resolve-only Jira adapter. JQL injection defense is thorough and well-tested. Auth hygiene (token via stdin curl config, never in argv) is correct. The normalization logic, exit-code mapping, pagination, and offline test suite are all high quality. One medium-severity gap and a few low-severity / nit items below. Medium —
|
|
Claude finished @kyle-sexton's task in 4m 39s —— View job Security Review — PR #857 (Jira Cloud adapter)
Reviewed files: IMPORTANT —
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 705590d649
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… non-array config Address two Codex review threads on the jira adapter (PR #857). P1 (information scoping): reads were bounded only by site, so `--repo <site>/OTHER` and a `get-item` id for any token-visible project could read tickets outside the binding's declared project set. config.jira.project_keys is now enforced as the read AND authorization boundary: get-item refuses an out-of-scope project id, and --repo may only narrow within project_keys (never widen) — both exit 2 before any request. P2 (config validation): a scalar `project_keys:"ABC"` satisfied the bare `length>0` check (string length 3), then `.[]`/`map(...)` jq-errored into an empty `project in ()` clause this non-`set -e` script ignored. project_keys is now type-checked as a non-empty array (and done_category_keys as an array) at config load — a malformed value exits 3 per the contract. Tests: out-of-scope get-item id and --repo → exit 2 with no HTTP call; non-array project_keys → exit 3; a direct wit_jira_project_in_scope unit check. CONTRACT.md and the adapter README document the scope-as-boundary rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0ae80cc5fe
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Bring the branch up to date with main (12 commits, incl. work-items 0.18.2 #365 gh-bot resolution fix). Resolved two conflicts: kept the 0.19.0 version bump (a minor above main's 0.18.2 patch) in plugin.json, and interleaved the CHANGELOG so [0.19.0] (Jira adapter) precedes main's [0.18.2] entry. CONTRACT.md auto-merged (main's Identity-routing edit vs this branch's jira section — disjoint).
|
Claude finished @kyle-sexton's task in 2m 33s —— View job PR Review Update — feat(work-items): Jira Cloud adapterTasks
Review at 8c0f99aFixed in 0ae80cc — confirmed ✓Both issues flagged by Codex's first review pass are fixed and well-tested:
Non-array Still open — three issues remain unaddressed1.
printf 'header = "Authorization: Basic %s"\nurl = "%s"\n' "$auth" "$url" | ...
The project-key and status-category key patterns already follow the allowlist approach at readonly WIT_JIRA_SITE_RE='^[A-Za-z0-9][A-Za-z0-9._-]*$'Validate 2.
local token="${!WIT_JIRA_AUTH_ENV:-}"Two problems:
Add an identifier allowlist check at config load time, validating readonly WIT_JIRA_AUTH_ENV_RE='^[A-Z][A-Z0-9_]{1,63}$'Validate after the non-empty check and exit 3. Empty The new array type-check correctly rejects a scalar
The binding's default (when Add a length check alongside the type check: [[ "$(jq -r 'type' <<<"$WIT_JIRA_DONE_KEYS")" == "array" ]] &&
[[ "$(jq -r 'length' <<<"$WIT_JIRA_DONE_KEYS")" -gt 0 ]] ||
missing+=" config.jira.done_category_keys(non-empty array)"Nit —
|
|
Claude finished @kyle-sexton's task in 4m 29s —— View job Security Review — PR #857 (Jira Cloud adapter, HEAD
|
Resolves conflicts on source-control and work-items plugin.json/CHANGELOG.md: main advanced source-control to 0.15.8 (#839, merged) and work-items to 0.18.2 (#826, merged) since this branch was last rebased. Also re-checked live open PRs at merge time and found work-items now carries a new open claim, #857 at 0.19.0 (Jira adapter) — re-bumped this branch's work-items claim from 0.18.3 to 0.19.1 to stay ahead of it. source-control's 0.15.10 and claude-ops's 0.17.3 remain valid (still one past #840/#860's 0.15.9 and #844/#860's 0.17.2 open claims, respectively). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1V3gkrfSf75isB8MiDy3o
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8c0f99aa90
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…y done keys Second Codex review round on PR #857 — three config-validation gaps, all fixed at config load (exit 3) before any request. P1 (credential egress): config.jira.site is the host the Basic-auth token is sent to, and the binding is tracked (PR-modifiable). A binding could point site at an arbitrary host and exfiltrate the token. site is now validated to be a bare hostname (no scheme/path/@/port/control chars — blocks URL-structure smuggling) AND an *.atlassian.net Cloud host unless the binding sets config.jira.allow_custom_domain: true (deny-by-default on credential egress, with an explicit reviewable opt-in for custom-domain tenants). P2 (auth_env): auth_env is dereferenced via ${!name}; an invalid identifier (e.g. "JIRA-TOKEN") aborts bash on first read. It is now required to be a valid shell variable name at config load. P2 (done_category_keys): an explicitly empty array passed the type check, then built `statusCategory not in ()` — invalid JQL Jira 400s — surfaced as a confusing exit 1. Now required non-empty (the default remains when the key is absent). Tests cover each: site with URL structure and non-atlassian host without opt-in → exit 3, custom-domain opt-in accepted; invalid auth_env → exit 3; empty done_category_keys → exit 3. CONTRACT.md and the adapter README document the credential-egress guard and the auth_env/site rules. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 4m 18s —— View job Security Review — PR #857 (Jira Cloud adapter, HEAD
|
|
Claude finished @kyle-sexton's task in 2m 45s —— View job PR Review Update — feat(work-items): Jira Cloud adapter (HEAD
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dce959c1e8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Third Codex review round on PR #857 (P2). An empty or non-string config.jira.blocked_by_link_type override matched no issuelink, so blocked_by_count was silently 0 for every issue and list-frontier would surface actually-blocked tickets (silent-wrong). blocked_by_link_type, when present, is now validated as a non-empty string at config load (exit 3); absent still falls back to the documented "Blocks" default. Tests: empty and non-string overrides → exit 3, a valid override accepted. CONTRACT.md notes the rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 4m 23s —— View job PR Review Update — feat(work-items): Jira Cloud adapter (HEAD
|
|
Claude finished @kyle-sexton's task in 3m 39s —— View job Security Review — PR #857 (Jira Cloud adapter, HEAD
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a8260b74e6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…/non-string) Fourth Codex round on PR #857 (P2). done_category_keys:[""] (and the same for project_keys) slipped through: the bash line-loop skipped empty entries, and command-substitution stripped a trailing "" before the loop even saw it, so an element-empty array passed the non-empty-array check and built statusCategory not in ("") / project in (""). Replace both bash validation loops with jq-side element validation: every element of project_keys and done_category_keys is checked in jq against the same anchored allowlist (test()), with a type guard rejecting non-string elements — no element can evade validation via newline stripping. Tests: [""] in either array, and a non-string project key element, all → exit 3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 3m 31s —— View job PR Review Update — feat(work-items): Jira Cloud adapter (HEAD
|
|
Claude finished @kyle-sexton's task in 3m 18s —— View job Security Review — PR #857 (Jira Cloud adapter, HEAD
|
| Finding | Fixed in | Verification at HEAD |
|---|---|---|
JQL injection via project_keys/--repo |
0ae80cc / 705590d |
WIT_JIRA_PROJECT_KEY_RE allowlist applied at config load (lines 192–193) and at --repo parse (list-items.sh:46–47). wit_jira_project_in_scope enforces scope on both read paths before any HTTP call. |
WIT_JIRA_SITE curl config injection (newline → proxy = ...) |
dce959c |
WIT_JIRA_HOSTNAME_RE='^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$' blocks scheme, userinfo, port, path segments, and control chars at lines 170–174. .atlassian.net suffix check adds deny-by-default credential egress. |
WIT_JIRA_AUTH_ENV indirect expansion crash / CI secret exfiltration |
dce959c |
WIT_JIRA_ENV_NAME_RE='^[A-Za-z_][A-Za-z0-9_]*$' validated at lines 177–178 before ${!WIT_JIRA_AUTH_ENV:-} at line 226. |
Non-array project_keys/done_category_keys |
0ae80cc / dce959c |
Type + length check at lines 149–157. |
Empty done_category_keys: [] → invalid JQL |
dce959c |
Caught by same length check. |
blocked_by_link_type: "" → silent blocked_by_count: 0 |
a8260b7 |
jq type discrimination at lines 184–185 rejects empty-string and non-string while accepting null (falls back to default). |
[""] element in key arrays → JQL bypass |
d586ab6 |
jq element validation at lines 191–195 (this commit). |
The curl stdin config printf at line 258 remains structurally safe: $auth contains only base64 characters (newlines stripped by tr -d '\n'); $url composes WIT_JIRA_SITE (hostname-regex validated), a hardcoded path literal, and $WIT_JIRA_FIELDS (hardcoded readonly constant) — none of which can contain " or newline.
WIT_ID_NUMBER (get-item.sh:34) is constrained to [0-9]+ by the shared ID grammar, so the native key $WIT_ID_REPO-$WIT_ID_NUMBER carries no URL path injection risk.
wit_jira_project_in_scope (common.sh:219) uses --arg k to pass the project key to jq's index(), so the key value is never interpolated into jq program text.
Previously noted SUGGESTION items — unchanged, no new findings
Two SUGGESTION-level items from prior passes remain by design and do not represent newly introduced risk:
WIT_JIRA_CURLseam executes an env-controlled binary (common.sh:58): intentional testability seam, correctly quoted, no word-splitting risk. CI policy (not code) is the right mitigation.allow_custom_domain: trueroutes credentials to any hostname: documented opt-in escape hatch, requires explicit binding modification, visible in PR diff. Design decision, not a code defect.
No security issues found at HEAD d586ab6. The fixup chain (0ae80cc → dce959c → a8260b7 → d586ab6) has addressed every IMPORTANT and CRITICAL finding raised across all review passes. The PR is ready from a security standpoint.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d586ab69bf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…sue body Fifth Codex round on PR #857 (P2). A search or issue GET that returns 2xx but a body that is not the expected shape — malformed JSON, or an error envelope an intermediary returned with a 200 — was swallowed: list-items' jq produced nothing, page_count went empty, the loop broke, and an empty success envelope was emitted, silently telling list-frontier there is no work; get-item instead crashed the normalizer into a misleading exit 5. Both verbs now validate the response shape after the status check — list-items requires an object with an `issues` array, get-item an object with a string `.key` — and exit 8 (provider unavailable) with a clear message otherwise. Tests: a 200 error envelope → exit 8 for each verb. (The companion "reject empty project_keys element" thread is already covered by the prior commit's jq element validation.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 4m 10s —— View job PR Review Update — feat(work-items): Jira Cloud adapter (HEAD
|
|
Claude finished @kyle-sexton's task in 2m 54s —— View job Security Review — PR #857 (Jira Cloud adapter, HEAD
|
| Area | Status |
|---|---|
| JQL injection — project keys and done-category keys | Fixed (705590d/0ae80cc): allowlist-validated via WIT_JIRA_PROJECT_KEY_RE/WIT_JIRA_CATEGORY_KEY_RE before any JQL assembly; verified in jq element-by-element in d586ab6 so [""] and non-string elements can't slip through |
WIT_JIRA_SITE curl config injection (newline → proxy = …) |
Fixed (dce959c): WIT_JIRA_HOSTNAME_RE rejects scheme, path, userinfo, port, and control chars; .atlassian.net suffix guard adds deny-by-default credential egress |
WIT_JIRA_AUTH_ENV indirect expansion crash / CI secret exfiltration |
Fixed (dce959c): WIT_JIRA_ENV_NAME_RE applied at config load before ${!WIT_JIRA_AUTH_ENV:-} at line 226 |
Non-array project_keys / done_category_keys |
Fixed (0ae80cc/dce959c): type + length check at config load |
Empty done_category_keys: [] → invalid JQL |
Fixed (dce959c): length check catches [] |
blocked_by_link_type: "" / [] → silent blocked_by_count: 0 |
Fixed (a8260b7): direct binding read with null/string discrimination at lines 184–185 |
Key-array element validation ([""] bypass) |
Fixed (d586ab6): validation moved inside jq, eliminating bash newline-stripping escape |
2xx without issues array → silent empty envelope |
Fixed (161cf89): shape check at list-items.sh:91–94 exits 8 |
| Token never in argv | Correct throughout: wit_jira_http feeds credential via curl stdin config (-K -) |
WIT_ID_NUMBER digits-only |
Correct throughout: ID grammar constrains to [0-9]+, no URL path injection |
No new attack surfaces introduced in 161cf89. The commit is a targeted two-site fix (shape check in list-items.sh and get-item.sh) with tests; no new config fields, no new external input paths, no changes to the auth or JQL layers.
Non-security items (out of scope for this review, noted for completeness)
done_category_keys: false → silently defaults (Codex P2 from d586ab6, correctness only): jq's // alternative operator activates on both null and false, so a binding with done_category_keys: false falls through to the default ["done","completed"] at line 138 before the type/length validation at lines 155–157 runs on the already-defaulted value. The effect is benign (the default is applied, not a wrong classification), but inconsistent: blocked_by_link_type: false is caught by a separate raw-binding read at lines 184–185 that explicitly checks the JSON type. Not a security vulnerability; the result of the silence is correct default behavior.
plugin.json description still omits jira (plugin.json:5): cosmetic, no security impact.
The PR is clean from a security standpoint. All IMPORTANT and CRITICAL findings from the multi-pass review trail are resolved with consistent allowlist patterns, layered defences, and targeted test coverage.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 161cf89e74
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ent hygiene Sixth Codex round (P2) plus two CI hygiene gates on PR #857. - done_category_keys: false — jq's `//` collapsed an explicit false/null into the default, silently applying the default classification for a typo'd override. Read the key by presence now (has()) instead of `// default`, so a present-but-invalid value (false, null, non-array, empty, empty element) reaches the config-error path (exit 3). Test added. - exec-bit hygiene: the new adapter/conformance *.sh files were committed 100644; set them 100755 to match every other tracker-seam script (they are entry scripts). - comment-hygiene: removed tracker issue-number references from code comments (version control owns that history) — rephrased to describe the constraint/seam directly. Also built the default done-key array via jq rather than a quoted shell literal (SC2089/2090). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 3m 59s —— View job PR Review Update — feat(work-items): Jira Cloud adapter (HEAD
|
|
Claude finished @kyle-sexton's task in 4m 56s —— View job Security Review — PR #857 (Jira Cloud adapter, HEAD
|
| Area | Status at HEAD |
|---|---|
| JQL injection — project keys, done-category keys | Fixed: allowlist-validated via WIT_JIRA_PROJECT_KEY_RE/WIT_JIRA_CATEGORY_KEY_RE before JQL assembly; element-level check runs in jq so [""] and non-string elements are caught |
WIT_JIRA_SITE curl config injection (\nproxy = …) |
Fixed: WIT_JIRA_HOSTNAME_RE blocks scheme, path, userinfo, port, and control chars; .atlassian.net suffix adds deny-by-default credential egress |
WIT_JIRA_AUTH_ENV indirect expansion crash / CI secret exfiltration |
Fixed: WIT_JIRA_ENV_NAME_RE applied before ${!WIT_JIRA_AUTH_ENV:-} at line 237; combined with site validation the exfiltration chain is broken at both ends |
Non-array or empty project_keys / done_category_keys |
Fixed: type + length checks at lines 160–168 |
done_category_keys: false / null silent default collapse |
Fixed (2ea1fbb): has() presence read; both values now exit 3 |
blocked_by_link_type empty / non-string / false |
Fixed (a8260b7): direct binding read via the $b == null ? default : type check jq expression at lines 195–196 correctly rejects false, [], "", non-string while accepting absent/null (→ default) and a non-empty string |
Key-array element validation — [""] and non-string elements |
Fixed (d586ab6): element validation runs inside jq, eliminating the bash newline-stripping escape |
2xx without issues array / issue object — silent empty envelope |
Fixed (161cf89): shape checks exit EX_UNAVAILABLE in both list-items and get-item before the normalizer runs |
| Token never in argv | Correct throughout: wit_jira_http feeds credential via `printf … |
WIT_ID_NUMBER digits-only |
Correct throughout: ID grammar constrains to [0-9]+, no URL path injection via issue number |
| Project-key scope enforcement | Fixed (0ae80cc): wit_jira_project_in_scope gates both get-item (id project) and list-items --repo (override project) before any HTTP call |
Previously noted SUGGESTION items — unchanged, no new findings
Normalizer failure on malformed issue key (Codex P2 from 161cf89, still open): a Jira response where an issue's .key doesn't match the expected PROJECT-NUMBER format would cause capture() to fail; the normalizer jq exits non-zero inside the { while } | jq -c -s pipe, silently dropping that issue from output while emitted is still incremented. This is a correctness concern for robustness against unexpected Jira responses, but not a security vulnerability under the existing threat model: an adversarial response of this kind requires the Jira server (or an in-path proxy) to return non-standard content, which is foreclosed for legitimate *.atlassian.net tenants by the site-validation guard; the allow_custom_domain: true escape hatch already concedes that a custom-domain server is fully trusted.
plugin.json description omits jira (plugin.json:5): still reads "github and local-markdown adapters". Cosmetic; no security impact.
The PR is clean from a security standpoint. Every security-critical finding from the multi-pass review trail — JQL injection, site curl-config injection, auth_env exfiltration and bad-substitution crash, empty/false/non-array done_category_keys, empty blocked_by_link_type, key-array element bypass, and 2xx error-envelope silent collapse — is addressed, tested, and confirmed at HEAD.
Seventh Codex round on PR #857 (P2). The normalizer's key parser (capture) emits an EMPTY result on a non-matching key rather than erroring, so an issue with an unparseable key was silently dropped and an all-invalid search page collapsed to items:[] with exit 0, hiding real work from list-frontier. qualify now raises an explicit jq error on an unparseable key. list-items normalizes each page atomically (capture + exit check) and get-item captures its normalize, both mapping a normalizer failure to exit 8. Tests: an un-normalizable key -> exit 8 each. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 1s —— View job PR Review Update — feat(work-items): Jira Cloud adapter (HEAD
|
| Finding | Status |
|---|---|
| JQL injection — project keys + done-category keys | ✓ Fixed: allowlist-validated in jq element-by-element; [""] and non-string elements caught |
WIT_JIRA_SITE curl config injection (\nproxy = …) |
✓ Fixed: WIT_JIRA_HOSTNAME_RE + .atlassian.net deny-by-default guard |
WIT_JIRA_AUTH_ENV indirect expansion crash / CI secret exfiltration |
✓ Fixed: WIT_JIRA_ENV_NAME_RE validated before ${!WIT_JIRA_AUTH_ENV:-} |
Non-array / empty project_keys or done_category_keys |
✓ Fixed: type + length check at config load |
done_category_keys: false silent default collapse |
✓ Fixed: presence-based read (has()) so false reaches type check → exit 3 |
blocked_by_link_type: "" / [] → silent blocked_by_count: 0 |
✓ Fixed: direct binding read with null/string/other discrimination |
2xx without issues array / issue object → silent empty envelope |
✓ Fixed: shape checks exit 8 in both verbs before normalizer runs |
| Normalizer failure on malformed key → silent issue drop | ✓ Fixed (1b58f34): atomic buffer + explicit exit check in both verbs |
| Project-key scope enforcement (read boundary) | ✓ Fixed: wit_jira_project_in_scope gates both get-item and list-items --repo |
| Token never in argv | ✓ Correct throughout: credential fed via `printf … |
Overall assessment
After ten commits and six review passes, the PR is in excellent shape. The 1b58f34 fix closes the last open correctness finding. The implementation is security-reviewed to a high standard — layered defences on the credential and JQL paths, consistent allowlist patterns with clear diagnostics, and thorough offline test coverage. The only outstanding item is the one-word plugin.json description nit, which does not block merge.
|
Claude finished @kyle-sexton's task in 2m 41s —— View job Security Review — PR #857 (Jira Cloud adapter, HEAD
|
| Area | Status at HEAD |
|---|---|
| JQL injection — project keys, done-category keys | Fixed: allowlist-validated via WIT_JIRA_PROJECT_KEY_RE/WIT_JIRA_CATEGORY_KEY_RE; element-level check in jq so [""] and non-string elements are caught |
WIT_JIRA_SITE curl config injection (\nproxy = …) |
Fixed: WIT_JIRA_HOSTNAME_RE blocks scheme, path, userinfo, port, and control chars; .atlassian.net suffix adds deny-by-default credential egress |
WIT_JIRA_AUTH_ENV indirect expansion crash / CI secret exfiltration |
Fixed: WIT_JIRA_ENV_NAME_RE applied before ${!WIT_JIRA_AUTH_ENV:-} at line 237; site validation breaks the exfiltration chain at the destination |
Non-array or empty project_keys / done_category_keys |
Fixed: type + length checks at config load |
done_category_keys: false / null silent default collapse |
Fixed: has() presence read ensures false reaches the type check and exits 3 |
blocked_by_link_type empty / non-string |
Fixed: direct binding type-and-length check at lines 195–196 |
Key-array element validation — [""] and non-string elements |
Fixed: element validation runs inside jq |
2xx without issues array / issue object — silent empty envelope |
Fixed: shape checks exit EX_UNAVAILABLE in both verbs before the normalizer runs |
| Normalizer silent drop on unparseable key | Fixed (1b58f34): keyparts raises jq error; both verb scripts wrap the normalizer call with || exit $EX_UNAVAILABLE |
| Token never in argv | Correct throughout: wit_jira_http feeds credential via printf … | curl -K - |
WIT_ID_NUMBER digits-only |
Correct throughout: ID grammar constrains to [0-9]+, no URL path injection |
| Project-key scope enforcement | Fixed: wit_jira_project_in_scope gates both get-item and list-items --repo before any HTTP call |
The PR is clean from a security standpoint. Every IMPORTANT and CRITICAL finding from the multi-pass review trail has been resolved with consistent allowlist patterns, layered defences, and targeted test coverage. No new attack surfaces were introduced in 1b58f34.
Re-syncs onto main after #860 (fleet-wide `shell: bash` declaration sweep) merged, which bumped source-control to 0.15.9, claude-ops to 0.17.2, and repo-hygiene to 0.4.5. Resolved conflicts on those three plugins' plugin.json and CHANGELOG.md; re-derived each held version against the new main plus a fresh live-open-PR check rather than assuming the prior numbers still held: - source-control 0.15.10: still one past main's 0.15.9 and #840's live 0.15.9 claim. - claude-ops 0.17.3: still one past main's 0.17.2 and #844's live 0.17.2 claim. - repo-hygiene 0.4.6: still one past main's new 0.4.5 (#860, now merged); no open-PR collision remains for this plugin. - work-items 0.19.1 and guardrails 0.9.6 were untouched by this merge (no conflict) and remain correct against #857/#861 (both still 0.19.0, open) and no open collision, respectively. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1V3gkrfSf75isB8MiDy3o
Satisfies the repo typos gate (crate-ci/typos) on the normalizer's error message. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 12s —— View job PR Review Update — feat(work-items): Jira Cloud adapter (HEAD
|
| Finding | Commit | Status |
|---|---|---|
| JQL injection — project keys + done-category keys | 705590d/0ae80cc |
✓ Allowlist-validated via WIT_JIRA_PROJECT_KEY_RE/WIT_JIRA_CATEGORY_KEY_RE before any JQL assembly |
project_keys scope as read/authorization boundary |
0ae80cc |
✓ wit_jira_project_in_scope gates both get-item (id project) and list-items --repo before any HTTP call |
Non-array project_keys/done_category_keys |
0ae80cc |
✓ Type + length check at config load |
WIT_JIRA_SITE curl config injection (\nproxy = …) |
dce959c |
✓ WIT_JIRA_HOSTNAME_RE blocks scheme, path, userinfo, port, and control chars; .atlassian.net suffix adds deny-by-default credential egress |
WIT_JIRA_AUTH_ENV indirect expansion crash / CI secret exfiltration |
dce959c |
✓ WIT_JIRA_ENV_NAME_RE applied before ${!WIT_JIRA_AUTH_ENV:-} |
Empty done_category_keys: [] → invalid JQL |
dce959c |
✓ Caught by length check at config load |
blocked_by_link_type empty/non-string → silent blocked_by_count: 0 |
a8260b7 |
✓ Direct binding read with null/string/type discrimination |
Key-array element validation — [""] and non-string elements |
d586ab6 |
✓ Element validation runs in jq; bash newline-stripping escape closed |
2xx without issues array / issue object — silent empty envelope |
161cf89 |
✓ Shape checks exit EX_UNAVAILABLE in both verbs before the normalizer runs |
done_category_keys: false silent default collapse |
2ea1fbb |
✓ has() presence read; false now reaches the type check and exits 3 |
| Normalizer silent drop on unparsable issue key | 1b58f34 |
✓ keyparts raises jq error on no match; both verbs wrap normalizer with || exit $EX_UNAVAILABLE |
| Spelling: unparseable → unparsable | ca3aa2b |
✓ Cosmetic; no behavioral impact |
| Token never in argv | Throughout | ✓ Credential fed exclusively via printf … | curl -K - |
WIT_ID_NUMBER digits-only |
Throughout | ✓ ID grammar constrains to [0-9]+; no URL path injection |
The PR is in excellent shape. The plugin.json description nit is the only remaining item — a cosmetic one-word fix that does not affect functionality, security, or correctness.
|
Claude finished @kyle-sexton's task in 1m 17s —— View job Security Review — PR #857 (Jira Cloud adapter, HEAD
|
| Control | Code location | Status |
|---|---|---|
WIT_JIRA_SITE hostname allowlist + .atlassian.net deny-by-default |
common.sh:87–88, 181–184 |
✓ intact |
WIT_JIRA_AUTH_ENV identifier allowlist before ${!name} deref |
common.sh:91, 188–189 |
✓ intact |
| JQL injection — element-level jq validation of project keys + done keys | common.sh:203–206 |
✓ intact |
done_category_keys presence-based read (not //) to catch false/null |
common.sh:145–149 |
✓ intact |
blocked_by_link_type non-empty string check |
common.sh:195–196 |
✓ intact |
Normalizer failure → loud exit (keyparts // error(...)) |
common.sh:331–332 |
✓ intact |
Token fed via printf … | curl -K -, never in argv |
common.sh:269–270 |
✓ intact |
The PR is clean from a security standpoint. The ca3aa2b commit is a one-word spelling fix in a diagnostic string; it introduces no new attack surface. Every security-critical finding raised across the multi-pass review trail has been addressed and remains addressed at this HEAD.
Keep the branch current with main (5 commits); clean auto-merge, no conflicts.
|
Claude finished @kyle-sexton's task in 1m 45s —— View job PR Review Update — feat(work-items): Jira Cloud adapter (HEAD
|
| Finding | Commit | Status |
|---|---|---|
| JQL injection — project keys + done-category keys | 705590d/0ae80cc |
✓ Allowlist-validated in jq element-by-element |
project_keys scope as read/authorization boundary |
0ae80cc |
✓ wit_jira_project_in_scope gates both read paths |
Non-array project_keys/done_category_keys |
0ae80cc |
✓ Type + length check at config load |
WIT_JIRA_SITE curl config injection (\nproxy = …) |
dce959c |
✓ Hostname regex + .atlassian.net deny-by-default |
WIT_JIRA_AUTH_ENV indirect expansion crash / CI secret exfiltration |
dce959c |
✓ Identifier regex before ${!WIT_JIRA_AUTH_ENV:-} |
Empty done_category_keys: [] → invalid JQL |
dce959c |
✓ Length check at config load |
blocked_by_link_type empty/non-string → silent blocked_by_count: 0 |
a8260b7 |
✓ Direct binding type discrimination |
Key-array element validation — [""] and non-string elements |
d586ab6 |
✓ Element validation in jq |
2xx without issues array / issue object — silent empty envelope |
161cf89 |
✓ Shape checks exit EX_UNAVAILABLE in both verbs |
done_category_keys: false silent default collapse |
2ea1fbb |
✓ has() presence read; false reaches type check → exit 3 |
| Normalizer silent drop on unparsable issue key | 1b58f34 |
✓ keyparts raises jq error; both verbs exit EX_UNAVAILABLE |
| Spelling: unparseable → unparsable | ca3aa2b |
✓ Cosmetic only |
The PR is in excellent shape. Once the plugin.json description is updated (one word), there are no outstanding findings.
|
Claude finished @kyle-sexton's task in 2m 23s —— View job Security Review — PR #857 (Jira Cloud adapter, HEAD
|
| Control | Location | Status |
|---|---|---|
WIT_JIRA_SITE hostname allowlist + .atlassian.net deny-by-default |
common.sh:87–88, common.sh:181–184 |
✓ intact |
WIT_JIRA_AUTH_ENV identifier allowlist before ${!name} deref |
common.sh:91, common.sh:188–189 |
✓ intact |
| JQL injection — element-level jq validation of project keys + done keys | common.sh:203–206 |
✓ intact |
done_category_keys presence-based read (not //) to catch false/null |
common.sh:145–149 |
✓ intact |
blocked_by_link_type non-empty string check |
common.sh:195–196 |
✓ intact |
Normalizer failure → loud exit (keyparts // error(...)) |
common.sh:331–332 |
✓ intact |
Token fed via printf … | curl -K -, never in argv |
common.sh:269–270 |
✓ intact |
Project-key scope enforcement (wit_jira_project_in_scope) |
common.sh:229–231 |
✓ intact |
2xx without issues array → exit 8 |
list-items.sh:91–94 |
✓ intact |
Atomic page normalization with || exit check |
list-items.sh:102–107 |
✓ intact |
2xx non-issue-object shape check in get-item |
get-item.sh:41–44 |
✓ intact |
The PR is clean from a security standpoint. The merge is a no-op for the jira adapter. Every IMPORTANT and CRITICAL finding from the twelve-commit, multi-pass review trail has been addressed and remains addressed at this HEAD.
|
Tower merge (overnight delegation, veto window open): A1 item, 23/23 green, 0 unresolved threads across 7 review rounds — every security/silent-wrong finding fixed on-branch (JQL-injection allowlist, credential-egress tenant guard, read-scope confinement, fail-loud normalization), consume-only enforced structurally, follow-up #852 filed for the pre-existing seam leak. This was generated by AI (control tower, session 6). |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b116507411
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| wit_jira_project_in_scope "$WIT_ID_REPO" || | ||
| wit_usage_error "project '$WIT_ID_REPO' is not in the binding's config.jira.project_keys (declared read scope)" |
There was a problem hiding this comment.
Re-check project scope after Jira resolves moved keys
This check only scopes the requested ID before the fetch; with a moved Jira issue, an old in-scope key such as SW2-1 can resolve to a current key in an undeclared project, and the normalizer will emit and expose that returned issue. Fresh evidence: the Jira Cloud GET issue docs state that issueIdOrKey performs moved-issue lookup and returns the found issue's current key (https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-get). Validate the project parsed from the response .key against config.jira.project_keys before emitting it.
Useful? React with 👍 / 👎.
#857 (Jira Cloud adapter) merged into main since this branch's last rebase, bumping work-items to 0.19.0. Resolved the resulting conflict, keeping this branch's 0.19.1 (already one past both the new main and #861's still-open 0.19.0 claim, so no further bump needed). Re-verified all 5 plugins fresh against current main and every live open PR (#840 0.15.10, #844 0.17.2, #861 0.19.0): source-control 0.15.11, claude-ops 0.17.3, repo-hygiene 0.4.6, and guardrails 0.9.6 all remain correct with no change required. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1V3gkrfSf75isB8MiDy3o
Resolve the work-items version/CHANGELOG collision with #857 (Jira adapter), which took work-items to 0.19.0 after this branch also claimed 0.19.0. Stack this branch's pipeline-shape SSOT bump to 0.20.0 with its own CHANGELOG section above #857's 0.19.0 (Jira adapter) entry; both entries preserved. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
#844 (claude-ops) merged into main at exactly 0.17.4, which this branch's prior 0.17.5 was already one past — kept as-is, no re-bump needed for the number itself, just resolved the resulting plugin.json/CHANGELOG.md conflict. Full fresh collision sweep after the merge found claude-ops now has NO open-PR collision at all. It also found #861 (work-items) re-derived its own claim from 0.19.0 to 0.20.0 since the last check (following #857's Jira-adapter minor bump into main) — colliding with this branch's prior 0.19.1. Re-bumped work-items to 0.20.1. source-control (0.16.1, held behind #882's 0.16.0 and #840's 0.15.10), repo-hygiene (0.4.6, no collision), and guardrails (0.9.6, no collision) re-verified against current main and all live open PRs — unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1V3gkrfSf75isB8MiDy3o
… as deliberate (#853) ## Summary Closes the design fork raised in #820: whether the shell assert-helper duplicated across 5 plugins (and the divergent per-script exit-code taxonomies alongside it) should be consolidated into a shared mechanism, or documented as deliberate. This PR documents. ## Fix - Adds `docs/conventions/shell-test-helpers/README.md` as the owner doc explaining why the duplication and divergence stay as-is, and registers it in `docs/PLUGIN-PHILOSOPHY.md`'s convention registry table. - Adds a one-line pointer comment at each copy site back to the owner doc: `guardrails/hooks/guardrails-test-helpers.sh`, `claude-ops/hooks/claude-ops-test-helpers.sh`, `source-control/scripts/test-helpers.sh`, `repo-hygiene/skills/clean/scripts/lib/test-helpers.sh`, `work-items/tools/work-item-tracker/tests/lib.sh`. - Adds the same kind of pointer to `scripts/check-skill-portability.test.sh`, which opted out for an unrelated reason (it's repo tooling, not a plugin, so no plugin assertion library applies) — noted so the fork's second observation isn't left unexplained. - Bumps `plugin.json` + adds a `CHANGELOG.md` entry for every plugin whose helper file gained the pointer comment. Each has been re-derived from current `main` many times as sibling PRs merged or rebased mid-flight (see Related for the current picture): - `repo-hygiene`: 0.4.4→0.4.6 - `source-control`: 0.15.7→0.16.1 - `claude-ops`: 0.17.1→0.17.5 - `work-items`: 0.18.1→0.20.1 - `guardrails`: 0.9.5→0.9.6 (no open-PR collision at any point so far) No behavior change anywhere — comments and docs only. ## Decision **Chose Option B: document the per-plugin duplication and exit-code divergence as deliberate. No shared helper introduced.** Investigation before deciding: - This repo already has one sanctioned cross-plugin shared-source mechanism: a canonical file under `lib/` (e.g. `lib/hook-utils.sh`), copied — not imported — into each carrying plugin by a dedicated `scripts/sync-*.sh`, tracked in `scripts/cross-plugin-source-registry.txt`, and drift-checked by `scripts/check-cross-plugin-source-drift.sh --check`. - That mechanism is scoped to clusters meant to stay **byte-identical**. Running `check-cross-plugin-source-drift.sh discover` confirms it never even flags the five assert-helper files as a cluster candidate — they live at different paths per plugin and are not byte-identical, so they fall outside that mechanism's scope entirely. - Reading all five files: they are already three genuinely different shapes, not one library that drifted — a hook-contract shape (`guardrails`/`claude-ops`: `ok`/`bad`, `PASS`/`FAIL`, `make_sink`/`wait_for_sink`), a skill-script shape (`source-control`/`repo-hygiene`: `pass`/`fail`, `FAILED`/`CASE_NUM`, file-existence assertions), and a vendored-seam shape (`work-items`: same primitives, but owned by the seam itself so it stays correct wherever the seam is resolved from, independent of this repo's tooling). - Consolidating would mean designing a fourth, unified assertion API and rewriting every existing `*.test.sh` onto it — a bigger, riskier change than the coupling it would remove, and it would cross the plugin-independence boundary `docs/PLUGIN-PHILOSOPHY.md`'s design boundary section already draws (no plugin imports files from a sibling plugin). - Exit-code taxonomies (`remove-path.sh` 0/1/2/3/4, `git-tree-reset-batch.sh` 0/1/2 forwarding a child's 5/7, `check-skill-portability.sh` 0/1/2) encode genuinely different per-script contracts, not arbitrary numbering — each script already documents its own `Exit:` line, and a shared usage/exit helper would either flatten those contracts or grow per-caller branching. - Deferred, not rejected: `guardrails-test-helpers.sh` and `claude-ops-test-helpers.sh` are the one pair that already share a shape closely. If they converge to byte-identical, vendoring just that pair through the existing `lib/` + `sync-*.sh` + registry mechanism is the smaller, precedented move — recorded as the trigger in the owner doc rather than acted on now. ## Verification - `shellcheck` clean on all 6 edited shell files. - Full `check-skill-portability.test.sh` suite: 16/16 pass. - `check-cross-plugin-source-drift.sh --check`: no unregistered or drifted clusters. - `check-changelog-parity.sh --check`: passes with every version bump. - Ran every `*.test.sh` that sources an edited helper (repo-hygiene/clean, guardrails hooks, claude-ops hooks, work-items adapters/lib) — all green, confirming the comment-only edits changed no behavior. - `markdownlint-cli2` and `lychee` clean on the new and modified docs. ## Related - Scope note: issue #820's title says "disk-hygiene/clean", but `disk-hygiene` is Python-only (`hygiene.py`) with no shell assert-helper — the actual duplication lives in the 5 plugins the issue body names (repo-hygiene, source-control, guardrails, claude-ops, work-items) plus root `scripts/`. Treating the title as a triage typo (disk-hygiene vs. repo-hygiene, both "-hygiene" plugins with a `clean` skill) rather than touching disk-hygiene. - **`do-not-merge` held.** This session has had exceptionally heavy concurrent-lane traffic against these same 5 plugins — this PR has been rebased/re-derived nine times as siblings merged (#839, #826, #857, #877, #870, #844) or rebased in place (#840, #882, #861, each more than once). Current picture, last verified fresh at commit `e75e45ed` (`mergeable: MERGEABLE`; all 5 plugins re-checked against current `main` AND every live open PR): - `repo-hygiene` (claims 0.4.6): no open-PR collision. Main is at 0.4.5. - `source-control` (claims 0.16.1): held behind **#882** (`fix/511-babysit-self-identity-decouple`, claims 0.16.0, open) and **#840** (claims 0.15.10, open). Main is at 0.15.9. - `claude-ops` (claims 0.17.5): **no open-PR collision anymore** — #844 (the PR this leg was previously held behind) has merged, landing at exactly 0.17.4; this claim stays one past it. Main is at 0.17.4. - `work-items` (claims 0.20.1): held behind **#861** (`feat/613-mini-sdlc-pipeline-ssot`). #861 itself has re-derived its claim twice as `main` moved — from 0.19.0 up to 0.20.0 (following #857's Jira-adapter minor bump into main) — so this PR's claim moved from 0.19.1 to 0.20.1 to stay ahead. Main is at 0.19.0. - `guardrails` (claims 0.9.6): no open-PR collision. Main is at 0.9.5. - This plugin set has produced a new collision within minutes of nearly every prior check — including siblings re-deriving their own claims upward more than once, an unrelated PR landing at the exact same version by coincidence, and legs clearing and new ones opening. Re-run the full collision protocol (`gh pr list --repo melodic-software/claude-code-plugins --state open --json number,headRefName,files` filtered per plugin, AND a fresh diff of each plugin's version on `main` since this PR's last rebase) immediately before removing `do-not-merge` — do not trust this snapshot. Closes #820 Work-class: C2 (mechanical) — attended triage 2026-07-23, operator-ratified. 🤖 --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
What
Adds a bundled
jiraadapter to the work-item-tracker seam (plugins/work-items/tools/work-item-tracker/adapters/jira/), alongside the shippedgithubandlocal-markdownadapters. GitHub stays the default. Scoped to the read/resolve path per the #379 maintainer decisions (15:48, re-intake).Closes #379.
Adapter surface
Consume-only by default (issue #379 hard constraint) — the manifest declares:
get-itemGET /rest/api/3/issue/{key}→ normalized itemlist-itemsPOST /rest/api/3/search/jql,nextPageTokenpagination, JQL scoped toproject_keyscapabilitiescreate-item,claim,renew-lease,reclaim,link-blocks,add-sub-itemlist-sub-itemslist-frontier --parentdegrades to exit 6No code path creates, claims, or mutates a Jira ticket by default.
/work-items:work,track start, andlist-frontier --parentare consequently unavailable on a Jira binding — an accepted gap. Branch/PRSW2-*linkage and opt-in writes are sequenced follow-ups.Read-path normalization
state(statusCategory → open/closed),assignees(single assignee → accountId),labels(verbatim),type(issue-type name), open-onlyblocked_by_count(inward blocker links; linked-issue status inlined inissuelinks, so one call),parent_id(fields.parent),url(browse link). ID grammar:jira:<site>/<PROJECTKEY>#<number>⇄ native keyPROJECTKEY-number.Auth & config
Basic auth (email + API token). The token is read from the env var named by
config.jira.auth_env— never stored in the tracked binding, and passed to curl via a stdin config (-K -) so it never appears in argv. Required config:config.jira.{site, project_keys[], auth_email, auth_env}. The blocker link type (blocked_by_link_type, defaultBlocks) and the "done" statusCategory key set (done_category_keys, default["done","completed"]) are configurable override seams for the two facts deferred to the live-instance pass, so the adapter is independent of them.Fresh-docs research (per CLAUDE.md fresh-docs mandate)
Every endpoint/field was verified this session against the official Atlassian OpenAPI spec (
https://dac-static.atlassian.com/cloud/jira/platform/swagger-v3.v3.json) and official docs — not training-data recall:POST /rest/api/3/search/jql(requestjql/fields/maxResults≤5000/nextPageToken; responseissues[]/nextPageToken/isLast): https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/#api-rest-api-3-search-jql-postGET /rest/api/3/issue/{issueIdOrKey}(fieldsquery param): https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-getIssueBean.fields.issuelinks[].inwardIssueis aLinkedIssuewhosefieldsuses theFieldsschema, which includesstatus.statusCategory.key— so a single search call deep-populates each blocker's status (no second round-trip). Verified in the spec's component schemas.done_category_keysdefaults to both): the spec's ownGET /statuscategoryexample returns keysin-flight/completed, while real Jira Cloud instances widely returnnew/indeterminate/done(community-flagged doc gap) — only the live instance settles it. https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-workflow-status-categories/#api-rest-api-3-statuscategory-getAuthorization: Basic base64(email:token)): https://developer.atlassian.com/cloud/jira/platform/basic-auth-for-rest-apis/ · tokens at https://id.atlassian.com/manage/api-tokensTests
curlvia aWIT_JIRA_CURLseam — the read path (normalization, OPEN-only blocked_by_count, done-exclusion, parent qualification, pagination across twonextPageTokenpages, error → exit-code mapping) is exercised fully offline, no live Jira call.jiraconformance binding runs the abstract seam suite offline in CI (the consume-only manifest makes every suite-exercised path pre-network), and re-runs it under agh/curl-blocking PATH shim to prove zero network.shellcheck --rcfile=.shellcheckrc, changelog-parity, plugin validation, skill-portability, and markdownlint pass locally.Independent review
Audited by a fresh-context reviewer (producer ≠ critic). It found — and this branch fixes — a JQL-injection CRITICAL:
list-itemsassembled theproject in (...)/statusCategory in (...)clauses by string concatenation without escaping embedded quotes, so a--repoor configured key containing"could inject arbitrary JQL. Fixed by allowlist-validating every JQL-interpolated value before assembly (configured keys → exit 3 at config load;--repokey → exit 2 at parse; both before any HTTP call), with tests asserting the literal JQL payload and hostile-input rejection. Advisory nits (null-accountId assignee →[], up-front rejection of a malformed project key in an id, a hardcoded date in the conformance binding) also folded in. Everything else — consume-only structure, token hygiene, exit codes, pagination — verified clean.Contract gaps found
lib/binding.shcarries alocal-markdown-specificstorage_dirleak; adapters must re-resolve the binding to read their ownconfig.<provider>). The Jira adapter readsconfig.jiraitself rather than growing that leak. Filed as scoped follow-up refactor(work-items): dispatcher should export the resolved binding path to adapters #852.Not in scope (sequenced follow-ups)
Opt-in writes; branch/PR
SW2-*linkage (spans two plugins' source, forbidden by this issue's acceptance); container-scoped frontier (list-sub-items).Related
Related work this PR does not close:
config.<provider>without re-resolving (retires thelib/binding.shprovider-config leak). Not required for this adapter.ghbypass the seam). Independent of this adapter landing, but they bound how broadly a non-GitHub provider is reachable across all consumers.statusCategory"done" key (feat(work-items): Jira adapter for the work-item-tracker seam #379 decision ci: onboard to the CI platform (ci-workflows + standards) #4) and the authoritative blocker/sub-item link type (feat(work-items): Jira adapter for the work-item-tracker seam #379 decision perf(markdown-formatter): unwired producer builds telemetry payload before the opt-in guard (~90ms/edit on Windows) #6); both have configurable defaults here.