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
20 changes: 18 additions & 2 deletions docs/skills/embedded-skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,16 +210,17 @@ The `github` skill provides a complete git + GitHub workflow through script-back
forge skills add github
```

This registers fourteen tools:
This registers fifteen tools:

| Tool | Purpose |
|------|---------|
| `github_clone` | Clone a repository and create a feature branch |
| `github_checkout` | Switch to or create a branch |
| `github_branch_name_from_ticket` | Generate a conventional branch name from a ticket ID + title (e.g. `ENG-123` + title → `feat/eng-123-add-invoice-creation-endpoint`). No network call — pure string transform. |
| `github_status` | Show git status for a cloned project |
| `github_commit` | Stage and commit changes |
| `github_push` | Push a feature branch to the remote |
| `github_create_pr` | Create a pull request |
| `github_create_pr` | Create a pull request. Optional `ticket_id` / `ticket_url` parameters auto-suffix the title with `[<ticket_id>]` and append a `Tracks:` back-link footer to the body. |
| `github_create_issue` | Create a GitHub issue |
| `github_list_issues` | List open issues for a repository |
| `github_list_prs` | List pull requests with state filter and pagination |
Expand All @@ -231,6 +232,21 @@ This registers fourteen tools:

**Workflow:** Clone → explore → edit → status → commit → push → create PR. The skill's system prompt enforces this sequence and prevents raw `git` commands via `cli_execute`.

**Workflow-completion rule:** the skill normally requires driving the full sequence in one session without stopping after exploration. Three exceptions allow pausing:

- **Ticket-driven mode** — if the task originated from a `linear_get_issue` call (or any external ticket) and the ticket leaves a material question unanswered, post a `linear_add_comment` asking the question, then stop and wait. Do not guess on irreversible decisions like API contract shape or data-model changes.
- **Code planning** — if `code_plan_create` returns `complexity: "high"` or non-empty `risks`, present the plan to the user and confirm before writing code.
- **Genuine ambiguity** — if you cannot determine what to change even after thorough exploration, stop and ask. Do not invent a change.

**Ticket-driven PR conventions:** when the work originates from a Linear ticket or GitHub issue:

1. Call `github_branch_name_from_ticket` first to generate a conventional branch name. Do not invent your own scheme like `claude/fix-thing`.
2. Pass `ticket_id` (and `ticket_url` if available) to `github_create_pr` — the skill builds the back-link footer automatically.
3. PR title format: `<type>(<scope>): <short description> [<ticket-id>]`. Examples: `feat(billing): add invoice creation endpoint [ENG-123]`, `fix(auth): reject empty refresh tokens [ENG-456]`, `chore(deps): bump go to 1.25.3 [INFRA-7]`.
4. After `github_create_pr` returns the PR URL, post a single comment back on the originating ticket using the tracker's skill (e.g. `linear_add_comment`). Do **not** post the PR URL into the PR itself.

`github_branch_name_from_ticket` accepts a `prefix` (default `feat`; allow-list `feat`/`fix`/`chore`/`docs`/`refactor`), lowercases the ticket ID, slugifies the title (lowercase, non-alnum → `-`, strip leading/trailing `-`), and truncates to 60 chars cutting at the last hyphen boundary so no half-word is emitted.

**Pagination:** List tools (`github_list_prs`, `github_list_stargazers`, `github_list_forks`, `github_pr_author_profiles`, `github_stargazer_profiles`) support `page` (1-based) and `per_page` (default 30, max 100) parameters. Responses include `pagination.has_next_page` to indicate more results are available.

**PII exemption:** Profile-returning tools (`github_get_user`, `github_pr_author_profiles`, `github_stargazer_profiles`) are pre-configured in the default policy scaffold's `no_pii` `allow_tools` list, so they can return public profile data (emails, bios) without triggering PII guardrails. See [Per-Tool PII Exemptions](../security/guardrails.md#per-tool-pii-exemptions).
Expand Down
59 changes: 55 additions & 4 deletions forge-skills/local/embedded/github/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ tags:
- stargazers
- forks
- users
- ticket-driven
- linear-compatible
description: Create issues, PRs, clone repos, and manage git workflows
metadata:
forge:
Expand Down Expand Up @@ -55,8 +57,17 @@ The `repo` parameter accepts any of these formats:
- ALL tools that accept `project_dir` (github tools, code-agent tools) accept BOTH `openclaw` and `workspace/openclaw` — the `workspace/` prefix is stripped automatically.
- For `directory_tree`, `grep_search`, `glob_search` use `workspace/<project_dir>` as the `path` (e.g. `workspace/openclaw`).

**You MUST complete the entire workflow — do NOT stop after exploring.**
When asked to fix a bug or make changes, you must: explore → understand → edit → commit → push → create PR. Do NOT stop after step 2 to report findings. Complete ALL steps in ONE session. Only stop early if you genuinely cannot determine what to change.
**Workflow completion rule:**

When asked to fix a bug or make changes, you must drive the full sequence: explore → understand → edit → commit → push → create PR. Do NOT stop after exploration to report findings. Complete the full sequence in one session.

**Exceptions where pausing is correct:**

- **Ticket-driven mode**: if the task originated from a `linear_get_issue` call (or any external ticket) and the ticket leaves a material question unanswered, post a `linear_add_comment` (or the equivalent for your tracker) asking the question, then stop and wait. Do NOT guess on irreversible decisions like API contract shape, data model changes, or user-facing copy.
- **Code planning**: if `code_plan_create` returns `complexity: "high"` or non-empty `risks`, present the plan to the user and confirm before writing code.
- **Genuine ambiguity**: if you cannot determine what to change even after thorough exploration, stop and ask. Do not invent a change.

Outside these exceptions, complete all steps in one session.

**Exploration strategy — bug fixes:**
1. `directory_tree` to understand project structure.
Expand Down Expand Up @@ -87,6 +98,28 @@ When asked to fix a bug or make changes, you must: explore → understand → ed
- `github_commit`, `github_push`, and `github_checkout` refuse to operate on main/master.
- Always use `github_status` before committing to review what changed.

**When working from a ticket**, prefer to:
1. Call `github_branch_name_from_ticket` first with the ticket ID and title.
2. Pass the resulting `branch` to `github_clone` via its `branch` parameter.

This produces consistent naming across runs. Do not invent branch names like `claude/fix-thing` or `agent-branch-1`.

**Ticket-driven PR conventions:**

When the work originates from a Linear ticket or GitHub issue:

1. Use `github_branch_name_from_ticket` to generate a conventional branch name from the identifier and title. Do not invent your own naming scheme.
2. Pass `ticket_id` (and `ticket_url` if available) to `github_create_pr`. The skill builds the PR body with a back-link automatically.
3. The PR title format is: `<type>(<scope>): <short description> [<ticket-id>]` — examples below.
4. After `github_create_pr` returns the PR URL, post a comment back on the originating ticket with the PR URL using the appropriate tracker skill. Do NOT post the PR URL into the PR itself as a comment.

Title examples:
- `feat(billing): add invoice creation endpoint [ENG-123]`
- `fix(auth): reject empty refresh tokens [ENG-456]`
- `chore(deps): bump go to 1.25.3 [INFRA-7]`

If no clear conventional-commit type applies, use `chore`. Never invent a type like `wip` or `tmp`.

**Pagination:**
For tools that return lists (`github_list_prs`, `github_list_stargazers`, `github_list_forks`, `github_pr_author_profiles`, `github_stargazer_profiles`), use `page` (1-based) and `per_page` (default 30, max 100) parameters. The response includes a `pagination` object with `has_next_page` — if true, increment `page` to fetch the next batch.

Expand Down Expand Up @@ -125,6 +158,13 @@ Switch to or create a branch. Refuses to switch to main/master.
**Input:** project_dir (string), branch (string: target branch name), create (boolean, optional: create new branch — default false)
**Output:** `{status, branch}`

## Tool: github_branch_name_from_ticket

Generate a conventional branch name from a ticket identifier and title. Use this instead of inventing branch names manually — it ensures consistency across agents and humans. No network call.

**Input:** ticket_id (string), title (string), prefix (string, optional: `feat`/`fix`/`chore`/`docs`/`refactor`, default `feat`)
**Output:** `{branch: "feat/eng-123-add-invoice-creation-endpoint"}`

## Tool: github_create_issue

Create a GitHub issue.
Expand All @@ -141,9 +181,20 @@ List open issues for a repository.

## Tool: github_create_pr

Create a pull request.
Create a pull request. When `ticket_id` is supplied, the title is auto-suffixed with `[<ticket_id>]` (if not already present) and a `Tracks:` back-link footer is appended to the body — the LLM does not need to format the back-link manually.

**Input:**

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| repo | string | yes | `owner/repo`, SSH URL, or HTTPS URL |
| title | string | yes | PR title |
| body | string | yes | PR body (markdown) |
| head | string | yes | Source branch |
| base | string | no | Target branch. Default `main`. |
| ticket_id | string | no | External ticket identifier (e.g. `ENG-123`). Appended to title and PR body if not already present. |
| ticket_url | string | no | Full URL to the ticket. When provided alongside `ticket_id`, added as a back-link in the PR body as `Tracks: [<ticket_id>](<ticket_url>)`. |

**Input:** repo (string), title (string), body (string), head (string), base (string)
**Output:** Pull request URL

## Tool: github_list_prs
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# github-branch-name-from-ticket.sh — Generate a conventional branch name
# from a ticket identifier and title.
# Usage: ./github-branch-name-from-ticket.sh '{"ticket_id":"ENG-123","title":"Add invoice creation"}'
#
# No network call. Pure string transformation. Output:
# {"branch": "<prefix>/<lower-id>-<slug>"}
set -euo pipefail

INPUT_JSON="${1:-$(cat)}"
if ! printf '%s' "$INPUT_JSON" | jq empty 2>/dev/null; then
echo '{"error": "invalid JSON input"}' >&2
exit 1
fi

TICKET_ID="$(printf '%s' "$INPUT_JSON" | jq -r '.ticket_id // empty')"
TITLE="$(printf '%s' "$INPUT_JSON" | jq -r '.title // empty')"
PREFIX="$(printf '%s' "$INPUT_JSON" | jq -r '.prefix // "feat"')"

if [ -z "$TICKET_ID" ]; then
echo '{"error": "ticket_id is required"}' >&2
exit 1
fi
if [ -z "$TITLE" ]; then
echo '{"error": "title is required"}' >&2
exit 1
fi

# Validate prefix against allow-list; fall back to feat for any other value.
case "$PREFIX" in
feat|fix|chore|docs|refactor) ;;
*) PREFIX="feat" ;;
esac

LOWER_ID="$(printf '%s' "$TICKET_ID" | tr '[:upper:]' '[:lower:]')"

# Slugify: lowercase, non-alnum → -, collapse repeats, trim leading/trailing -.
SLUG="$(printf '%s' "$TITLE" \
| tr '[:upper:]' '[:lower:]' \
| sed -E 's/[^a-z0-9]+/-/g' \
| sed -E 's/^-+//' \
| sed -E 's/-+$//')"

# Truncate slug to 60 chars; if a hyphen exists in the truncated string,
# cut back to it so we don't leave a half-word at the end.
if [ "${#SLUG}" -gt 60 ]; then
SLUG="${SLUG:0:60}"
case "$SLUG" in
*-*) SLUG="${SLUG%-*}" ;;
esac
fi

BRANCH="$PREFIX/$LOWER_ID-$SLUG"

jq -n --arg branch "$BRANCH" '{branch: $branch}'
25 changes: 25 additions & 0 deletions forge-skills/local/embedded/github/scripts/github-create-pr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ TITLE=$(printf '%s' "$INPUT" | jq -r '.title // empty')
BODY=$(printf '%s' "$INPUT" | jq -r '.body // empty')
HEAD=$(printf '%s' "$INPUT" | jq -r '.head // empty')
BASE=$(printf '%s' "$INPUT" | jq -r '.base // empty')
TICKET_ID=$(printf '%s' "$INPUT" | jq -r '.ticket_id // empty')
TICKET_URL=$(printf '%s' "$INPUT" | jq -r '.ticket_url // empty')

if [ -z "$REPO" ]; then
echo '{"error": "repo is required"}' >&2
Expand Down Expand Up @@ -52,6 +54,29 @@ if [ -z "$BASE" ]; then
BASE="main"
fi

# --- Apply ticket-driven conventions ---
# When ticket_id is provided, suffix the title with [<ticket_id>] if not
# already present, and append a "Tracks:" back-link footer to the body.
# This lets callers pass an unmodified title/body and have the skill
# enforce the convention uniformly.
if [ -n "$TICKET_ID" ]; then
case "$TITLE" in
*"[$TICKET_ID]"*) ;;
*) TITLE="$TITLE [$TICKET_ID]" ;;
esac
if [ -n "$TICKET_URL" ]; then
BODY="$BODY

---
Tracks: [$TICKET_ID]($TICKET_URL)"
else
BODY="$BODY

---
Tracks: $TICKET_ID"
fi
fi

# --- Create PR via gh CLI ---
PR_URL=$(gh pr create \
--repo "$REPO" \
Expand Down
Loading