From e25d45a14f59421c547e88c4e9e1839f430eb674 Mon Sep 17 00:00:00 2001 From: Andy Baran Date: Thu, 16 Jul 2026 16:33:44 -0400 Subject: [PATCH 1/2] feat(project): wire enforced GitHub Project status transitions with graceful degradation Closes #8. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- skills/issue-driven-github-flow/SKILL.md | 10 +++ .../references/projects.md | 67 ++++++++++++++++--- tests/run.sh | 23 ++++++- 3 files changed, 88 insertions(+), 12 deletions(-) diff --git a/skills/issue-driven-github-flow/SKILL.md b/skills/issue-driven-github-flow/SKILL.md index efd5dca..65becf6 100644 --- a/skills/issue-driven-github-flow/SKILL.md +++ b/skills/issue-driven-github-flow/SKILL.md @@ -218,6 +218,10 @@ git add -A "$SKILL/scripts/gitflow.sh" pr "feat(export): add CSV export for reports" 42 ``` +Move the linked GitHub Project item to **In Progress** after the branch is cut; +use [projects.md](references/projects.md) for the ID-resolution recipe, and +no-op cleanly if no Project exists or Project auth is unavailable. + Pick `type` (∈ `feat|fix|chore|docs|refactor|test|perf`) to match the work and a hyphenated description that reads cleanly. Prefer issue-numbered branch names: `feat/42-oauth-login`, `fix/87-null-deref-on-empty-cart`, @@ -290,6 +294,10 @@ gh pr checks --watch gh pr merge --squash --delete-branch ``` +Move the linked GitHub Project item to **Done** after the squash merge; use +[projects.md](references/projects.md) for the ID-resolution recipe, and no-op +cleanly if no Project exists or Project auth is unavailable. + After merge, sync the default branch and confirm the linked issue closed (the `Closes #N` footer does this automatically on merge). If it didn't, close it with a comment pointing at the merged PR. @@ -352,10 +360,12 @@ items across status columns and linking the project to the repo. | Multiple independent issues | Use worktrees via [worktrees.md](references/worktrees.md) | | Plan written | Hand to review agent; iterate until `Verdict: APPROVED` | | Approved plan | Dispatch implementation agent | +| Step 3 branch cut | Move Project item to **In Progress** via [projects.md](references/projects.md); no-op cleanly if no Project exists or Project auth is unavailable | | Code done | Run verification, commit, open a draft PR | | Draft PR open | Dispatch 🔬 code-review agent; require a verdict | | Critical/Important review finding | Loop back to implementation; re-verify and re-review | | PR ready to land | Require human approval and green `gh pr checks` when CI exists | +| PR squash-merged | Move Project item to **Done** via [projects.md](references/projects.md); no-op cleanly if no Project exists or Project auth is unavailable | | PR merged | `--delete-branch`, sync `main`, confirm issue closed | | Bad squash merge | `git revert ` on a new branch; open a revert PR | | Secret requested | Refuse to commit it; use [security.md](references/security.md) | diff --git a/skills/issue-driven-github-flow/references/projects.md b/skills/issue-driven-github-flow/references/projects.md index 70f5e29..3861003 100644 --- a/skills/issue-driven-github-flow/references/projects.md +++ b/skills/issue-driven-github-flow/references/projects.md @@ -26,24 +26,69 @@ filed. ## Move items across status columns +Status updates are required workflow bookkeeping when a Project exists, but they +must never block implementation or merge. If there is no linked Project, treat status updates as a clean no-op. +If `gh` is unavailable or the active token lacks the `project` scope, print the +problem, continue the workflow, and let the user refresh auth later with +`gh auth refresh -s project`. + Projects v2 status is a custom single-select field. To set it you need the -field and option IDs: +Project ID, the Status field ID, the option ID, and the Project item ID for the +issue. These IDs are dynamic, so resolve them every time rather than hard-coding +them: ```bash -# Discover the Status field id and its option ids (Todo / In Progress / Done) -gh project field-list --owner "@me" --format json +PROJECT_NUMBER= +ISSUE_NUMBER= +OWNER="@me" # for org-owned repos, use the org login instead + +# Resolve the Project id. +gh project view --owner "@me" --format json +PROJECT_ID="$(gh project view "$PROJECT_NUMBER" --owner "$OWNER" --format json --jq '.id')" -# Then update an item's status +# Resolve the Status field id and its option ids (Todo / In Progress / Done). +gh project field-list --owner "@me" --format json +STATUS_FIELD_ID="$( + gh project field-list "$PROJECT_NUMBER" --owner "$OWNER" --format json \ + --jq '.fields[] | select(.name == "Status") | .id' +)" +TODO_OPTION_ID="$( + gh project field-list "$PROJECT_NUMBER" --owner "$OWNER" --format json \ + --jq '.fields[] | select(.name == "Status") | .options[] | select(.name == "Todo") | .id' +)" +IN_PROGRESS_OPTION_ID="$( + gh project field-list "$PROJECT_NUMBER" --owner "$OWNER" --format json \ + --jq '.fields[] | select(.name == "Status") | .options[] | select(.name == "In Progress") | .id' +)" +DONE_OPTION_ID="$( + gh project field-list "$PROJECT_NUMBER" --owner "$OWNER" --format json \ + --jq '.fields[] | select(.name == "Status") | .options[] | select(.name == "Done") | .id' +)" + +# Resolve the Project item id for this issue; map via .content.number. +gh project item-list --owner "@me" --format json +ITEM_ID="$( + gh project item-list "$PROJECT_NUMBER" --owner "$OWNER" --format json \ + --jq ".items[] | select(.content.number == $ISSUE_NUMBER) | .id" +)" + +# Set the status by choosing the desired option id. +# Command shape: +# gh project item-edit --id --project-id --field-id --single-select-option-id +OPTION_ID="$IN_PROGRESS_OPTION_ID" # or "$TODO_OPTION_ID" / "$DONE_OPTION_ID" gh project item-edit \ - --id \ - --project-id \ - --field-id \ - --single-select-option-id + --id "$ITEM_ID" \ + --project-id "$PROJECT_ID" \ + --field-id "$STATUS_FIELD_ID" \ + --single-select-option-id "$OPTION_ID" ``` -Move an item to **In Progress** when the implementation agent starts its branch, -and to **Done** when the PR merges. This keeps the board honest without manual -bookkeeping by the user. +Use those commands for these transitions: + +- Move to **In Progress** when Step 3 cuts the implementation branch. +- Move to **Done** after Step 4 squash-merges the PR. + +This keeps the board honest without manual bookkeeping by the user. ## Linking the project to the repo diff --git a/tests/run.sh b/tests/run.sh index 8a5a8cd..7c8ff37 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -19,7 +19,7 @@ trap cleanup EXIT assert_contains() { file="$1" text="$2" - if ! grep -Fq "$text" "$file"; then + if ! grep -Fq -- "$text" "$file"; then echo "--- $file ---" >&2 cat "$file" >&2 fail "expected '$text' in $file" @@ -140,6 +140,26 @@ test_review_package_contains_stat_and_hunks() { assert_contains "$outfile" "+review package change" } +test_project_status_docs_are_wired_into_workflow() { + skill_doc="$ROOT/skills/issue-driven-github-flow/SKILL.md" + project_doc="$ROOT/skills/issue-driven-github-flow/references/projects.md" + + assert_contains "$project_doc" 'gh project view --owner "@me" --format json' + assert_contains "$project_doc" 'gh project field-list --owner "@me" --format json' + assert_contains "$project_doc" 'gh project item-list --owner "@me" --format json' + assert_contains "$project_doc" 'gh project item-edit' + assert_contains "$project_doc" '--project-id ' + assert_contains "$project_doc" '--single-select-option-id ' + assert_contains "$project_doc" 'Move to **In Progress** when Step 3 cuts the implementation branch' + assert_contains "$project_doc" 'Move to **Done** after Step 4 squash-merges the PR' + assert_contains "$project_doc" 'If there is no linked Project, treat status updates as a clean no-op' + + assert_contains "$skill_doc" 'Move the linked GitHub Project item to **In Progress**' + assert_contains "$skill_doc" 'Move the linked GitHub Project item to **Done**' + assert_contains "$skill_doc" '| Step 3 branch cut | Move Project item to **In Progress** via [projects.md](references/projects.md); no-op cleanly if no Project exists or Project auth is unavailable |' + assert_contains "$skill_doc" '| PR squash-merged | Move Project item to **Done** via [projects.md](references/projects.md); no-op cleanly if no Project exists or Project auth is unavailable |' +} + main() { rm -rf "$SCRATCH" mkdir -p "$SCRATCH" @@ -151,6 +171,7 @@ main() { test_rejects_empty_stage test_commit_message_contains_issue_and_coauthor test_review_package_contains_stat_and_hunks + test_project_status_docs_are_wired_into_workflow echo "All smoke tests passed" } From fc0e962095d500f8724a0e605b721f6fd050aa24 Mon Sep 17 00:00:00 2001 From: Andy Baran Date: Thu, 16 Jul 2026 16:36:41 -0400 Subject: [PATCH 2/2] fix(project): raise item-list page limit in status recipe Closes #8. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- skills/issue-driven-github-flow/references/projects.md | 4 ++-- tests/run.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/skills/issue-driven-github-flow/references/projects.md b/skills/issue-driven-github-flow/references/projects.md index 3861003..057257d 100644 --- a/skills/issue-driven-github-flow/references/projects.md +++ b/skills/issue-driven-github-flow/references/projects.md @@ -66,9 +66,9 @@ DONE_OPTION_ID="$( )" # Resolve the Project item id for this issue; map via .content.number. -gh project item-list --owner "@me" --format json +gh project item-list --owner "@me" --limit 100 --format json ITEM_ID="$( - gh project item-list "$PROJECT_NUMBER" --owner "$OWNER" --format json \ + gh project item-list "$PROJECT_NUMBER" --owner "$OWNER" --limit 100 --format json \ --jq ".items[] | select(.content.number == $ISSUE_NUMBER) | .id" )" diff --git a/tests/run.sh b/tests/run.sh index 7c8ff37..cb3ee8d 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -146,7 +146,7 @@ test_project_status_docs_are_wired_into_workflow() { assert_contains "$project_doc" 'gh project view --owner "@me" --format json' assert_contains "$project_doc" 'gh project field-list --owner "@me" --format json' - assert_contains "$project_doc" 'gh project item-list --owner "@me" --format json' + assert_contains "$project_doc" 'gh project item-list --owner "@me" --limit 100 --format json' assert_contains "$project_doc" 'gh project item-edit' assert_contains "$project_doc" '--project-id ' assert_contains "$project_doc" '--single-select-option-id '