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
10 changes: 10 additions & 0 deletions skills/issue-driven-github-flow/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 <squash-sha>` on a new branch; open a revert PR |
| Secret requested | Refuse to commit it; use [security.md](references/security.md) |
Expand Down
67 changes: 56 additions & 11 deletions skills/issue-driven-github-flow/references/projects.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <project-number> --owner "@me" --format json
PROJECT_NUMBER=<project-number>
ISSUE_NUMBER=<issue-number>
OWNER="@me" # for org-owned repos, use the org login instead

# Resolve the Project id.
gh project view <project-number> --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 <project-number> --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 <project-number> --owner "@me" --limit 100 --format json
ITEM_ID="$(
gh project item-list "$PROJECT_NUMBER" --owner "$OWNER" --limit 100 --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 <item-id> --project-id <project-id> --field-id <status-field-id> --single-select-option-id <option-id>
OPTION_ID="$IN_PROGRESS_OPTION_ID" # or "$TODO_OPTION_ID" / "$DONE_OPTION_ID"
gh project item-edit \
--id <item-id> \
--project-id <project-id> \
--field-id <status-field-id> \
--single-select-option-id <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

Expand Down
23 changes: 22 additions & 1 deletion tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 <project-number> --owner "@me" --format json'
assert_contains "$project_doc" 'gh project field-list <project-number> --owner "@me" --format json'
assert_contains "$project_doc" 'gh project item-list <project-number> --owner "@me" --limit 100 --format json'
assert_contains "$project_doc" 'gh project item-edit'
assert_contains "$project_doc" '--project-id <project-id>'
assert_contains "$project_doc" '--single-select-option-id <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"
Expand All @@ -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"
}
Expand Down
Loading