Skip to content

Commit 080baf8

Browse files
johanzanderclaude
andauthored
feat: add the board bootstrap script, with the Priority tiers the board actually has (#656)
* feat: add the board bootstrap script and the PO's board memory Split out of #614, whose review's primary blocker was that this board work was undescribed and orthogonal to that PR's mypy/review-bot fixes. Most of the original bundle is now redundant: main already carries the 'Ready for Dev'/'In Progress' column names, the awaiting-before-analyzed precedence fix, and the 'there is no P0' correction in the backlog skill. What was left unlanded is the bootstrap script and the two PO memory files. backlog-board-init.sh created the Priority field with P0,P1,P2 — a tier the live board does not have and no consumer reads. It now creates P1-P4, which is what the board carries, what the skill documents and what the digest ranks on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vHvj9qtD1jDYVnm3LjTnt * fix: keep the board memory to its durable lesson, not a state snapshot The Stage 4 review found this violates the backlog skill's own rule ('State lives on GitHub, nowhere else' — never write a local file that mirrors board or issue state). The per-issue triage list was exactly that, and was already stale two days after being written. What stays is the generalizable lesson: the digest derives a column for an issue whether or not a card exists, so board presence has to be checked against gh project item-list directly. The name and index line now say that rather than framing the file as a snapshot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vHvj9qtD1jDYVnm3LjTnt * test: pin the board bootstrap's tiers, idempotence and failed lookup The review was right that 'this cannot be tested' does not hold against this repo's own precedent — test_backlog_digest.py shims gh on PATH for exactly this shape of script, and #614 used the same technique. Three things are worth pinning and all are visible from the arguments the script passes: - Priority is created as P1,P2,P3,P4. It is a bare string literal that already drifted once (P0,P1,P2), and nothing else would catch it. - An existing board creates nothing at all. - A failed lookup does not become a second board. That last one is also a fix, not just a test. The lookup ended in '|| true', which makes 'no such board' and 'the lookup failed' indistinguishable — a rate limit or transient GraphQL error would have created a duplicate 'BESS Manager Backlog', contradicting the idempotence the header promises. It now stops instead. Each test verified to redden under the matching mutation: restoring the P0 tiers fails the first, restoring '|| true' fails the third. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vHvj9qtD1jDYVnm3LjTnt --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent e765a3b commit 080baf8

5 files changed

Lines changed: 259 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- [Board field IDs](project_board_field_ids.md) — GraphQL node/field/option IDs for Status/Priority/Awaiting; Priority has NO P0 option, only P1-P4
2+
- [Digest column ≠ board presence](project_backlog_board_state_2026_08_16.md) — the digest derives a column whether or not a card exists; diff `item-list` against `issue list` before trusting sync
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
name: project_backlog_board_state_2026_08_16
3+
description: The digest deriving a column for an issue does not mean the issue is on the board — learned when a 2026-08-16 pass found 1 card against 36 open issues
4+
metadata:
5+
type: project
6+
---
7+
8+
Before this pass, the board (project 1) had exactly **one** card (#611,
9+
P1/Backlog) despite 36 open issues existing. `backlog-digest.sh` derives a
10+
`column` for every open issue regardless of whether it's on the board, which
11+
made this easy to miss without diffing against `gh project item-list`
12+
directly — the digest's presence doesn't imply board presence.
13+
14+
**Why:** board-init/bootstrap work (PRs around #609-611 per recent commits)
15+
created the project and field schema but never did a bulk backlog import —
16+
only the issue that happened to be filed around that time landed on it.
17+
18+
**How to apply:** before trusting "the board is roughly in sync," diff
19+
`gh project item-list` counts against `gh issue list --state open` counts.
20+
Don't assume prior passes kept the board populated.
21+
22+
See [[project_board_field_ids]] for the GraphQL mechanics used.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
name: project_board_field_ids
3+
description: GraphQL node/field/option IDs for the backlog board (project 1) — Status, Priority, Awaiting — and the Priority field's real options
4+
metadata:
5+
type: project
6+
---
7+
8+
Board GraphQL IDs, confirmed working 2026-08-16 via a full-board write pass
9+
(all 36 open issues added, Status/Priority/Awaiting set on every card):
10+
11+
- Project id: `PVT_kwHOACEigM4Bgiwa`
12+
- Status field: `PVTSSF_lAHOACEigM4Bgiwazhfh7Mg` — options: Backlog `f75ad846`,
13+
Analysis `012dae50`, Ready for Dev `456880aa`, In Progress `47fc9ee4`,
14+
In Review `58ad8ead`, Done `98236657`
15+
- Priority field: `PVTSSF_lAHOACEigM4Bgiwazhfh7NQ` — options are **P1 `131c5c2f`,
16+
P2 `107b9947`, P3 `6d4b1494`, P4 `4d153125`. There is no P0 option** — the
17+
"P0 da61340b" that older skill and task text referenced does not exist on
18+
the live field. Treat P1 as the top tier.
19+
- Awaiting field: `PVTSSF_lAHOACEigM4Bgiwazhfh7Nw` — options: reporter
20+
`71ef723a`, discussion `82098dd9`, upstream `16ca2f41`, analysis `c7538747`
21+
22+
Mutation shape that works: `addProjectV2ItemById(input: {projectId, contentId})`
23+
to add a card (contentId = issue node id from `gh issue list --json id`), then
24+
`updateProjectV2ItemFieldValue(input: {projectId, itemId, fieldId, value:
25+
{singleSelectOptionId}})` per field. Run via
26+
`scripts/gh-agent.sh --as po api graphql -f query='...' -f name=value ...`
27+
from inside the repo checkout (it resolves `.env` via `git rev-parse
28+
--git-common-dir`, so it fails silently with "BESS_PO_TOKEN not set" if run
29+
from a non-repo cwd like a scratch tmpdir).
30+
31+
See [[project_backlog_board_state_2026_08_16]] for what was actually on the
32+
board before/after this pass.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""Tests for scripts/backlog-board-init.sh — the one-shot board bootstrap.
2+
3+
The script's every effect is a `gh project` call, so `gh` is a shim on PATH
4+
that records its arguments and replays canned output — the same technique
5+
test_backlog_digest.py uses. That is enough to pin the three things worth
6+
pinning: the Priority tiers it creates, that an existing board is left alone,
7+
and that a failed lookup does not become a second board.
8+
9+
Priority is the reason this file exists. It was created as `P0,P1,P2` — a
10+
tier the live board does not have, missing two the digest ranks on — and a
11+
plain string literal can drift back with nothing to catch it.
12+
"""
13+
14+
import os
15+
import stat
16+
import subprocess
17+
from pathlib import Path
18+
19+
REPO_ROOT = Path(__file__).resolve().parents[2]
20+
SCRIPT = REPO_ROOT / "scripts" / "backlog-board-init.sh"
21+
22+
23+
def _make_gh(bin_dir: Path, log: Path, *, list_output: str, list_rc: int = 0) -> None:
24+
"""A `gh` that logs every invocation and answers the calls the script makes.
25+
26+
`auth status` must mention the project scope or the script stops early;
27+
`project list` is the idempotency lookup; `project create` returns a
28+
number; `project field-create` is what we assert on.
29+
"""
30+
body = f"""
31+
echo "$@" >> "{log}"
32+
case "$1 $2" in
33+
"auth status") echo "✓ Logged in, scopes: repo, project" ;;
34+
"project list") printf '%s' '{list_output}'; exit {list_rc} ;;
35+
"project create") echo 7 ;;
36+
"project field-create") ;;
37+
*) ;;
38+
esac
39+
exit 0
40+
"""
41+
p = bin_dir / "gh"
42+
p.write_text("#!/bin/sh\n" + body)
43+
p.chmod(p.stat().st_mode | stat.S_IEXEC)
44+
45+
46+
def _run(
47+
tmp_path: Path, **gh_kwargs: object
48+
) -> tuple[subprocess.CompletedProcess, str]:
49+
bin_dir = tmp_path / "bin"
50+
bin_dir.mkdir(parents=True, exist_ok=True)
51+
log = tmp_path / "gh.log"
52+
log.write_text("")
53+
_make_gh(bin_dir, log, **gh_kwargs) # type: ignore[arg-type]
54+
55+
env = dict(os.environ, PATH=f"{bin_dir}:{os.environ['PATH']}")
56+
proc = subprocess.run(
57+
["bash", str(SCRIPT)],
58+
cwd=tmp_path,
59+
capture_output=True,
60+
text=True,
61+
env=env,
62+
)
63+
return proc, log.read_text()
64+
65+
66+
def test_priority_field_is_created_with_the_tiers_the_board_has(
67+
tmp_path: Path,
68+
) -> None:
69+
"""P1-P4, and no P0 — matching the live board, the skill and the digest."""
70+
proc, calls = _run(tmp_path, list_output="")
71+
72+
assert proc.returncode == 0, proc.stderr
73+
priority = [
74+
line
75+
for line in calls.splitlines()
76+
if "field-create" in line and "Priority" in line
77+
]
78+
assert len(priority) == 1, calls
79+
assert "P1,P2,P3,P4" in priority[0]
80+
assert "P0" not in priority[0]
81+
82+
83+
def test_an_existing_board_is_reported_and_left_alone(tmp_path: Path) -> None:
84+
"""Idempotence: report the number, create nothing."""
85+
proc, calls = _run(tmp_path, list_output="3")
86+
87+
assert proc.returncode == 0, proc.stderr
88+
assert "PROJECT_NUMBER 3" in proc.stdout
89+
assert "project create" not in calls
90+
assert "field-create" not in calls
91+
92+
93+
def test_a_failed_lookup_does_not_create_a_second_board(tmp_path: Path) -> None:
94+
"""A lookup that errors must not read as "no board exists".
95+
96+
This is the whole reason the lookup is not `|| true`: an empty result and
97+
a failed call are indistinguishable afterwards, and the script creates a
98+
board when it sees the former.
99+
"""
100+
proc, calls = _run(tmp_path, list_output="", list_rc=1)
101+
102+
assert proc.returncode != 0
103+
assert "project create" not in calls
104+
assert "could not list projects" in proc.stderr

scripts/backlog-board-init.sh

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Create the Product Owner's kanban board, once. Idempotent: if a project with
4+
# this title already exists, print its number and change nothing.
5+
#
6+
# Run this as the MAINTAINER, not as the PO identity. A GitHub user cannot
7+
# create a Project inside another user's account, and the board deliberately
8+
# lives under the maintainer's account — it is their backlog and where they
9+
# will look for it. `scripts/gh-agent.sh --as po project list --owner
10+
# johanzander` returns "unknown owner type" for exactly this reason.
11+
#
12+
# The PO then gets write access to the board as a project collaborator, which
13+
# is a separate grant from repo collaboration — repo write does NOT imply
14+
# project write. That step is manual (see the end of this script's output):
15+
# the Projects v2 API exposes no collaborator-invite mutation.
16+
#
17+
# Usage:
18+
# scripts/backlog-board-init.sh # create or report the board
19+
#
20+
# Output (stdout, last line):
21+
# PROJECT_NUMBER <n>
22+
set -euo pipefail
23+
24+
owner="${PROJECT_OWNER:-johanzander}"
25+
title="BESS Manager Backlog"
26+
27+
# `gh project` needs the `project` scope. Fail with the fix rather than a raw
28+
# GraphQL error — this is the single most common setup failure here.
29+
if ! gh auth status 2>&1 | grep -q "project"; then
30+
echo "backlog-board-init.sh: your gh token lacks the 'project' scope." >&2
31+
echo " Fix: gh auth refresh -s project" >&2
32+
exit 1
33+
fi
34+
35+
# `|| true` here would be the difference between "no such board" and "the
36+
# lookup failed", and this script creates a board when it sees the former. A
37+
# rate limit or a transient GraphQL error would therefore produce a SECOND
38+
# "BESS Manager Backlog" project, breaking the idempotence promised above. So
39+
# a failed lookup stops the run instead of being read as an empty result.
40+
if ! existing=$(gh project list --owner "$owner" --format json \
41+
--jq ".projects[] | select(.title == \"$title\") | .number"); then
42+
echo "backlog-board-init.sh: could not list projects for '$owner'." >&2
43+
echo " Refusing to create a board without knowing whether one exists." >&2
44+
exit 1
45+
fi
46+
47+
if [ -n "$existing" ]; then
48+
echo "Board already exists — nothing changed." >&2
49+
echo "PROJECT_NUMBER $existing"
50+
exit 0
51+
fi
52+
53+
number=$(gh project create --owner "$owner" --title "$title" \
54+
--format json --jq '.number')
55+
56+
echo "Created project #$number." >&2
57+
58+
# Custom fields. The built-in Status field carries the columns and is edited
59+
# separately (see the closing instructions) — `gh` cannot rewrite the options
60+
# of a built-in single-select field.
61+
#
62+
# Priority is P1-P4 with no P0: that is what the live board carries, what the
63+
# backlog skill documents, and what the digest ranks on. A P0 here would
64+
# create an option no consumer reads.
65+
gh project field-create "$number" --owner "$owner" \
66+
--name "Priority" --data-type SINGLE_SELECT \
67+
--single-select-options "P1,P2,P3,P4" >/dev/null
68+
echo " + Priority field (P1,P2,P3,P4)" >&2
69+
70+
gh project field-create "$number" --owner "$owner" \
71+
--name "Source" --data-type SINGLE_SELECT \
72+
--single-select-options "issue,TODO" >/dev/null
73+
echo " + Source field (issue,TODO)" >&2
74+
75+
gh project field-create "$number" --owner "$owner" \
76+
--name "Awaiting" --data-type SINGLE_SELECT \
77+
--single-select-options "reporter,discussion,upstream,analysis" >/dev/null
78+
echo " + Awaiting field (reporter,discussion,upstream,analysis)" >&2
79+
80+
cat >&2 <<REMAINING
81+
82+
Two steps remain and neither can be scripted — the Projects v2 API exposes no
83+
mutation for either:
84+
85+
1. Columns. Open the board, edit the built-in Status field, and set its
86+
options to exactly:
87+
Backlog, Analysis, Ready for Dev, In Progress, In Review, Done
88+
The digest derives these names; a mismatch silently strands cards.
89+
90+
2. PO access. Project -> ... -> Settings -> Manage access -> invite
91+
bess-product-owner with write. Repo collaboration does NOT grant project
92+
access, so without this every board write fails as the PO.
93+
94+
Then export PROJECT_NUMBER=$number (or add it to your shell profile) and run
95+
scripts/backlog-digest.sh to confirm the board is readable.
96+
97+
REMAINING
98+
99+
echo "PROJECT_NUMBER $number"

0 commit comments

Comments
 (0)