Initial commit
This commit is contained in:
506
skills/github-task-sync/SKILL.md
Normal file
506
skills/github-task-sync/SKILL.md
Normal file
@@ -0,0 +1,506 @@
|
||||
---
|
||||
name: github-task-sync
|
||||
description: Manage task documentation by syncing between local task directories and GitHub issues
|
||||
---
|
||||
|
||||
# GitHub Task Sync Skill
|
||||
|
||||
Seamlessly manage task documentation by syncing between local task directories and GitHub issues. All task documentation (SPEC, PLAN, TEST_PLAN, COMMIT_MESSAGE) lives both locally and on GitHub, with easy push/pull synchronization.
|
||||
|
||||
## Overview
|
||||
|
||||
This skill provides a complete workflow for managing tasks:
|
||||
|
||||
1. **Create** a new GitHub issue with `create-issue.sh`
|
||||
2. **Push** local task files to GitHub with `push.sh` or `push-file.sh`
|
||||
3. **Pull** task files from GitHub with `pull.sh` or `pull-file.sh`
|
||||
4. **Read** task files from GitHub to stdout with `read-issue-file.sh`
|
||||
5. **Log** work progress with `log-entry.sh` (creates AI Work Log comment with timestamped entries)
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Create a new GitHub issue and task directory
|
||||
./create-issue.sh "Add dark mode toggle" "Implement dark/light theme switcher"
|
||||
|
||||
# Work on files locally (SPEC.md, PLAN.md, etc.)
|
||||
|
||||
# Push all files to GitHub
|
||||
./push.sh 188 ./tasks/188-add-dark-mode-toggle
|
||||
|
||||
# Or pull the latest from GitHub (automatically creates task directory from issue title)
|
||||
./pull.sh 188
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
There are 7 scripts in this skill:
|
||||
|
||||
1. **create-issue.sh** - Create GitHub issue and initialize task directory
|
||||
2. **push.sh** - Push all task files to GitHub
|
||||
3. **push-file.sh** - Push single task file with status summary
|
||||
4. **pull.sh** - Pull all task files from GitHub
|
||||
5. **pull-file.sh** - Pull single task file from GitHub
|
||||
6. **read-issue-file.sh** - Read task file from GitHub to stdout
|
||||
7. **log-entry.sh** - Add timestamped entry to AI Work Log
|
||||
|
||||
### create-issue.sh
|
||||
|
||||
Create a new GitHub issue and initialize a task directory. Can also convert existing task directories to GitHub issues. Automatically applies GitHub labels based on issue context.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
./create-issue.sh <title> [description] [existing-task-dir] [labels]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `title` - GitHub issue title
|
||||
- `description` - Issue description (optional)
|
||||
- `existing-task-dir` - Path to existing task directory to convert (optional)
|
||||
- `labels` - Comma-separated labels to apply (optional, e.g., "UI,bug" or "CLI,feature")
|
||||
|
||||
**Available labels:**
|
||||
- `UI` - User interface related issues
|
||||
- `CLI` - Command-line interface related issues
|
||||
- `bug` - Bug fixes and issue resolutions
|
||||
- `feature` - New features and enhancements
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Create new issue with title only
|
||||
./create-issue.sh "Add dark mode toggle"
|
||||
|
||||
# Create new issue with title, description, and labels
|
||||
./create-issue.sh "Add dark mode toggle" "Implement dark/light theme switcher in settings" "" "UI,feature"
|
||||
|
||||
# Convert existing task directory to GitHub issue with labels
|
||||
./create-issue.sh "Fix login button styling" "" ./tasks/login-styling "UI,bug"
|
||||
|
||||
# Create issue with description and labels (no existing task dir)
|
||||
./create-issue.sh "Add date filter to extract" "Filter commits by date range" "" "CLI,feature"
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
1. Analyzes issue content to determine appropriate labels (optional)
|
||||
2. Creates a new GitHub issue with the provided title, description, and labels
|
||||
3. Creates local task directory named `{issue-number}-{title-slug}/`
|
||||
4. If task files exist, automatically syncs them to GitHub
|
||||
5. Outputs issue URL and task directory path
|
||||
|
||||
**Output:**
|
||||
```
|
||||
Creating GitHub issue...
|
||||
Applying labels: UI,feature
|
||||
✓ GitHub issue created: https://github.com/<github-user>/<repo-name>/issues/189
|
||||
✓ Created task directory: tasks/189-add-dark-mode-toggle
|
||||
|
||||
✅ Task setup complete!
|
||||
Issue: https://github.com/<github-user>/<repo-name>/issues/189
|
||||
Task Directory: tasks/189-add-dark-mode-toggle
|
||||
Task Number: 189
|
||||
```
|
||||
|
||||
### push.sh
|
||||
|
||||
Push all task documentation files (SPEC.md, PLAN.md, TEST_PLAN.md, COMMIT_MESSAGE.md) to a GitHub issue as collapsible comments.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
./push.sh <issue-url-or-number> [task-directory]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `issue-url-or-number` - Full GitHub URL or just the issue number
|
||||
- `task-directory` - Directory containing task files (optional, defaults to current directory)
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Using issue number
|
||||
./push.sh 188 ./tasks/188-account-deletion
|
||||
|
||||
# Using full URL
|
||||
./push.sh https://github.com/<github-user>/<repo-name>/issues/188 ./tasks/188-account-deletion
|
||||
|
||||
# Using current directory
|
||||
./push.sh 188
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- Uploads all four task file types as separate collapsible comments
|
||||
- Each file type gets a unique marker so it can be updated independently
|
||||
- Creates new comments or updates existing ones
|
||||
- Each file wrapped in `<details>` section that starts collapsed
|
||||
|
||||
**Output:**
|
||||
```
|
||||
📤 Syncing task files to GitHub issue #188 in <github-user>/<repo-name>
|
||||
|
||||
Processing SPEC.md...
|
||||
+ Creating new comment...
|
||||
✓ Created
|
||||
|
||||
Processing PLAN.md...
|
||||
↻ Updating existing comment (ID: 123456789)...
|
||||
✓ Updated
|
||||
|
||||
...
|
||||
✅ Sync complete!
|
||||
View the issue: https://github.com/<github-user>/<repo-name>/issues/188
|
||||
```
|
||||
|
||||
### push-file.sh
|
||||
|
||||
Update a single task file comment on a GitHub issue with a status summary and file content.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
./push-file.sh <issue-url-or-number> <file-type> <status-file> <content-file>
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `issue-url-or-number` - GitHub issue URL or issue number
|
||||
- `file-type` - One of: `SPEC`, `PLAN`, `TEST_PLAN`, `COMMIT_MESSAGE`
|
||||
- `status-file` - File containing status summary (2 paragraphs + optional bullets)
|
||||
- `content-file` - File containing the full file content
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Update SPEC with status and content
|
||||
./push-file.sh 188 SPEC SPEC-STATUS.md SPEC.md
|
||||
|
||||
# Update PLAN after review
|
||||
./push-file.sh 188 PLAN plan-status.txt PLAN.md
|
||||
```
|
||||
|
||||
**Status File Format:**
|
||||
The status file should contain a 2-paragraph summary describing the document state:
|
||||
|
||||
```markdown
|
||||
**Status:** [Draft | Complete | Review Needed | etc.]
|
||||
|
||||
This is the first paragraph explaining the current state of the document.
|
||||
It should describe what has been completed, what's pending, or any key status information.
|
||||
|
||||
This is the second paragraph providing additional context or details about the document state.
|
||||
|
||||
- Key point 1 (optional)
|
||||
- Key point 2 (optional)
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- Creates or updates a single comment for the specified file type
|
||||
- Combines the status summary with the file content in a collapsible section
|
||||
- Each file type has a unique marker for independent updates
|
||||
|
||||
**Output:**
|
||||
```
|
||||
↻ Updating SPEC comment on issue #188 (ID: 123456789)...
|
||||
✓ Updated successfully
|
||||
|
||||
View the issue: https://github.com/<github-user>/<repo-name>/issues/188
|
||||
```
|
||||
|
||||
### pull.sh
|
||||
|
||||
Pull all task documentation files from a GitHub issue to a local task directory. **Automatically determines the task directory name** from the issue title.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
./pull.sh <issue-url-or-number>
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `issue-url-or-number` - GitHub issue URL or issue number
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Pull using issue number
|
||||
./pull.sh 188
|
||||
|
||||
# Pull using full URL
|
||||
./pull.sh https://github.com/<github-user>/<repo-name>/issues/188
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
1. Fetches the issue title from GitHub
|
||||
2. Converts the title to a URL-safe slug
|
||||
3. Creates task directory as `tasks/{issue-number}-{title-slug}/`
|
||||
4. Fetches all four task files from GitHub issue comments
|
||||
5. Extracts content from collapsible sections
|
||||
6. Writes each to local file (SPEC.md, PLAN.md, etc.)
|
||||
|
||||
**Output:**
|
||||
```
|
||||
📥 Fetching issue #188 from <github-user>/<repo-name>...
|
||||
📥 Pulling task files from GitHub issue #188: "Account deletion and data export"
|
||||
📁 Task directory: tasks/188-account-deletion-and-data-export
|
||||
|
||||
Pulling SPEC.md...
|
||||
✓ Pulled to SPEC.md
|
||||
|
||||
Pulling PLAN.md...
|
||||
✓ Pulled to PLAN.md
|
||||
|
||||
...
|
||||
✅ Pull complete!
|
||||
Task directory: tasks/188-account-deletion-and-data-export
|
||||
```
|
||||
|
||||
### pull-file.sh
|
||||
|
||||
Pull a single task file from a GitHub issue to a local file.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
./pull-file.sh <issue-url-or-number> <file-type> [output-file]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `issue-url-or-number` - GitHub issue URL or issue number
|
||||
- `file-type` - One of: `SPEC`, `PLAN`, `TEST_PLAN`, `COMMIT_MESSAGE`
|
||||
- `output-file` - File to write to (default: `{file-type}.md` in current directory)
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Pull SPEC to SPEC.md
|
||||
./pull-file.sh 188 SPEC
|
||||
|
||||
# Pull PLAN to specific file
|
||||
./pull-file.sh 188 PLAN ./my-plan.md
|
||||
|
||||
# Pull and pipe to stdout
|
||||
./pull-file.sh 188 SPEC | head -20
|
||||
```
|
||||
|
||||
**Output:**
|
||||
Pure file content (great for piping or redirecting)
|
||||
|
||||
### read-issue-file.sh
|
||||
|
||||
Read a task file from a GitHub issue and output to stdout. Useful for debugging, piping, or quick content inspection.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
./read-issue-file.sh <issue-url-or-number> <file-type>
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `issue-url-or-number` - GitHub issue URL or issue number
|
||||
- `file-type` - One of: `SPEC`, `PLAN`, `TEST_PLAN`, `COMMIT_MESSAGE`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Read SPEC to stdout
|
||||
./read-issue-file.sh 188 SPEC
|
||||
|
||||
# Pipe to file
|
||||
./read-issue-file.sh 188 PLAN > my-plan.md
|
||||
|
||||
# View first 20 lines
|
||||
./read-issue-file.sh 188 SPEC | head -20
|
||||
```
|
||||
|
||||
**Output:**
|
||||
Pure file content sent to stdout
|
||||
|
||||
### log-entry.sh
|
||||
|
||||
Add timestamped entries to a task's AI Work Log on a GitHub issue. Creates or updates a running log of work progress throughout the task lifecycle.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
./log-entry.sh <issue-url-or-number> <entry-text>
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `issue-url-or-number` - GitHub issue URL or issue number
|
||||
- `entry-text` - Description of work being done (e.g., "Started writing spec")
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Log that spec writing started
|
||||
./log-entry.sh 188 "Started writing spec"
|
||||
|
||||
# Log that plan writing finished
|
||||
./log-entry.sh 188 "Finished writing plan"
|
||||
|
||||
# Use full URL
|
||||
./log-entry.sh https://github.com/<github-user>/<repo-name>/issues/190 "Started implementation"
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- Creates a new "AI Work Log" comment on the issue if it doesn't exist
|
||||
- Appends timestamped entries to the work log (one per line with format: `- YYYY-MM-DD HH:MM:SS: entry-text`)
|
||||
- Each entry is timestamped and represents a work milestone
|
||||
- Useful for tracking progress through spec writing, planning, implementation, testing, and completion
|
||||
|
||||
**Output:**
|
||||
```
|
||||
↻ Adding entry to work log on issue #188...
|
||||
✓ Entry added
|
||||
|
||||
View the issue: https://github.com/<github-user>/<repo-name>/issues/188
|
||||
```
|
||||
|
||||
## Task Directory Structure
|
||||
|
||||
When using `create-issue.sh`, directories are automatically named with the issue number:
|
||||
|
||||
```
|
||||
tasks/
|
||||
├── 188-account-deletion/
|
||||
│ ├── SPEC.md (Specification)
|
||||
│ ├── PLAN.md (Implementation plan)
|
||||
│ ├── TEST_PLAN.md (Test scenarios)
|
||||
│ └── COMMIT_MESSAGE.md (Git commit message)
|
||||
├── 189-add-dark-mode/
|
||||
│ └── [similar structure]
|
||||
└── archive/
|
||||
└── [completed tasks]
|
||||
```
|
||||
|
||||
**Naming Convention:** `{issue-number}-{task-name-slug}`
|
||||
|
||||
The issue number in the directory name provides direct reference to the GitHub issue.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Creating a New Task
|
||||
|
||||
```bash
|
||||
# 1. Create GitHub issue and task directory
|
||||
./create-issue.sh "Add authentication" "Implement magic link authentication"
|
||||
|
||||
# 2. Log that work is starting
|
||||
./log-entry.sh 190 "Started writing spec"
|
||||
|
||||
# 3. Work on files locally
|
||||
# - Create SPEC.md
|
||||
# - Create PLAN.md
|
||||
# - Create TEST_PLAN.md
|
||||
# - Create COMMIT_MESSAGE.md
|
||||
|
||||
# 4. Push files to GitHub
|
||||
./push.sh 190 ./tasks/190-add-authentication
|
||||
|
||||
# 5. Log work progress
|
||||
./log-entry.sh 190 "Finished writing spec"
|
||||
./log-entry.sh 190 "Started writing plan"
|
||||
|
||||
# 6. Continue development...
|
||||
# When you update files, push again
|
||||
./push.sh 190 ./tasks/190-add-authentication
|
||||
./log-entry.sh 190 "Finished writing plan"
|
||||
./log-entry.sh 190 "Started implementation"
|
||||
```
|
||||
|
||||
### Converting Existing Tasks to GitHub Issues
|
||||
|
||||
If you have an existing task directory without a GitHub issue:
|
||||
|
||||
```bash
|
||||
# 1. Create GitHub issue from existing directory
|
||||
./create-issue.sh "My feature" "Description" ./tasks/my-feature
|
||||
|
||||
# 2. Files are automatically synced to GitHub
|
||||
# Task directory renamed to: tasks/191-my-feature
|
||||
```
|
||||
|
||||
### Syncing During Work
|
||||
|
||||
**Push workflow** (local → GitHub):
|
||||
```bash
|
||||
# Log that you're starting work
|
||||
./log-entry.sh 188 "Started writing code"
|
||||
|
||||
# Update single file on GitHub with status
|
||||
./push-file.sh 188 SPEC SPEC-STATUS.md SPEC.md
|
||||
|
||||
# Update all files on GitHub
|
||||
./push.sh 188 ./tasks/188-account-deletion
|
||||
|
||||
# Log when work is complete
|
||||
./log-entry.sh 188 "Finished writing code"
|
||||
```
|
||||
|
||||
**Pull workflow** (GitHub → local):
|
||||
```bash
|
||||
# Pull all files from GitHub
|
||||
./pull.sh 188 ./tasks/188-account-deletion
|
||||
|
||||
# Pull single file from GitHub
|
||||
./pull-file.sh 188 PLAN
|
||||
|
||||
# Log that you pulled latest
|
||||
./log-entry.sh 188 "Pulled latest files from GitHub"
|
||||
```
|
||||
|
||||
### Task Completion
|
||||
|
||||
When finishing a task (via `/finish` command):
|
||||
|
||||
1. All work is complete and tested
|
||||
2. Run `push.sh` one final time to sync latest versions
|
||||
3. Task directory is archived from `tasks/` to `tasks/archive/`
|
||||
4. GitHub issue remains as permanent record
|
||||
|
||||
## Key Features
|
||||
|
||||
- ✅ **Bidirectional sync** - Push changes to GitHub or pull from GitHub
|
||||
- ✅ **Selective updates** - Push/pull individual files or all at once
|
||||
- ✅ **Status tracking** - Each file can have a 2-paragraph status summary
|
||||
- ✅ **Collapsible display** - Large files stay organized on GitHub
|
||||
- ✅ **AI Work Log** - Timestamped activity log tracking progress (spec writing, planning, implementation, etc.)
|
||||
- ✅ **Issue creation** - Automatically initialize task structure
|
||||
- ✅ **Directory conversion** - Convert existing tasks to GitHub issues
|
||||
- ✅ **No git commits** - Task files never committed (in `.gitignore`)
|
||||
- ✅ **GitHub-centric** - Documentation source of truth lives on GitHub
|
||||
|
||||
## Requirements
|
||||
|
||||
- `gh` CLI installed and authenticated
|
||||
- Bash shell
|
||||
- Read/write access to the GitHub repository
|
||||
- Git repository with GitHub remote (for auto-detection)
|
||||
|
||||
## Integration with Other Commands
|
||||
|
||||
**With `/write-spec`:**
|
||||
- Creates SPEC.md locally
|
||||
- Agent calls `push-file.sh` to sync status + content to GitHub
|
||||
|
||||
**With `/write-plan`:**
|
||||
- Creates PLAN.md locally
|
||||
- Agent calls `push-file.sh` to sync to GitHub
|
||||
|
||||
**With `/finish`:**
|
||||
- Calls `push.sh` to sync all files to GitHub as final step
|
||||
- Task archived and GitHub issue contains complete documentation
|
||||
|
||||
## Setup & Configuration
|
||||
|
||||
The scripts **automatically detect** the GitHub repository from your current git remote (origin). No configuration needed!
|
||||
|
||||
**Repository Detection:**
|
||||
1. **Auto-detect** (recommended): Scripts automatically extract owner/repo from `git remote get-url origin`
|
||||
- Supports both HTTPS: `https://github.com/owner/repo.git`
|
||||
- Supports SSH: `git@github.com:owner/repo.git`
|
||||
|
||||
2. **Environment variables** (optional override):
|
||||
```bash
|
||||
export GITHUB_OWNER="myorg"
|
||||
export GITHUB_REPO="myrepo"
|
||||
```
|
||||
|
||||
3. **Full URLs** (always works):
|
||||
```bash
|
||||
# Use full URL instead of issue number to override auto-detection
|
||||
./push.sh "https://github.com/otherorg/otherrepo/issues/42" ./tasks/42-myfeature
|
||||
```
|
||||
|
||||
**Error Handling:**
|
||||
If you run scripts outside a git repository or without a GitHub remote, you'll see a helpful error:
|
||||
```
|
||||
Error: Not in a git repository
|
||||
Please run this command from within a git repository, or set GITHUB_OWNER and GITHUB_REPO environment variables
|
||||
```
|
||||
141
skills/github-task-sync/create-issue.sh
Executable file
141
skills/github-task-sync/create-issue.sh
Executable file
@@ -0,0 +1,141 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Script to create a GitHub issue and initialize a task directory
|
||||
# Can also convert an existing task directory to a GitHub issue
|
||||
# Usage: ./create-issue.sh <title> [description] [existing-task-dir] [labels]
|
||||
# If existing-task-dir is provided, converts that directory to a GitHub issue
|
||||
# Labels should be comma-separated (e.g., "UI,bug" or "CLI,feature")
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "Usage: $0 <title> [description] [existing-task-dir] [labels]"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " title Issue title"
|
||||
echo " description Issue description (optional)"
|
||||
echo " existing-task-dir Path to existing task directory to convert (optional)"
|
||||
echo " labels Comma-separated labels to apply (optional)"
|
||||
echo ""
|
||||
echo "Available labels: UI, CLI, bug, feature"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 'Add dark mode toggle'"
|
||||
echo " $0 'Add dark mode toggle' 'Implement dark/light theme switcher' '' 'UI,feature'"
|
||||
echo " $0 'Fix authentication bug' '' ./tasks/existing-task 'bug'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TITLE="$1"
|
||||
DESCRIPTION="${2:-}"
|
||||
EXISTING_TASK_DIR="${3:-}"
|
||||
LABELS="${4:-}"
|
||||
|
||||
# Detect GitHub repository from git remote or environment
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
# shellcheck source=lib-repo-detect.sh
|
||||
source "$SCRIPT_DIR/lib-repo-detect.sh"
|
||||
|
||||
if ! detect_github_repo; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OWNER="$REPO_OWNER"
|
||||
REPO="$REPO_NAME"
|
||||
REPO_FULL="$OWNER/$REPO"
|
||||
|
||||
echo "Creating GitHub issue..."
|
||||
[ -n "$LABELS" ] && echo "Applying labels: $LABELS"
|
||||
|
||||
# Build gh issue create command with optional labels
|
||||
GH_ARGS=("issue" "create" "--repo" "$REPO_FULL" "--title" "$TITLE")
|
||||
|
||||
if [ -n "$DESCRIPTION" ]; then
|
||||
GH_ARGS+=("--body" "$DESCRIPTION")
|
||||
fi
|
||||
|
||||
if [ -n "$LABELS" ]; then
|
||||
GH_ARGS+=("--label" "$LABELS")
|
||||
fi
|
||||
|
||||
ISSUE_URL=$(gh "${GH_ARGS[@]}" 2>/dev/null)
|
||||
|
||||
if [ -z "$ISSUE_URL" ]; then
|
||||
echo "Error: Failed to create GitHub issue"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ GitHub issue created: $ISSUE_URL"
|
||||
|
||||
# Extract issue number from URL
|
||||
if [[ $ISSUE_URL =~ /issues/([0-9]+)$ ]]; then
|
||||
ISSUE_NUM="${BASH_REMATCH[1]}"
|
||||
else
|
||||
echo "Error: Could not extract issue number from URL: $ISSUE_URL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine task directory name
|
||||
TASK_NAME=$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/-$//')
|
||||
TASK_DIR="tasks/${ISSUE_NUM}-${TASK_NAME}"
|
||||
|
||||
# Handle existing task directory conversion
|
||||
if [ -n "$EXISTING_TASK_DIR" ]; then
|
||||
if [ ! -d "$EXISTING_TASK_DIR" ]; then
|
||||
echo "Error: Existing task directory not found: $EXISTING_TASK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Converting existing task directory..."
|
||||
|
||||
# Check if task directory already exists with the new name
|
||||
if [ -d "$TASK_DIR" ]; then
|
||||
echo "Error: Task directory already exists: $TASK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Move existing directory to new location
|
||||
mv "$EXISTING_TASK_DIR" "$TASK_DIR"
|
||||
echo "✓ Renamed $EXISTING_TASK_DIR to $TASK_DIR"
|
||||
else
|
||||
# Create new task directory
|
||||
mkdir -p "$TASK_DIR"
|
||||
echo "✓ Created task directory: $TASK_DIR"
|
||||
fi
|
||||
|
||||
# Check if task files exist and push them
|
||||
echo ""
|
||||
echo "Checking for task files to sync..."
|
||||
|
||||
HAS_FILES=0
|
||||
if [ -f "$TASK_DIR/SPEC.md" ] || [ -f "$TASK_DIR/PLAN.md" ] || \
|
||||
[ -f "$TASK_DIR/TEST_PLAN.md" ] || [ -f "$TASK_DIR/COMMIT_MESSAGE.md" ]; then
|
||||
HAS_FILES=1
|
||||
fi
|
||||
|
||||
if [ $HAS_FILES -eq 1 ]; then
|
||||
echo "Syncing task files to GitHub issue..."
|
||||
|
||||
# Use the push.sh script to sync all files
|
||||
PUSH_SCRIPT="$(dirname "$0")/push.sh"
|
||||
if [ -x "$PUSH_SCRIPT" ]; then
|
||||
"$PUSH_SCRIPT" "$ISSUE_NUM" "$TASK_DIR"
|
||||
else
|
||||
echo "Warning: Could not find push.sh script to sync files"
|
||||
fi
|
||||
else
|
||||
echo "No task files found to sync (create SPEC.md, PLAN.md, etc. to sync them)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "✅ Task setup complete!"
|
||||
echo "========================================="
|
||||
echo "Issue: $ISSUE_URL"
|
||||
echo "Task Directory: $TASK_DIR"
|
||||
echo "Task Number: $ISSUE_NUM"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Create SPEC.md, PLAN.md, TEST_PLAN.md, COMMIT_MESSAGE.md in $TASK_DIR"
|
||||
echo "2. Run 'push.sh $ISSUE_NUM $TASK_DIR' to sync files to GitHub"
|
||||
echo "3. Run '/write-spec' or other commands to begin work"
|
||||
86
skills/github-task-sync/lib-repo-detect.sh
Normal file
86
skills/github-task-sync/lib-repo-detect.sh
Normal file
@@ -0,0 +1,86 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Shared library for detecting GitHub repository from git remote
|
||||
# Usage: source this file and call detect_github_repo
|
||||
|
||||
# Extracts owner and repo from a GitHub URL
|
||||
# Supports both HTTPS and SSH formats:
|
||||
# https://github.com/owner/repo.git
|
||||
# git@github.com:owner/repo.git
|
||||
extract_repo_from_url() {
|
||||
local url="$1"
|
||||
|
||||
# Remove .git suffix if present
|
||||
url="${url%.git}"
|
||||
|
||||
# Handle HTTPS format: https://github.com/owner/repo
|
||||
if [[ $url =~ https://github\.com/([^/]+)/([^/]+) ]]; then
|
||||
echo "${BASH_REMATCH[1]}/${BASH_REMATCH[2]}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Handle SSH format: git@github.com:owner/repo
|
||||
if [[ $url =~ git@github\.com:([^/]+)/(.+) ]]; then
|
||||
echo "${BASH_REMATCH[1]}/${BASH_REMATCH[2]}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Detects GitHub repository from environment or git remote
|
||||
# Sets REPO_OWNER and REPO_NAME global variables
|
||||
# Returns 0 on success, 1 on failure
|
||||
detect_github_repo() {
|
||||
# Method 1: Use environment variables if set
|
||||
if [ -n "${GITHUB_OWNER:-}" ] && [ -n "${GITHUB_REPO:-}" ]; then
|
||||
REPO_OWNER="$GITHUB_OWNER"
|
||||
REPO_NAME="$GITHUB_REPO"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Method 2: Extract from git remote
|
||||
if ! command -v git &> /dev/null; then
|
||||
echo "Error: git command not found" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if we're in a git repository
|
||||
if ! git rev-parse --git-dir &> /dev/null; then
|
||||
echo "Error: Not in a git repository" >&2
|
||||
echo "Please run this command from within a git repository, or set GITHUB_OWNER and GITHUB_REPO environment variables" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Get the origin remote URL
|
||||
local remote_url
|
||||
remote_url=$(git config --get remote.origin.url 2>/dev/null)
|
||||
|
||||
if [ -z "$remote_url" ]; then
|
||||
echo "Error: No 'origin' remote found in git repository" >&2
|
||||
echo "Please add a GitHub remote or set GITHUB_OWNER and GITHUB_REPO environment variables" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Extract owner/repo from URL
|
||||
local repo_full
|
||||
repo_full=$(extract_repo_from_url "$remote_url")
|
||||
|
||||
if [ -z "$repo_full" ]; then
|
||||
echo "Error: Could not extract GitHub repository from remote URL: $remote_url" >&2
|
||||
echo "Please ensure the remote is a GitHub URL, or set GITHUB_OWNER and GITHUB_REPO environment variables" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Split into owner and repo
|
||||
REPO_OWNER="${repo_full%%/*}"
|
||||
REPO_NAME="${repo_full#*/}"
|
||||
|
||||
# Validate we got both parts
|
||||
if [ -z "$REPO_OWNER" ] || [ -z "$REPO_NAME" ]; then
|
||||
echo "Error: Failed to parse repository owner and name from: $repo_full" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
113
skills/github-task-sync/log-entry.sh
Executable file
113
skills/github-task-sync/log-entry.sh
Executable file
@@ -0,0 +1,113 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Script to add an entry to a task's work log on a GitHub issue
|
||||
# Usage: ./log-entry.sh <issue-url-or-number> <entry-text>
|
||||
# Creates or updates a WORK_LOG comment with timestamped entries
|
||||
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Usage: $0 <issue-url-or-number> <entry-text>"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " issue-url-or-number GitHub issue URL or issue number"
|
||||
echo " entry-text Text describing the work (e.g., 'Started writing spec')"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 188 'Started writing spec'"
|
||||
echo " $0 190 'Finished writing plan'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ISSUE_INPUT="$1"
|
||||
ENTRY_TEXT="$2"
|
||||
|
||||
# Detect GitHub repository from git remote or environment
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
# shellcheck source=lib-repo-detect.sh
|
||||
source "$SCRIPT_DIR/lib-repo-detect.sh"
|
||||
|
||||
# Normalize the issue URL/number
|
||||
if [[ $ISSUE_INPUT =~ ^https?://github\.com/ ]]; then
|
||||
ISSUE_URL="$ISSUE_INPUT"
|
||||
else
|
||||
# Need repo info for building URL from issue number
|
||||
if ! detect_github_repo; then
|
||||
exit 1
|
||||
fi
|
||||
ISSUE_URL="https://github.com/$REPO_OWNER/$REPO_NAME/issues/$ISSUE_INPUT"
|
||||
fi
|
||||
|
||||
# Parse the URL to extract owner, repo, and issue number
|
||||
if [[ $ISSUE_URL =~ github\.com/([^/]+)/([^/]+)/issues/([0-9]+) ]]; then
|
||||
OWNER="${BASH_REMATCH[1]}"
|
||||
REPO="${BASH_REMATCH[2]}"
|
||||
ISSUE_NUM="${BASH_REMATCH[3]}"
|
||||
else
|
||||
echo "Error: Invalid GitHub issue URL"
|
||||
echo "Expected format: https://github.com/owner/repo/issues/NUMBER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_FULL="$OWNER/$REPO"
|
||||
MARKER="WORK_LOG_MARKER"
|
||||
|
||||
# Get current timestamp
|
||||
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# Check if WORK_LOG comment already exists
|
||||
existing_comment_id=$(gh api repos/$REPO_FULL/issues/$ISSUE_NUM/comments \
|
||||
--jq ".[] | select(.body | contains(\"<!-- ${MARKER} -->\")) | .id" 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$existing_comment_id" ]; then
|
||||
# Get existing comment body
|
||||
existing_body=$(gh api repos/$REPO_FULL/issues/$ISSUE_NUM/comments/$existing_comment_id \
|
||||
--jq '.body' 2>/dev/null)
|
||||
|
||||
# Append new entry (insert before closing of last list item or at end)
|
||||
# Format: - TIMESTAMP: entry-text
|
||||
new_entry="- $TIMESTAMP: $ENTRY_TEXT"
|
||||
|
||||
# Insert new entry before the last closing detail tag if it exists, or just append
|
||||
if [[ $existing_body == *"</details>"* ]]; then
|
||||
new_body="${existing_body%</details>*}$new_entry
|
||||
|
||||
</details>"
|
||||
else
|
||||
new_body="$existing_body
|
||||
|
||||
$new_entry"
|
||||
fi
|
||||
|
||||
# Update existing comment
|
||||
echo "↻ Adding entry to work log on issue #$ISSUE_NUM..."
|
||||
temp_body_file=$(mktemp)
|
||||
echo "$new_body" > "$temp_body_file"
|
||||
gh api repos/$REPO_FULL/issues/$ISSUE_NUM/comments/$existing_comment_id \
|
||||
-X PATCH \
|
||||
-F body=@"$temp_body_file" > /dev/null
|
||||
rm "$temp_body_file"
|
||||
echo "✓ Entry added"
|
||||
else
|
||||
# Create new WORK_LOG comment
|
||||
echo "+ Creating work log on issue #$ISSUE_NUM..."
|
||||
|
||||
new_entry="- $TIMESTAMP: $ENTRY_TEXT"
|
||||
|
||||
body="<!-- ${MARKER} -->
|
||||
|
||||
## AI Work Log
|
||||
|
||||
$new_entry"
|
||||
|
||||
temp_body_file=$(mktemp)
|
||||
echo "$body" > "$temp_body_file"
|
||||
gh api repos/$REPO_FULL/issues/$ISSUE_NUM/comments \
|
||||
-X POST \
|
||||
-F body=@"$temp_body_file" > /dev/null
|
||||
rm "$temp_body_file"
|
||||
echo "✓ Work log created"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "View the issue: $ISSUE_URL"
|
||||
98
skills/github-task-sync/pull-file.sh
Executable file
98
skills/github-task-sync/pull-file.sh
Executable file
@@ -0,0 +1,98 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Script to pull a single task file from a GitHub issue to local file
|
||||
# Usage: ./pull-file.sh <issue-url-or-number> <file-type> [output-file]
|
||||
# File types: SPEC, PLAN, TEST_PLAN, COMMIT_MESSAGE
|
||||
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Usage: $0 <issue-url-or-number> <file-type> [output-file]"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " issue-url-or-number GitHub issue URL or issue number"
|
||||
echo " file-type One of: SPEC, PLAN, TEST_PLAN, COMMIT_MESSAGE"
|
||||
echo " output-file File to write to (default: {file-type}.md in current directory)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 188 SPEC"
|
||||
echo " $0 188 PLAN PLAN.md"
|
||||
echo " $0 https://github.com/owner/repo/issues/188 TEST_PLAN ./test-plan.md"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ISSUE_INPUT="$1"
|
||||
FILE_TYPE="$2"
|
||||
OUTPUT_FILE="${3:-./${FILE_TYPE}.md}"
|
||||
|
||||
# Validate file type
|
||||
case "$FILE_TYPE" in
|
||||
SPEC|PLAN|TEST_PLAN|COMMIT_MESSAGE)
|
||||
;;
|
||||
*)
|
||||
echo "Error: Invalid file type '$FILE_TYPE'"
|
||||
echo "Valid types: SPEC, PLAN, TEST_PLAN, COMMIT_MESSAGE"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Detect GitHub repository from git remote or environment
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
# shellcheck source=lib-repo-detect.sh
|
||||
source "$SCRIPT_DIR/lib-repo-detect.sh"
|
||||
|
||||
# Normalize the issue URL/number
|
||||
if [[ $ISSUE_INPUT =~ ^https?://github\.com/ ]]; then
|
||||
ISSUE_URL="$ISSUE_INPUT"
|
||||
else
|
||||
# Need repo info for building URL from issue number
|
||||
if ! detect_github_repo; then
|
||||
exit 1
|
||||
fi
|
||||
ISSUE_URL="https://github.com/$REPO_OWNER/$REPO_NAME/issues/$ISSUE_INPUT"
|
||||
fi
|
||||
|
||||
# Parse the URL to extract owner, repo, and issue number
|
||||
if [[ $ISSUE_URL =~ github\.com/([^/]+)/([^/]+)/issues/([0-9]+) ]]; then
|
||||
OWNER="${BASH_REMATCH[1]}"
|
||||
REPO="${BASH_REMATCH[2]}"
|
||||
ISSUE_NUM="${BASH_REMATCH[3]}"
|
||||
else
|
||||
echo "Error: Invalid GitHub issue URL"
|
||||
echo "Expected format: https://github.com/owner/repo/issues/NUMBER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_FULL="$OWNER/$REPO"
|
||||
MARKER="${FILE_TYPE}_MARKER"
|
||||
|
||||
echo "Pulling $FILE_TYPE from issue #$ISSUE_NUM..."
|
||||
|
||||
# Fetch the comment with the matching marker and extract the content
|
||||
# Use startswith to match the exact marker (not substrings like PLAN_MARKER vs TEST_PLAN_MARKER)
|
||||
comment_body=$(gh api repos/$REPO_FULL/issues/$ISSUE_NUM/comments \
|
||||
--jq ".[] | select(.body | startswith(\"<!-- $MARKER -->\")) | .body" 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$comment_body" ]; then
|
||||
echo "Error: Could not find $FILE_TYPE comment on issue #$ISSUE_NUM" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract the content between the markdown code fences
|
||||
# Handle both plain markdown blocks and those wrapped in <details> tags
|
||||
if echo "$comment_body" | grep -q '<details>'; then
|
||||
# For content in <details>, extract from ```markdown to </details>, then remove first line and last 2 lines
|
||||
extracted=$(echo "$comment_body" | sed -n '/```markdown/,/<\/details>/p' | sed '1d' | sed '$d' | sed '$d')
|
||||
else
|
||||
# For unwrapped content, extract between ```markdown and the LAST ``` (to handle nested code blocks)
|
||||
extracted=$(echo "$comment_body" | awk '/```markdown/{flag=1; next} /^```$/ && flag{exit} flag')
|
||||
fi
|
||||
|
||||
if [ -z "$extracted" ]; then
|
||||
echo "Error: Could not extract content from $FILE_TYPE comment" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Write to output file
|
||||
echo "$extracted" > "$OUTPUT_FILE"
|
||||
echo "✓ Pulled to $OUTPUT_FILE"
|
||||
128
skills/github-task-sync/pull.sh
Executable file
128
skills/github-task-sync/pull.sh
Executable file
@@ -0,0 +1,128 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Script to pull task documentation files from a GitHub issue to local task directory
|
||||
# Usage: ./pull.sh <issue-url-or-number>
|
||||
# Pulls SPEC.md, PLAN.md, TEST_PLAN.md, COMMIT_MESSAGE.md from issue comments
|
||||
# Automatically creates directory as tasks/{issue-number}-{title-slug}
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "Usage: $0 <issue-url-or-number>"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " issue-url-or-number GitHub issue URL or issue number"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 188"
|
||||
echo " $0 https://github.com/<github-user>/<repo-name>/issues/188"
|
||||
echo ""
|
||||
echo "The script will automatically create a directory named:"
|
||||
echo " tasks/{issue-number}-{title-slug}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ISSUE_INPUT="$1"
|
||||
|
||||
# Detect GitHub repository from git remote or environment
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
# shellcheck source=lib-repo-detect.sh
|
||||
source "$SCRIPT_DIR/lib-repo-detect.sh"
|
||||
|
||||
# Normalize the issue URL/number
|
||||
if [[ $ISSUE_INPUT =~ ^https?://github\.com/ ]]; then
|
||||
ISSUE_URL="$ISSUE_INPUT"
|
||||
else
|
||||
# Need repo info for building URL from issue number
|
||||
if ! detect_github_repo; then
|
||||
exit 1
|
||||
fi
|
||||
ISSUE_URL="https://github.com/$REPO_OWNER/$REPO_NAME/issues/$ISSUE_INPUT"
|
||||
fi
|
||||
|
||||
# Parse the URL to extract owner, repo, and issue number
|
||||
if [[ $ISSUE_URL =~ github\.com/([^/]+)/([^/]+)/issues/([0-9]+) ]]; then
|
||||
OWNER="${BASH_REMATCH[1]}"
|
||||
REPO="${BASH_REMATCH[2]}"
|
||||
ISSUE_NUM="${BASH_REMATCH[3]}"
|
||||
else
|
||||
echo "Error: Invalid GitHub issue URL"
|
||||
echo "Expected format: https://github.com/owner/repo/issues/NUMBER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_FULL="$OWNER/$REPO"
|
||||
|
||||
# Fetch the issue title from GitHub
|
||||
echo "📥 Fetching issue #$ISSUE_NUM from $REPO_FULL..."
|
||||
ISSUE_TITLE=$(gh api repos/$REPO_FULL/issues/$ISSUE_NUM --jq '.title')
|
||||
|
||||
if [ -z "$ISSUE_TITLE" ]; then
|
||||
echo "Error: Could not fetch issue title"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Convert title to URL-safe slug
|
||||
# - Convert to lowercase
|
||||
# - Replace spaces and special characters with dashes
|
||||
# - Remove consecutive dashes
|
||||
# - Trim leading/trailing dashes
|
||||
SLUG=$(echo "$ISSUE_TITLE" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//' | sed 's/-$//')
|
||||
|
||||
# Create task directory as tasks/{issue-number}-{slug}
|
||||
TASK_DIR="tasks/$ISSUE_NUM-$SLUG"
|
||||
mkdir -p "$TASK_DIR"
|
||||
|
||||
echo "📥 Pulling task files from GitHub issue #$ISSUE_NUM: \"$ISSUE_TITLE\""
|
||||
echo "📁 Task directory: $TASK_DIR"
|
||||
echo ""
|
||||
|
||||
# Function to pull a file from GitHub
|
||||
pull_file() {
|
||||
local file=$1
|
||||
local marker=$2
|
||||
local file_path="$TASK_DIR/$file"
|
||||
|
||||
echo "Pulling $file..."
|
||||
|
||||
# Fetch the comment with the matching marker and extract the content
|
||||
# Use startswith to match the exact marker (not substrings like PLAN_MARKER vs TEST_PLAN_MARKER)
|
||||
comment_body=$(gh api repos/$REPO_FULL/issues/$ISSUE_NUM/comments \
|
||||
--jq ".[] | select(.body | startswith(\"<!-- $marker -->\")) | .body" 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$comment_body" ]; then
|
||||
echo " ⏭️ Skipping $file (not found on issue)"
|
||||
return
|
||||
fi
|
||||
|
||||
# Extract the content between the markdown code fences
|
||||
# Handle both plain markdown blocks and those wrapped in <details> tags
|
||||
if echo "$comment_body" | grep -q '<details>'; then
|
||||
# For content in <details>, extract from ```markdown to </details>, then remove first line and last 2 lines
|
||||
extracted=$(echo "$comment_body" | sed -n '/```markdown/,/<\/details>/p' | sed '1d' | sed '$d' | sed '$d')
|
||||
else
|
||||
# For unwrapped content, extract between ```markdown and the LAST ``` (to handle nested code blocks)
|
||||
# This is trickier - we need to find the matching closing fence
|
||||
# For now, use a simple approach: extract from ```markdown to end, then find the last ``` and trim from there
|
||||
extracted=$(echo "$comment_body" | awk '/```markdown/{flag=1; next} /^```$/ && flag{exit} flag')
|
||||
fi
|
||||
|
||||
if [ -z "$extracted" ]; then
|
||||
echo " ⚠️ Warning: Could not extract content from $file comment"
|
||||
return
|
||||
fi
|
||||
|
||||
# Write to file
|
||||
echo "$extracted" > "$file_path"
|
||||
echo " ✓ Pulled to $file"
|
||||
}
|
||||
|
||||
# Pull all four files
|
||||
pull_file "SPEC.md" "SPEC_MARKER"
|
||||
pull_file "PLAN.md" "PLAN_MARKER"
|
||||
pull_file "TEST_PLAN.md" "TEST_PLAN_MARKER"
|
||||
pull_file "COMMIT_MESSAGE.md" "COMMIT_MESSAGE_MARKER"
|
||||
|
||||
echo ""
|
||||
echo "✅ Pull complete!"
|
||||
echo "Task directory: $TASK_DIR"
|
||||
155
skills/github-task-sync/push-file.sh
Executable file
155
skills/github-task-sync/push-file.sh
Executable file
@@ -0,0 +1,155 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Script to update a single task file comment on a GitHub issue with status and content
|
||||
# Usage: ./update-issue-file.sh <issue-url-or-number> <file-type> <status-file> <content-file>
|
||||
# File types: SPEC, PLAN, TEST_PLAN, COMMIT_MESSAGE
|
||||
# The status file should contain a 2-paragraph summary with optional bullet points
|
||||
|
||||
if [ $# -lt 4 ]; then
|
||||
echo "Usage: $0 <issue-url-or-number> <file-type> <status-file> <content-file>"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " issue-url-or-number GitHub issue URL or issue number"
|
||||
echo " file-type One of: SPEC, PLAN, TEST_PLAN, COMMIT_MESSAGE"
|
||||
echo " status-file File containing status summary (2 paragraphs + optional bullets)"
|
||||
echo " content-file File containing the full content to display"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 188 SPEC SPEC-STATUS.md SPEC.md"
|
||||
echo " $0 https://github.com/owner/repo/issues/188 PLAN plan-status.txt PLAN.md"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ISSUE_INPUT="$1"
|
||||
FILE_TYPE="$2"
|
||||
STATUS_FILE="$3"
|
||||
CONTENT_FILE="$4"
|
||||
|
||||
# Validate file type
|
||||
case "$FILE_TYPE" in
|
||||
SPEC|PLAN|TEST_PLAN|COMMIT_MESSAGE)
|
||||
;;
|
||||
*)
|
||||
echo "Error: Invalid file type '$FILE_TYPE'"
|
||||
echo "Valid types: SPEC, PLAN, TEST_PLAN, COMMIT_MESSAGE"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Validate files exist
|
||||
if [ ! -f "$STATUS_FILE" ]; then
|
||||
echo "Error: Status file not found: $STATUS_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$CONTENT_FILE" ]; then
|
||||
echo "Error: Content file not found: $CONTENT_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect GitHub repository from git remote or environment
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
# shellcheck source=lib-repo-detect.sh
|
||||
source "$SCRIPT_DIR/lib-repo-detect.sh"
|
||||
|
||||
# Normalize the issue URL/number
|
||||
if [[ $ISSUE_INPUT =~ ^https?://github\.com/ ]]; then
|
||||
ISSUE_URL="$ISSUE_INPUT"
|
||||
else
|
||||
# Need repo info for building URL from issue number
|
||||
if ! detect_github_repo; then
|
||||
exit 1
|
||||
fi
|
||||
ISSUE_URL="https://github.com/$REPO_OWNER/$REPO_NAME/issues/$ISSUE_INPUT"
|
||||
fi
|
||||
|
||||
# Parse the URL to extract owner, repo, and issue number
|
||||
if [[ $ISSUE_URL =~ github\.com/([^/]+)/([^/]+)/issues/([0-9]+) ]]; then
|
||||
OWNER="${BASH_REMATCH[1]}"
|
||||
REPO="${BASH_REMATCH[2]}"
|
||||
ISSUE_NUM="${BASH_REMATCH[3]}"
|
||||
else
|
||||
echo "Error: Invalid GitHub issue URL"
|
||||
echo "Expected format: https://github.com/owner/repo/issues/NUMBER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_FULL="$OWNER/$REPO"
|
||||
MARKER="${FILE_TYPE}_MARKER"
|
||||
|
||||
# Read status and content
|
||||
status_content=$(cat "$STATUS_FILE")
|
||||
file_content=$(cat "$CONTENT_FILE")
|
||||
|
||||
# Extract status value from first line (e.g., "**Status:** Complete")
|
||||
status_line=$(echo "$status_content" | head -1)
|
||||
status_value=$(echo "$status_line" | sed 's/.*\*\*Status:\*\* *//' | sed 's/\*\*//')
|
||||
|
||||
# Remove the first line (status line) from status_content to avoid duplication
|
||||
remaining_content=$(echo "$status_content" | tail -n +2)
|
||||
|
||||
# Create the title from file type
|
||||
case "$FILE_TYPE" in
|
||||
SPEC)
|
||||
title="Specification"
|
||||
;;
|
||||
PLAN)
|
||||
title="Implementation Plan"
|
||||
;;
|
||||
TEST_PLAN)
|
||||
title="Test Plan"
|
||||
;;
|
||||
COMMIT_MESSAGE)
|
||||
title="Commit Message"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Create body with marker, heading with status, and collapsible section
|
||||
body="<!-- ${MARKER} -->
|
||||
|
||||
## $title: $status_value
|
||||
|
||||
$remaining_content
|
||||
|
||||
<details>
|
||||
<summary>📋 $title</summary>
|
||||
|
||||
\`\`\`markdown
|
||||
$file_content
|
||||
\`\`\`
|
||||
|
||||
</details>"
|
||||
|
||||
# Check if comment with this marker already exists
|
||||
# Use exact marker match including HTML comment tags to avoid matching TEST_PLAN_MARKER when looking for PLAN_MARKER
|
||||
existing_comment_id=$(gh api repos/$REPO_FULL/issues/$ISSUE_NUM/comments \
|
||||
--jq ".[] | select(.body | contains(\"<!-- ${MARKER} -->\")) | .id" 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$existing_comment_id" ]; then
|
||||
# Update existing comment
|
||||
echo "↻ Updating $FILE_TYPE comment on issue #$ISSUE_NUM (ID: $existing_comment_id)..."
|
||||
# Use a temporary file to avoid shell escaping issues with large bodies
|
||||
temp_body_file=$(mktemp)
|
||||
echo "$body" > "$temp_body_file"
|
||||
gh api repos/$REPO_FULL/issues/comments/$existing_comment_id \
|
||||
-X PATCH \
|
||||
-F body=@"$temp_body_file" > /dev/null
|
||||
rm "$temp_body_file"
|
||||
echo "✓ Updated successfully"
|
||||
else
|
||||
# Create new comment
|
||||
echo "+ Creating new $FILE_TYPE comment on issue #$ISSUE_NUM..."
|
||||
# Use a temporary file to avoid shell escaping issues with large bodies
|
||||
temp_body_file=$(mktemp)
|
||||
echo "$body" > "$temp_body_file"
|
||||
gh api repos/$REPO_FULL/issues/$ISSUE_NUM/comments \
|
||||
-X POST \
|
||||
-F body=@"$temp_body_file" > /dev/null
|
||||
rm "$temp_body_file"
|
||||
echo "✓ Created successfully"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "View the issue: $ISSUE_URL"
|
||||
181
skills/github-task-sync/push.sh
Executable file
181
skills/github-task-sync/push.sh
Executable file
@@ -0,0 +1,181 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Script to sync task documentation (SPEC, PLAN, TEST_PLAN, COMMIT_MESSAGE) to a GitHub issue
|
||||
# Usage: ./sync-to-github.sh <issue-url-or-number> [task-directory]
|
||||
# Examples:
|
||||
# ./sync-to-github.sh https://github.com/owner/repo/issues/188 /path/to/tasks/account-deletion
|
||||
# ./sync-to-github.sh 188 /path/to/tasks/account-deletion
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "Usage: $0 <issue-url-or-number> [task-directory]"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " issue-url-or-number GitHub issue URL or issue number"
|
||||
echo " task-directory Directory containing SPEC.md, PLAN.md, etc. (default: current directory)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 https://github.com/owner/repo/issues/188"
|
||||
echo " $0 188 ./tasks/account-deletion"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ISSUE_INPUT="$1"
|
||||
TASK_DIR="${2:-.}"
|
||||
|
||||
# Detect GitHub repository from git remote or environment
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
# shellcheck source=lib-repo-detect.sh
|
||||
source "$SCRIPT_DIR/lib-repo-detect.sh"
|
||||
|
||||
# Normalize the issue URL/number
|
||||
if [[ $ISSUE_INPUT =~ ^https?://github\.com/ ]]; then
|
||||
ISSUE_URL="$ISSUE_INPUT"
|
||||
else
|
||||
# Need repo info for building URL from issue number
|
||||
if ! detect_github_repo; then
|
||||
exit 1
|
||||
fi
|
||||
ISSUE_URL="https://github.com/$REPO_OWNER/$REPO_NAME/issues/$ISSUE_INPUT"
|
||||
fi
|
||||
|
||||
# Parse the URL to extract owner, repo, and issue number
|
||||
# Expected format: https://github.com/owner/repo/issues/NUMBER
|
||||
if [[ $ISSUE_URL =~ github\.com/([^/]+)/([^/]+)/issues/([0-9]+) ]]; then
|
||||
OWNER="${BASH_REMATCH[1]}"
|
||||
REPO="${BASH_REMATCH[2]}"
|
||||
ISSUE_NUM="${BASH_REMATCH[3]}"
|
||||
else
|
||||
echo "Error: Invalid GitHub issue URL"
|
||||
echo "Expected format: https://github.com/owner/repo/issues/NUMBER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_FULL="$OWNER/$REPO"
|
||||
|
||||
echo "📤 Syncing task files to GitHub issue #$ISSUE_NUM in $REPO_FULL"
|
||||
echo ""
|
||||
|
||||
# Function to sync a file to GitHub
|
||||
sync_file() {
|
||||
local file=$1
|
||||
local marker=$2
|
||||
local title=$3
|
||||
local file_path="$TASK_DIR/$file"
|
||||
|
||||
if [ ! -f "$file_path" ]; then
|
||||
echo "⏭️ Skipping $file (not found)"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Processing $file..."
|
||||
|
||||
# Read file content
|
||||
local content=$(cat "$file_path")
|
||||
|
||||
# Check if comment with this marker already exists
|
||||
local existing_comment_id=$(gh api repos/$REPO_FULL/issues/$ISSUE_NUM/comments \
|
||||
--jq ".[] | select(.body | contains(\"<!-- ${marker}_MARKER -->\")) | .id" 2>/dev/null || echo "")
|
||||
|
||||
local body
|
||||
|
||||
if [ -n "$existing_comment_id" ]; then
|
||||
# Fetch existing comment body to preserve summary/status
|
||||
echo " ↻ Fetching existing comment (ID: $existing_comment_id)..."
|
||||
local existing_body=$(gh api repos/$REPO_FULL/issues/comments/$existing_comment_id --jq '.body' 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$existing_body" ]; then
|
||||
# Check if there's a <details> tag in the existing body
|
||||
if echo "$existing_body" | grep -q "<details>"; then
|
||||
# Extract everything before the <details> tag (this is the summary/status section)
|
||||
# Use sed to get everything up to but not including the <details> line
|
||||
local summary_section=$(echo "$existing_body" | sed '/<details>/,$d')
|
||||
else
|
||||
# No <details> tag, use the whole body as summary (minus marker if present)
|
||||
local summary_section=$(echo "$existing_body")
|
||||
fi
|
||||
|
||||
# Check if there's a summary section (more than just the marker comment)
|
||||
if echo "$summary_section" | grep -q -v "^<!-- ${marker}_MARKER -->$" && [ -n "$(echo "$summary_section" | sed '/^[[:space:]]*$/d' | tail -n +2)" ]; then
|
||||
# Preserve the summary section and add updated content
|
||||
body="$summary_section
|
||||
|
||||
<details>
|
||||
<summary>📋 $title</summary>
|
||||
|
||||
\`\`\`markdown
|
||||
$content
|
||||
\`\`\`
|
||||
|
||||
</details>"
|
||||
else
|
||||
# No meaningful summary, use simple body
|
||||
body="<!-- ${marker}_MARKER -->
|
||||
|
||||
<details>
|
||||
<summary>📋 $title</summary>
|
||||
|
||||
\`\`\`markdown
|
||||
$content
|
||||
\`\`\`
|
||||
|
||||
</details>"
|
||||
fi
|
||||
else
|
||||
# Couldn't fetch existing body, use simple body
|
||||
body="<!-- ${marker}_MARKER -->
|
||||
|
||||
<details>
|
||||
<summary>📋 $title</summary>
|
||||
|
||||
\`\`\`markdown
|
||||
$content
|
||||
\`\`\`
|
||||
|
||||
</details>"
|
||||
fi
|
||||
|
||||
# Update existing comment
|
||||
echo " ↻ Updating existing comment (preserving summary)..."
|
||||
local temp_body_file=$(mktemp)
|
||||
echo "$body" > "$temp_body_file"
|
||||
gh api repos/$REPO_FULL/issues/comments/$existing_comment_id \
|
||||
-X PATCH \
|
||||
-F body=@"$temp_body_file" > /dev/null
|
||||
rm "$temp_body_file"
|
||||
echo " ✓ Updated"
|
||||
else
|
||||
# Create new comment with simple body (no existing summary to preserve)
|
||||
body="<!-- ${marker}_MARKER -->
|
||||
|
||||
<details>
|
||||
<summary>📋 $title</summary>
|
||||
|
||||
\`\`\`markdown
|
||||
$content
|
||||
\`\`\`
|
||||
|
||||
</details>"
|
||||
|
||||
echo " + Creating new comment..."
|
||||
local temp_body_file=$(mktemp)
|
||||
echo "$body" > "$temp_body_file"
|
||||
gh api repos/$REPO_FULL/issues/$ISSUE_NUM/comments \
|
||||
-X POST \
|
||||
-F body=@"$temp_body_file" > /dev/null
|
||||
rm "$temp_body_file"
|
||||
echo " ✓ Created"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Sync all four files
|
||||
sync_file "SPEC.md" "SPEC" "Specification"
|
||||
sync_file "PLAN.md" "PLAN" "Implementation Plan"
|
||||
sync_file "TEST_PLAN.md" "TEST_PLAN" "Test Plan"
|
||||
sync_file "COMMIT_MESSAGE.md" "COMMIT_MESSAGE" "Commit Message"
|
||||
|
||||
echo "✅ Sync complete!"
|
||||
echo "View the issue: $ISSUE_URL"
|
||||
93
skills/github-task-sync/read-issue-file.sh
Executable file
93
skills/github-task-sync/read-issue-file.sh
Executable file
@@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Script to read task documentation files from a GitHub issue comment
|
||||
# Usage: ./read-issue-file.sh <issue-url-or-number> <file-type>
|
||||
# File types: SPEC, PLAN, TEST_PLAN, COMMIT_MESSAGE
|
||||
# Output: File contents to stdout
|
||||
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Usage: $0 <issue-url-or-number> <file-type>"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " issue-url-or-number GitHub issue URL or issue number"
|
||||
echo " file-type One of: SPEC, PLAN, TEST_PLAN, COMMIT_MESSAGE"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 https://github.com/owner/repo/issues/188 SPEC"
|
||||
echo " $0 188 PLAN"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ISSUE_INPUT="$1"
|
||||
FILE_TYPE="$2"
|
||||
|
||||
# Validate file type
|
||||
case "$FILE_TYPE" in
|
||||
SPEC|PLAN|TEST_PLAN|COMMIT_MESSAGE)
|
||||
;;
|
||||
*)
|
||||
echo "Error: Invalid file type '$FILE_TYPE'"
|
||||
echo "Valid types: SPEC, PLAN, TEST_PLAN, COMMIT_MESSAGE"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Detect GitHub repository from git remote or environment
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
# shellcheck source=lib-repo-detect.sh
|
||||
source "$SCRIPT_DIR/lib-repo-detect.sh"
|
||||
|
||||
# Normalize the issue URL/number
|
||||
if [[ $ISSUE_INPUT =~ ^https?://github\.com/ ]]; then
|
||||
ISSUE_URL="$ISSUE_INPUT"
|
||||
else
|
||||
# Need repo info for building URL from issue number
|
||||
if ! detect_github_repo; then
|
||||
exit 1
|
||||
fi
|
||||
ISSUE_URL="https://github.com/$REPO_OWNER/$REPO_NAME/issues/$ISSUE_INPUT"
|
||||
fi
|
||||
|
||||
# Parse the URL to extract owner, repo, and issue number
|
||||
if [[ $ISSUE_URL =~ github\.com/([^/]+)/([^/]+)/issues/([0-9]+) ]]; then
|
||||
OWNER="${BASH_REMATCH[1]}"
|
||||
REPO="${BASH_REMATCH[2]}"
|
||||
ISSUE_NUM="${BASH_REMATCH[3]}"
|
||||
else
|
||||
echo "Error: Invalid GitHub issue URL"
|
||||
echo "Expected format: https://github.com/owner/repo/issues/NUMBER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_FULL="$OWNER/$REPO"
|
||||
MARKER="${FILE_TYPE}_MARKER"
|
||||
|
||||
# Fetch the comment with the matching marker and extract the content
|
||||
# Use startswith to match the exact marker (not substrings like PLAN_MARKER vs TEST_PLAN_MARKER)
|
||||
comment_body=$(gh api repos/$REPO_FULL/issues/$ISSUE_NUM/comments \
|
||||
--jq ".[] | select(.body | startswith(\"<!-- $MARKER -->\")) | .body" 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$comment_body" ]; then
|
||||
echo "Error: Could not find $FILE_TYPE comment on issue #$ISSUE_NUM" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract the content between the markdown code fences
|
||||
# Handle both plain markdown blocks and those wrapped in <details> tags
|
||||
if echo "$comment_body" | grep -q '<details>'; then
|
||||
# For content in <details>, extract from ```markdown to </details>, then remove first line and last 2 lines
|
||||
extracted=$(echo "$comment_body" | sed -n '/```markdown/,/<\/details>/p' | sed '1d' | sed '$d' | sed '$d')
|
||||
else
|
||||
# For unwrapped content, extract between ```markdown and the LAST ``` (to handle nested code blocks)
|
||||
extracted=$(echo "$comment_body" | awk '/```markdown/{flag=1; next} /^```$/ && flag{exit} flag')
|
||||
fi
|
||||
|
||||
if [ -z "$extracted" ]; then
|
||||
echo "Error: Could not extract content from $FILE_TYPE comment" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Output to stdout
|
||||
echo "$extracted"
|
||||
Reference in New Issue
Block a user