Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changes/work-items/feat-8324-get-field-options-3a9d4c71.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
bump: minor
---

### Added

- The tracker seam's `get-field-options --field <name-or-id> [--repo <owner>/<repo>]` verb reads
the options of one single-select issue field, so a caller such as planning's Work size can use a
tracker's own size values. The GitHub adapter implements it from the organization issue-field
listing `set-fields` validates against; a field the organization lacks prints an empty
`options` list and exits 0, and an adapter that declares no `get-field-options` key exits 6
(unsupported). The other bundled adapters' manifests, the adapter generator's verb list and the
adapter-spec reference now declare the verb too (`false` outside GitHub).
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ A self-hosted, forge-shaped provider with no lease support:
"create-item": true, "get-item": true, "claim": false, "renew-lease": false,
"release": false, "reclaim": false, "link-blocks": true, "add-sub-item": false,
"list-items": true, "list-sub-items": false, "label-provenance": false,
"set-fields": false, "change-link": true, "capabilities": true
"set-fields": false, "get-field-options": false, "change-link": true, "capabilities": true
},
"features": {
"cross_repo_edges": false, "sub_items": false, "leases": false, "labels": true
Expand Down Expand Up @@ -75,12 +75,13 @@ rather than defaulted, because an unlisted verb means the spec was written again
different contract revision, and guessing produces a manifest that lies.

`create-item`, `get-item`, `claim`, `renew-lease`, `release`, `reclaim`, `link-blocks`,
`add-sub-item`, `list-items`, `list-sub-items`, `label-provenance`, `set-fields`, `change-link`,
`add-sub-item`, `list-items`, `list-sub-items`, `label-provenance`, `set-fields`, `get-field-options`, `change-link`,
`capabilities`.

`set-fields` sets single-select issue fields by option id (`CONTRACT.md` "Verbs"). Declare it
`false` unless the provider has such fields and the adapter refuses an option id that is not one
of its field's options before writing.
of its field's options before writing. `get-field-options` is its read-only counterpart: declare it
`false` unless the adapter can list one field's options.

`label-provenance` reports, for each label passed with `--label`, who applied it most
recently, that person's permission on the repository, and whether the label is trusted
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ SHEBANG='#!/usr/bin/env bash'
# minus list-frontier (core-derived), plus list-items.
readonly ADAPTER_VERBS=(
create-item get-item claim renew-lease release reclaim link-blocks
add-sub-item list-items list-sub-items label-provenance set-fields change-link capabilities
add-sub-item list-items list-sub-items label-provenance set-fields get-field-options change-link capabilities
Comment thread
kyle-sexton marked this conversation as resolved.
)
readonly FEATURE_KEYS=(cross_repo_edges sub_items leases labels)
Comment thread
kyle-sexton marked this conversation as resolved.
readonly LIMIT_KEYS=(sub_items_per_parent sub_item_depth dependencies_per_type list_items_max)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ BASE_SPEC='{
"release": false,
"reclaim": false, "link-blocks": false, "add-sub-item": false,
"list-items": true, "list-sub-items": false, "label-provenance": false,
"set-fields": false,
"set-fields": false, "get-field-options": false,
"change-link": true, "capabilities": true
},
"features": { "cross_repo_edges": false, "sub_items": false, "leases": false, "labels": true },
Expand Down
11 changes: 11 additions & 0 deletions plugins/work-items/tools/work-item-tracker/CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ work-item-tracker.sh list-items [--state open|closed|all] [--repo <o>/<r>] [--la
work-item-tracker.sh list-frontier [--autonomous] [--parent <container-id>] [--repo <o>/<r>] [--label <name>]
work-item-tracker.sh label-provenance <id> --label <name> [--label <name> ...]
work-item-tracker.sh set-fields <id> --field <field-id>=<option-id> [--field <field-id>=<option-id> ...]
work-item-tracker.sh get-field-options --field <name-or-id> [--repo <o>/<r>]
work-item-tracker.sh change-link (<id> | --branch-ref <ref> | --branch <name>) [--repo <o>/<r>]
work-item-tracker.sh capabilities
```
Expand Down Expand Up @@ -243,6 +244,16 @@ option's name (pointer:
2026-10-18, recheck trigger: the write starts taking option ids, or the listing's permission
changes). An adapter that declares no `set-fields` key exits `6`.

`get-field-options` is the read-only counterpart of `set-fields`: it lists the options of one
single-select issue field, so a caller such as planning's Work size can use a tracker's own size
values. `--field` is the field's exact name or its numeric id; `--repo` picks the repository whose
organization is read (default: the working directory's remote). It prints `{schema_version, field,
options: [{id, name}]}`. A field the organization does not have, or one with no options, prints an
empty `options` list and exits `0`; a caller that gets one falls back to its own defaults. Only the
GitHub adapter implements it, from the same `GET /orgs/{org}/issue-fields` listing `set-fields`
validates against (same `read:org` need). An adapter that declares no `get-field-options` key exits
`6`, which a caller reads as "unsupported", not as a failure. Label groups are not read by this verb.

`claim --ttl-minutes <n>` (0–59) **adds to** `--ttl-hours`, it never replaces it: `--ttl-hours 24
--ttl-minutes 30` is a 24.5-hour lease. For a sub-hour lease pass `--ttl-hours 0 --ttl-minutes <n>`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"list-items": true,
"label-provenance": false,
"set-fields": false,
"get-field-options": false,
"list-sub-items": false,
"capabilities": true,
"change-link": true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"list-items": true,
"label-provenance": false,
"set-fields": false,
"get-field-options": false,
"list-sub-items": false,
"reclaim": false,
"release": false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"list-sub-items": true,
"label-provenance": true,
"set-fields": true,
"get-field-options": true,
"capabilities": true,
"change-link": true
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# get-field-options --field <name-or-id> [--repo <owner>/<repo>] — CONTRACT.md "Verbs (core public
# surface)". Read-only. Lists the options of one single-select organization issue field, from the
# same listing set-fields validates against (GET orgs/{org}/issue-fields). The field is matched by
# its exact name or its numeric id. A field the organization does not have, or one with no options,
# yields an empty list and exit 0, so a caller falls back to its own defaults.
set -uo pipefail
# shellcheck source=common.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh"

usage="usage: get-field-options --field <name-or-id> [--repo <owner>/<repo>]"
wit_help_if_requested "$usage" "$@"

field="" repo_override=""
while [[ $# -gt 0 ]]; do
case "$1" in
--field)
[[ $# -ge 2 && -n "$2" && -z "$field" ]] || wit_usage_error "--field needs one non-empty value"
field="$2"
shift 2
;;
--repo)
[[ $# -ge 2 && -n "$2" ]] || wit_usage_error "--repo needs a value"
repo_override="$2"
shift 2
;;
*) wit_usage_error "unknown argument: $1" ;;
esac
done
[[ -n "$field" ]] || wit_usage_error "$usage"
[[ -z "$repo_override" || "$repo_override" =~ ^[^/]+/[^/]+$ ]] ||
wit_usage_error "--repo must be <owner>/<repo> (got: $repo_override)"

# $() swallows wit_run_gh's exit-on-error; propagate its code (see create-item.sh).
target_repo="$(wit_resolve_repo "$repo_override")" || exit "$?"
owner="${target_repo%%/*}"

wit_run_gh read api -H "X-GitHub-Api-Version: 2026-03-10" "orgs/$owner/issue-fields"
options="$(jq -c --arg f "$field" '[.[] | select((.id | tostring) == $f or .name == $f)
| .options // [] | .[] | {id, name}]' <<<"$WIT_GH_OUT")" || {
echo "get-field-options: org issue-field listing for $owner is not JSON" >&2
exit "$EX_INTERNAL"
}

jq -nc --arg sv "$WIT_SCHEMA_VERSION" --arg field "$field" --argjson options "$options" \
'{schema_version: $sv, field: $field, options: $options}'
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
# shellcheck disable=SC2154 # FAILED/CASE_NUM initialized by the sourced helper
# Offline: get-field-options against a gh stub. The org field listing is the trimmed recording
# set-fields.test.sh uses (GET orgs/melodic-software/issue-fields, 2026-10-18).
set -uo pipefail
unset GIT_DIR GIT_WORK_TREE GIT_CONFIG

TMP_ROOT="$(mktemp -d)"
trap 'rm -rf "$TMP_ROOT"' EXIT
S="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/get-field-options.sh"
source "$(dirname "$S")/../../lib/verb-test-helpers.sh"

assert_help "$S"
assert_usage_error "$S" # no --field
assert_usage_error "$S" --field # no value
assert_usage_error "$S" --field Size --field Complexity # one field only
assert_usage_error "$S" --field Size --repo notarepo # malformed --repo
assert_usage_error "$S" --field Size --bogus

command -v jq >/dev/null 2>&1 || skip_suite "jq not on PATH"

STUB="$TMP_ROOT/bin"
LOG="$TMP_ROOT/gh.log"
mkdir -p "$STUB"
cat >"$STUB/gh" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "$*" >>"$GH_STUB_LOG"
for a in "$@"; do
case "$a" in
orgs/*/issue-fields)
[[ -z "${GH_STUB_ERR:-}" ]] || { printf '%s\n' "$GH_STUB_ERR" >&2; exit 1; }
cat <<'JSON'
[{"data_type":"single_select","id":15462270,"name":"Size","options":[{"id":27053566,"name":"Large"},{"id":27053567,"name":"Medium"},{"id":27053568,"name":"Small"}]},
{"data_type":"single_select","id":48625720,"name":"Complexity","options":[{"id":85127653,"name":"High"},{"id":85127651,"name":"Low"}]},
{"data_type":"date","id":7,"name":"Target date","options":null}]
JSON
exit 0
;;
esac
done
printf 'gh-stub: unhandled %s\n' "$*" >&2
exit 90
EOF
chmod +x "$STUB/gh"

run() {
: >"$LOG"
OUT="$(PATH="$STUB:$PATH" GH_STUB_LOG="$LOG" bash "$S" "$@" 2>"$TMP_ROOT/err")"
RC=$?
ERR="$(<"$TMP_ROOT/err")"
}

run --field Size --repo o/r
assert_eq "named field → exit 0" "0" "$RC"
assert_eq "lists the field's options" '[{"id":27053566,"name":"Large"},{"id":27053567,"name":"Medium"},{"id":27053568,"name":"Small"}]' \
"$(jq -c '.options' <<<"$OUT")"
assert_eq "output echoes the requested field" "Size" "$(jq -r '.field' <<<"$OUT")"
assert_contains "reads the repo owner's org" "$(head -n 1 "$LOG")" "orgs/o/issue-fields"

run --field 48625720 --repo o/r
assert_eq "field by numeric id → exit 0" "0" "$RC"
assert_eq "field by id lists its options" '["High","Low"]' "$(jq -c '[.options[].name]' <<<"$OUT")"

run --field Missing --repo o/r
assert_eq "absent field → exit 0" "0" "$RC"
assert_eq "absent field → empty list" "[]" "$(jq -c '.options' <<<"$OUT")"

run --field "Target date" --repo o/r
assert_eq "field with no options → exit 0" "0" "$RC"
assert_eq "field with no options → empty list" "[]" "$(jq -c '.options' <<<"$OUT")"

GH_STUB_ERR="HTTP 403: Forbidden" run --field Size --repo o/r
assert_eq "failed listing → mapped auth exit" "4" "$RC"
assert_contains "failed listing → gh error reaches stderr" "$ERR" "HTTP 403"

[[ $FAILED -eq 0 ]] || exit 1
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"list-items": true,
"label-provenance": false,
"set-fields": false,
"get-field-options": false,
"list-sub-items": false,
"capabilities": true,
"change-link": true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"list-items": true,
"label-provenance": false,
"set-fields": false,
"get-field-options": false,
"list-sub-items": true,
"capabilities": true,
"change-link": true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"list-items": true,
"label-provenance": false,
"set-fields": false,
"get-field-options": false,
"list-sub-items": true,
"reclaim": true,
"release": false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"list-items": true,
"label-provenance": false,
"set-fields": false,
"get-field-options": false,
"list-sub-items": true,
"capabilities": true,
"change-link": true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ wit_case_if_unsupported link-blocks "$FAKE_ID" --blocked-by "$FAKE_ID"
wit_case_if_unsupported add-sub-item "$FAKE_ID" --parent "$FAKE_ID"
wit_case_if_unsupported label-provenance "$FAKE_ID" --label agent-ready
wit_case_if_unsupported set-fields "$FAKE_ID" --field 1=2
wit_case_if_unsupported get-field-options --field Size
if ! verb_supported list-items; then
wit_case "unsupported list-items gates list-frontier → exit 6" 6 list-frontier
fi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Verbs:
list-frontier [--autonomous] [--parent <container-id>] [--repo <owner>/<repo>] [--label <name>]
label-provenance <id> --label <name> [--label <name> ...]
set-fields <id> --field <field-id>=<option-id> [--field <field-id>=<option-id> ...]
get-field-options --field <name-or-id> [--repo <owner>/<repo>]
change-link (<id> | --branch-ref <ref> | --branch <name>) [--repo <owner>/<repo>]
capabilities
Contract: tools/work-item-tracker/CONTRACT.md
Expand Down Expand Up @@ -128,7 +129,7 @@ main() {

local adapter_verb="$verb"
case "$verb" in
create-item | get-item | claim | renew-lease | release | reclaim | link-blocks | add-sub-item | list-sub-items | label-provenance | set-fields | change-link | capabilities) ;;
create-item | get-item | claim | renew-lease | release | reclaim | link-blocks | add-sub-item | list-sub-items | label-provenance | set-fields | get-field-options | change-link | capabilities) ;;
list-items) ;;
list-frontier)
# Chosen BEFORE the capability gate, so an adapter without list-sub-items
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,12 @@ RC_CAPTURE=$?
assert_eq "undeclared set-fields verb → exit 6" "6" "$RC_CAPTURE"
assert_contains "exit-6 stderr names the set-fields verb" "$ERR" "set-fields"

# An adapter with no issue fields declares no get-field-options key: the verb reports unsupported.
ERR="$(run_dispatcher get-field-options --field Size 2>&1 >/dev/null)"
RC_CAPTURE=$?
assert_eq "undeclared get-field-options verb → exit 6" "6" "$RC_CAPTURE"
assert_contains "exit-6 stderr names the get-field-options verb" "$ERR" "get-field-options"

# --- capabilities passthrough ---

OUT="$(run_dispatcher capabilities)"
Expand Down
Loading