
Github Agile
- 293 installs
- 133 repo stars
- Updated February 24, 2026
- jwynia/agent-skills
github-agile is a Claude agent skill that diagnoses GitHub-driven agile workflow problems and guides developers through feature-branch development using GitHub Issues, milestones, pull requests, and context-network docum
About
github-agile is a diagnostic agent skill (version 1.0) from jwynia/agent-skills that helps developers establish and maintain lightweight agile execution on GitHub. The skill maps eight workflow states—from missing GitHub CLI through healthy delivery—and prescribes interventions using gh commands, four bundled Deno scripts (gh-verify, gh-init-project, gh-audit, gh-sync-context), and context-network files such as status.md and decisions.md. It initializes labels across three schemes, issue templates for features, bugs, and tasks, branch protection, and PR templates, then enforces feature-branch naming, MoSCoW backlog grooming, and milestone sync ceremonies. Reach for github-agile when a repository has chaotic issues, stale PRs, direct-to-main commits, or a context network that no longer matches GitHub reality.
- Structures issues and milestones on GitHub
- Supports incremental delivery planning
- Aligns agent tasks to sprint-like cycles
- Improves backlog clarity and prioritization
- Connects planning habits to repo workflow
Github Agile by the numbers
- 293 all-time installs (skills.sh)
- +3 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #914 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jwynia/agent-skills --skill github-agileAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 293 |
|---|---|
| repo stars | ★ 133 |
| Last updated | February 24, 2026 |
| Repository | jwynia/agent-skills ↗ |
How do you run agile planning on GitHub Issues?
Run lightweight agile planning and execution with GitHub issues, milestones, and delivery rhythm while Claude helps break work into actionable increments.
Who is it for?
Developers using GitHub CLI who want structured issue tracking, milestone cadence, and feature-branch PR workflows without adopting a separate project-management platform.
Skip if: Developers on GitLab, Gitea, or Jira-only workflows who do not use GitHub Issues, milestones, and the gh CLI as their execution layer.
When should I use this skill?
A developer reports GitHub backlog chaos, stale PRs, missing branch protection, direct commits to main, or a context network out of sync with open issues and milestones.
What you get
Labeled GitHub issues and milestones, feature-branch PRs with templates, branch protection on main, context/status.md and decisions.md updates, and gh-audit health scores with remediation steps.
- .github issue and PR templates
- context/status.md sprint updates
- gh-audit workflow health report
By the numbers
- Defines 8 diagnostic workflow states labeled GH0 through GH8
- Bundles 4 Deno helper scripts for verify, init, audit, and context sync
- Supports 3 label schemes: standard, simple, and minimal
Files
GitHub Agile: Feature Branch Development with Context Networks
You diagnose GitHub-driven agile workflow problems. Your role is to help developers establish and maintain healthy workflows using GitHub Issues, Pull Requests, and feature branches, while preserving understanding in context networks.
Core Principle
GitHub is where work lives, context networks are where understanding lives. Issues track what needs doing; context networks preserve why decisions were made. Both persist, but serve different functions: GitHub for collaboration and execution, context networks for judgment and continuity.
The States
Setup Track
---
State GH0: No GitHub CLI
Symptoms:
ghcommand not found- Cannot execute GitHub operations from the command line
- User reports they have not installed GitHub CLI
- Manual web-based GitHub interaction only
Key Questions:
- Is this a fresh machine or an existing development setup?
- Do you use Homebrew (macOS), apt (Linux), or winget/scoop (Windows)?
- Have you authenticated with GitHub before?
- Do you have a GitHub account?
Interventions:
- Run
scripts/gh-verify.tsto diagnose environment - Installation guidance by platform:
- macOS:
brew install gh - Linux:
sudo apt install ghor see https://github.com/cli/cli/blob/trunk/docs/install_linux.md - Windows:
winget install --id GitHub.cliorscoop install gh - After install, authenticate:
gh auth login - Validate with
gh auth status
---
State GH1: Repository Not Initialized
Symptoms:
- Directory exists but is not a git repository
- Git repository exists but has no GitHub remote
- GitHub remote exists but
gh repo viewfails - Working locally without version control
Key Questions:
- Is this a new project or existing code without GitHub?
- Do you want a public or private repository?
- Does a context network exist yet for this project?
- Are there existing files that need an initial commit?
Interventions:
- Initialize git if needed:
git init - Create and link GitHub repository:
gh repo create <name> --source=. --push - Or link existing remote:
git remote add origin <url> - Initialize context network if missing (create
context/directory) - Create initial commit with conventional structure
- Verify with
gh repo view
---
State GH2: Workflow Not Established
Symptoms:
- GitHub repository exists but no labels, milestones, or issue templates
- No branch protection on main
- No
.github/directory with templates - Context network not connected to GitHub workflow
- No conventions documented
Key Questions:
- Is this a solo project or team project?
- What label scheme fits your work style? (standard/simple/custom)
- Do you want milestones for time-boxing work?
- Should main branch be protected from direct commits?
Interventions:
- Run
scripts/gh-init-project.tsto set up project structure - Create
.github/ISSUE_TEMPLATE/with feature, bug, task templates - Create
.github/pull_request_template.md - Enable branch protection:
gh api repos/{owner}/{repo}/branches/main/protection -X PUT -f ... - Document workflow in
context/architecture.md - Record setup decisions in
context/decisions.md
---
Workflow Track
---
State GH3: Backlog Chaos
Symptoms:
- Many issues with no labels or inconsistent labels
- No milestones assigned
- Duplicate or overlapping issues
- Cannot tell what to work on next
- Issues describe solutions rather than problems
- Old issues mixed with current priorities
Key Questions:
- How many open issues do you have?
- What determines priority? (deadline, value, dependencies, effort)
- Are there issues that should be closed or consolidated?
- When was the last backlog grooming?
- Are issues linked to requirements or just ad-hoc ideas?
Interventions:
- Run
scripts/gh-audit.tsto assess backlog health - Audit issues:
gh issue list --state open --json number,title,labels,createdAt - Apply MoSCoW prioritization (Must/Should/Could/Won't)
- Create "icebox" label for deferred items not worth deleting
- Close duplicates with reference to canonical issue
- Link issues to requirements if requirements-analysis was used
- Create milestone for current focus period
- Update
context/status.mdwith current sprint/milestone focus
---
State GH4: Feature Branch Violations
Symptoms:
- Commits directly to main branch
- No branch naming convention
- Feature work mixed across branches
- Merge conflicts frequent due to long-lived branches
- Cannot tell which branch relates to which issue
- PRs created from main to main (if possible)
Key Questions:
- Is branch protection enabled on main?
- What naming convention would work? (
feature/,fix/,issue-{number}/) - Are you the sole contributor or expecting others?
- How long do feature branches typically live?
Interventions:
- Enable branch protection via GitHub settings or API
- Establish branch naming convention in
context/architecture.md: feature/{issue-number}-short-descriptionfix/{issue-number}-short-descriptionchore/{description}for maintenance without issues- Create branch from issue:
gh issue develop {number} --base main - Or manually:
git checkout -b feature/{number}-description main - Keep branches short-lived (days, not weeks)
- Document branch workflow in
context/decisions.md
---
State GH5: PR Without Context
Symptoms:
- PRs with minimal descriptions ("fixes bug", "updates code")
- No linked issues in PRs
- No reference to decisions, requirements, or architecture
- Code review lacks context for why changes were made
- Cannot trace code back to requirements or decisions
- Future archaeology impossible
Key Questions:
- Do you have a PR template?
- How should PRs link to issues? (
Fixes #,Closes #,Related to #) - Should PRs reference ADRs or requirements documents?
- What information does a reviewer (or future you) need?
Interventions:
- Create/update
.github/pull_request_template.mdwith required sections: - Summary (what changed)
- Related Issue (with closing keyword)
- Why (motivation, context)
- How to Test
- Context References (links to decisions, ADRs if relevant)
- Add checklist: linked issue, test plan, context reference
- Use
gh pr create --templateto apply template - Cross-reference
context/decisions.mdwhen architectural changes are involved
---
State GH6: Stale Issues/PRs
Symptoms:
- Open issues from months ago with no activity
- Draft PRs that will never be merged
- "WIP" labels on abandoned work
- Issue count keeps growing, never shrinking
- Cannot tell active work from abandoned work
- Mental load from zombie issues
Key Questions:
- What makes an issue "stale"? (30 days? 90 days?)
- Should stale items be auto-labeled or auto-closed?
- Are some issues actually "someday/maybe" and need different treatment?
- What's the cost of keeping stale items open?
Interventions:
- Run
scripts/gh-audit.ts --staleto identify old items - Audit stale items:
gh issue list --state open --json number,title,updatedAt | jq '.[] | select(...)' - Create "stale" or "needs-review" label for items needing decision
- Decision for each stale item:
- Still relevant? Update and re-prioritize
- Someday/maybe? Move to icebox with clear trigger for revival
- Never happening? Close with explanation
- Document staleness policy in
context/architecture.md - Consider GitHub Actions stale bot for automation
---
State GH7: Context Network Gap
Symptoms:
- GitHub has active work but context network is empty or outdated
- Cannot explain why past decisions were made
- New sessions start from scratch understanding the project
- ADRs (Architecture Decision Records) not recorded
status.mddoes not reflect current work- Knowledge lives only in closed issues/PRs (hard to find)
Key Questions:
- Does a context network exist for this project?
- When was
status.mdlast updated? - Are there decisions in GitHub discussions or issues that should be in
decisions.md? - Can someone (including future you) understand the project from context alone?
Interventions:
- Initialize context network if missing:
context/
├── discovery.md # Navigation and overview
├── status.md # Current work, recent changes
├── decisions.md # Key decisions with rationale
├── architecture.md # System design, workflows
└── glossary.md # Project-specific terms- Run
scripts/gh-sync-context.tsto generate status update - Update
status.mdwith current sprint/milestone - Extract decisions from closed issues/PRs to
decisions.md - Create
architecture.mdsection documenting GitHub workflow - Link GitHub milestones to context network phases
---
State GH8: Workflow Healthy
Symptoms:
- Issues are labeled, prioritized, and assigned to milestones
- All work happens on feature branches via PRs
- PRs link to issues and have meaningful descriptions
- Context network reflects current state and decisions
- Can answer "what am I working on?" and "why?"
- New sessions can resume without rediscovery
Indicators:
gh issue listshows only relevant, current workgh pr statusshows active work with clear purposecontext/status.mdmatches GitHub reality- Recent commits are on feature branches, not main
- No issues older than threshold without explanation
Maintenance:
- Weekly sync: GitHub state to
context/status.md - Sprint/milestone boundaries: retrospective insights to
decisions.md - Architecture changes: ADR creation in context network
- Regular audit with
scripts/gh-audit.ts
---
GitHub ↔ Context Network Boundary
Lives in GitHub
| Artifact | Why GitHub |
|---|---|
| Issues | Collaboration, state tracking, notifications, linking |
| Pull Requests | Code review, CI integration, merge tracking |
| Discussions | Team/community conversation, Q&A, RFCs |
| Actions/Workflows | CI/CD, automation, enforcement |
| Labels/Milestones | Organization, filtering, progress tracking |
| Project Boards | Visual workflow (optional) |
Lives in Context Network
| Artifact | Why Context Network |
|---|---|
| ADRs (Architecture Decision Records) | Structured reasoning, searchable, framework integration |
decisions.md | Cross-cutting decisions, policy, rationale |
status.md | Current focus, recent changes, session continuity |
architecture.md | System design, workflow documentation |
discovery.md | Project understanding, navigation |
glossary.md | Vocabulary, shared understanding |
| Retrospective insights | Learnings that improve future work |
Bridge Artifacts (Cross-Reference)
| Artifact | Primary Location | Cross-Reference |
|---|---|---|
| Requirements document | context/ or docs/ | Linked from issues |
| System design | context/architecture.md | Referenced in PRs |
| Sprint/milestone goals | status.md | Matched to GitHub milestone |
| Key decisions | decisions.md | Referenced in issue/PR comments |
---
Mode-Specific Workflows
Solo Developer Mode
Purpose: Self-discipline, history for future self, structured thinking
Adaptations:
- Branch protection still valuable (prevents accidents)
- Self-review PRs: use PR as thinking checkpoint, not just merge gate
- Issues as memory: write issues for future sessions
- Simplified labels:
type:feature,type:bug,type:task,priority:high/low - Context network especially important (no team to ask)
Workflow: 1. Start session: check context/status.md and gh issue list 2. Pick issue or create one for new work 3. Create feature branch: gh issue develop {number} 4. Work with regular commits (reference issue in messages) 5. Create PR: gh pr create --fill 6. Self-review: read diff as if someone else wrote it 7. Merge: gh pr merge --squash 8. Update context/status.md if significant
Team Mode (2-5 people)
Purpose: Collaboration, code review, shared understanding
Adaptations:
- Mandatory code review before merge
- Assignment conventions (who owns what)
- Sync ceremonies (standup, planning, retro)
- More structured labels including assignee-related
- Discussions for async decisions
Workflow: 1. Planning: create/refine issues, assign to milestone 2. Assignment: assign issues, communicate via comments 3. Development: feature branches, regular commits 4. PR creation: detailed description, request reviewers 5. Review: approve, request changes, or comment 6. Merge: after approval, squash merge 7. Sync: weekly context network update, milestone retros
Team Labels (additions):
status:needs-review- waiting for code reviewstatus:changes-requested- reviewer requested changesneeds:discussion- requires team input before proceeding
---
Key Workflows
1. Project Initialization Ceremony
Trigger: New project or first GitHub integration
Steps: 1. Verify GH CLI installed and authenticated (GH0): scripts/gh-verify.ts 2. Create or link repository (GH1): gh repo create or git remote add 3. Run initialization: scripts/gh-init-project.ts --labels standard --templates --protection 4. Initialize context network if missing 5. Document workflow in context/architecture.md 6. Record decisions in context/decisions.md 7. Create first milestone for initial work phase
2. Feature Development Workflow
Trigger: Starting new work item
Steps: 1. Ensure issue exists (create if not): gh issue create 2. Create feature branch from issue:
gh issue develop {number} --base main
# Or manually:
git checkout -b feature/{number}-short-description main3. Make commits with conventional messages:
feat(scope): description (#123)
fix(scope): description (#123)
chore: description4. Push regularly: git push -u origin HEAD 5. Create PR when ready:
gh pr create --fill
# Or with explicit template:
gh pr create --title "feat: description" --body-file .github/pull_request_template.md6. Ensure PR:
- Links to issue (
Closes #123) - Has meaningful description
- References context if architectural
7. Review (self or team) 8. Merge via gh pr merge --squash (squash keeps history clean) 9. Issue auto-closes via PR keywords 10. Delete branch: git branch -d feature/{number}-description
3. Sprint/Milestone Sync
Trigger: Beginning or end of milestone
Beginning: 1. Create milestone: gh api repos/{owner}/{repo}/milestones -f title="Sprint X" -f due_on="YYYY-MM-DD" 2. Assign issues to milestone 3. Update context/status.md with milestone focus and goals 4. Communicate priorities (solo: write them down; team: planning meeting)
End: 1. Review milestone: gh issue list --milestone "Sprint X" 2. Close completed milestone 3. Move incomplete issues to next milestone or icebox 4. Record retrospective in context/decisions.md:
- What worked well?
- What didn't work?
- What will we change?
5. Update context/status.md with summary
4. Context Sync Ceremony
Trigger: Weekly or after significant work
Steps: 1. Run audit: scripts/gh-audit.ts 2. Run sync: scripts/gh-sync-context.ts --dry-run (review first) 3. Update context/status.md:
- Current milestone/sprint
- Active issues/PRs
- Recent completions
- Blockers or decisions needed
4. Extract decisions from closed issues/PRs to decisions.md 5. Update architecture.md if workflow evolved 6. Verify: could someone resume work from context alone?
---
Anti-Patterns
The GitHub-as-Wiki
Problem: Using GitHub issues for long-form documentation, decisions, and context that should live in the context network. Important information gets buried in comments, impossible to find later. Symptoms: Massive issue descriptions, architecture debates in comments, decisions scattered across closed issues. Fix: GitHub tracks work items; context network tracks understanding. If it needs to survive beyond the issue lifecycle, move it to context/.
The Issue Graveyard
Problem: Issues created and never closed, making the backlog meaningless. New issues pile on top of old ones. Symptoms: 200+ open issues, most untouched for months, "I'll get to it" mentality, paralysis choosing what to work on. Fix: Regular grooming. If it won't be done in 90 days, icebox or close it. A small, current backlog beats a large, stale one. Delete aggressively.
The Context-Free PR
Problem: PRs that describe what changed but not why, making future archaeology impossible. Symptoms: "Fixed the bug", "Updated styles", "Refactored code" with no context. Six months later, no one knows why. Fix: PR template with required sections: What, Why, How to test, Related issues. If it's architectural, reference the ADR.
The Duplicate Issue Machine
Problem: Creating new issues without checking if one exists, leading to fragmented discussion and wasted effort. Symptoms: Multiple issues about the same thing, effort split across duplicates, conflicting resolutions. Fix: Search before creating: gh issue list --search "keyword". Close duplicates with reference to canonical issue.
The Eternal Draft PR
Problem: PRs opened as drafts and never completed, blocking mental progress and cluttering the PR list. Symptoms: Draft PRs older than 30 days, WIP labels that become permanent, scope creep making PRs unmergeable. Fix: Time-box drafts. If not ready in 2 weeks, close and re-scope. Small PRs that can merge beat large PRs that cannot.
The Branch Protection Bypass
Problem: Disabling branch protection "just this once" and committing directly to main. Creates precedent for future bypasses. Symptoms: Commits to main without PRs, broken builds on main, "I'll fix it in the next commit". Fix: Branch protection exists for a reason. Even solo developers benefit from the PR workflow for history, review checkpoint, and rollback capability.
The Disconnected Context
Problem: Context network exists but is not updated, becoming fiction rather than documentation. Worse than no context because it's misleading. Symptoms: status.md shows work completed months ago, decisions.md missing recent architectural changes, new sessions can't trust context. Fix: Make context sync part of the workflow, not an afterthought. End-of-session ritual: does status.md reflect reality?
---
Available Tools
gh-verify.ts
Verifies GitHub CLI installation and authentication status.
deno run --allow-run scripts/gh-verify.ts
deno run --allow-run scripts/gh-verify.ts --jsonOutput:
- CLI installation status and version
- Authentication status and current user
- Default repository (if in a repo directory)
- Recommendations if anything is missing
Exit codes: 0 (all good), 1 (gh missing), 2 (not logged in), 3 (no repo context)
gh-init-project.ts
Initializes GitHub project with labels, templates, and branch protection.
deno run --allow-run --allow-read --allow-write scripts/gh-init-project.ts
deno run --allow-run --allow-read --allow-write scripts/gh-init-project.ts --labels standard --templates --protection
deno run --allow-run --allow-read --allow-write scripts/gh-init-project.ts --mode team --labels standardOptions:
--labels [standard|simple|minimal]- Label scheme to create--templates- Create issue and PR templates--protection- Enable branch protection on main--mode [solo|team]- Adjust defaults for working style--dry-run- Show what would be created without creating
Creates:
- Labels via
gh label create .github/ISSUE_TEMPLATE/with feature, bug, task templates.github/pull_request_template.md- Branch protection rules (if
--protection)
gh-audit.ts
Audits current GitHub state against healthy workflow indicators.
deno run --allow-run scripts/gh-audit.ts
deno run --allow-run scripts/gh-audit.ts --json
deno run --allow-run scripts/gh-audit.ts --stale 30Options:
--json- Output as JSON for scripting--stale [days]- Flag items with no activity for N days (default: 30)--verbose- Show detailed item-by-item analysis
Checks:
- Open issue count and label coverage
- Open PR status (draft, linked issues, age)
- Recent commits (which branch, direct to main?)
- Milestone usage and progress
- Stale item identification
Output: Health score (0-100) with specific recommendations
gh-sync-context.ts
Generates context network updates from GitHub state.
deno run --allow-run --allow-write scripts/gh-sync-context.ts
deno run --allow-run scripts/gh-sync-context.ts --dry-run
deno run --allow-run --allow-write scripts/gh-sync-context.ts --status --decisionsOptions:
--dry-run- Output what would be written without writing--status- Generatestatus.mdupdate section--decisions- Extract decisions from closed issues with "decision" label--output [dir]- Directory to write to (default:context/)
Generates:
- Status update content for
status.md - Decision candidates from closed issues
- Milestone summary
---
Example Interactions
Solo Developer: Starting Fresh
Developer: "I have a local project I want to put on GitHub and start using proper workflow."
Diagnosis: State GH1 (Repository Not Initialized)
Approach: 1. Run scripts/gh-verify.ts - confirm CLI ready 2. Ask: "Public or private repository?" 3. Create repo: gh repo create my-project --source=. --private --push 4. Run scripts/gh-init-project.ts --mode solo --labels simple --templates 5. Initialize context network: create context/ with status.md, decisions.md 6. Document in context/architecture.md: "Using GitHub Issues for tracking, feature branches for all changes, PRs for merge and history." 7. Create first issue for current work 8. Demonstrate branch workflow: gh issue develop 1
Team: Backlog Cleanup
Developer: "We have 150 open issues and no one knows what's important."
Diagnosis: State GH3 (Backlog Chaos)
Approach: 1. Run scripts/gh-audit.ts --stale 60 - identify scope 2. Ask: "What's the current priority? What must ship soon?" 3. Create milestone for immediate focus 4. Triage issues:
- Critical for milestone → assign to milestone, add priority label
- Valid but not now → icebox label
- Stale with no path forward → close with explanation
- Duplicates → close with reference to canonical
5. Update context/status.md with focus 6. Establish grooming cadence: weekly 15-minute review
Solo Developer: Context Decay
Developer: "I came back to this project after a month and have no idea what I was doing."
Diagnosis: State GH7 (Context Network Gap)
Approach: 1. Check GitHub state: gh issue list, gh pr status 2. Run scripts/gh-sync-context.ts --dry-run to see current state 3. Initialize or update context network:
status.md: What's the current milestone? Active issues?decisions.md: Any architectural decisions in closed PRs?
4. Review closed issues/PRs from last session for context 5. Update status.md with "Last session" summary 6. Establish habit: end each session by updating status.md
---
Output Persistence
Output Discovery
Before doing any other work:
1. Check for context/output-config.md in the project 2. If found, look for this skill's entry 3. If not found, ask: "Where should I save GitHub workflow output?"
- Suggest:
context/for context network files,.github/for GitHub configuration
4. Store preference in context/output-config.md
Primary Output
| Output | Location |
|---|---|
| Issue templates | .github/ISSUE_TEMPLATE/ |
| PR template | .github/pull_request_template.md |
| Workflow documentation | context/architecture.md (GitHub workflow section) |
| Setup decisions | context/decisions.md |
| Status updates | context/status.md |
| Audit reports | context/github-audit-{date}.md (if persisted) |
Conversation vs. File
| Goes to File | Stays in Conversation |
|---|---|
Templates (.github/) | Diagnosis discussion |
| Workflow documentation | Label scheme exploration |
| Context network updates | Triage decisions |
| Audit reports (if requested) | Quick status checks |
---
What You Do NOT Do
- You do not create issues without user confirmation
- You do not merge PRs automatically
- You do not delete issues, PRs, or branches without explicit request
- You do not change branch protection without explicit request
- You do not skip verification of
ghavailability (always check GH0 first) - You do not assume GitHub access without
gh auth statuscheck - You do not replace the context network with GitHub-only storage
- You do not create commits on main branch (enforce feature branch workflow)
- You do not push to remote without user awareness
- You diagnose, recommend, and execute with confirmation - the developer decides
---
Integration with Other Skills
From requirements-analysis
| requirements-analysis Output | github-agile Input |
|---|---|
| Problem Statement | Creates initial issue(s) describing the problem |
| Need Hierarchy | Maps to issue priority labels |
| Constraint Inventory | Documents in context/, references in issues |
| Validated Requirements | Creates feature issues, links to requirements doc |
From system-design
| system-design Output | github-agile Input |
|---|---|
| ADRs | Stored in context/adr/ or docs/adr/, referenced in PRs |
| Component Map | Informs issue breakdown by component |
| Walking Skeleton | First milestone with linked issues |
| Integration Points | Documented in context/architecture.md |
To requirements-analysis
When GitHub state reveals requirements problems:
| github-agile State | Trigger | requirements-analysis State |
|---|---|---|
| GH3 (Backlog Chaos) | Issues describe solutions not problems | RA0-RA1 |
| GH3 (Backlog Chaos) | Cannot prioritize (everything important) | RA4 |
To system-design
When GitHub state reveals design problems:
| github-agile State | Trigger | system-design State |
|---|---|---|
| GH5 (PR Without Context) | PR involves undocumented architecture decisions | SD4 |
| GH4 (Branch Violations) | Frequent merge conflicts | SD3 (Missing Integration Points) |
---
References
This skill operationalizes:
- GitHub CLI documentation: https://cli.github.com/manual/
- Context Networks framework:
references/context-networks/ - Feature branch workflow best practices
- Conventional Commits: https://www.conventionalcommits.org/
Description
<!-- What's broken? Be specific about the failure mode. -->
Steps to Reproduce
1. 2. 3.
Expected Behavior
<!-- What should happen? -->
Actual Behavior
<!-- What actually happens? Include error messages if any. -->
Environment
- OS:
- Version/Commit:
- Browser (if applicable):
Screenshots / Logs
<!-- If applicable, add screenshots or paste relevant log output -->
Possible Fix
<!-- If you have an idea what's wrong, describe it here -->
Problem
<!-- What problem does this feature solve? Who has this problem? Be specific. -->
Proposed Solution
<!-- How should this work? Be specific about behavior and user experience. -->
Alternatives Considered
<!-- What other approaches did you consider? Why not those? -->
Acceptance Criteria
- [ ] Criterion 1
- [ ] Criterion 2
- [ ] Criterion 3
Context References
<!-- Link to requirements doc, discussion, or context/decisions.md if relevant --> <!-- Example: See context/decisions.md#feature-approach for background -->
Additional Notes
<!-- Screenshots, mockups, or other relevant information -->
Description
<!-- What needs to be done? Be specific. -->
Motivation
<!-- Why is this needed? What problem does it solve or prevent? -->
Acceptance Criteria
- [ ] Criterion 1
- [ ] Criterion 2
Notes
<!-- Any additional context, constraints, or dependencies -->
{
"_meta": {
"description": "Standard label scheme for GitHub-driven agile workflow",
"usage": "Use with gh-init-project.ts --labels standard or apply manually via gh label create"
},
"type": {
"description": "What kind of work is this?",
"labels": [
{ "name": "type:feature", "description": "New functionality", "color": "0E8A16" },
{ "name": "type:bug", "description": "Something broken", "color": "D73A4A" },
{ "name": "type:task", "description": "Maintenance, docs, infrastructure", "color": "0075CA" },
{ "name": "type:question", "description": "Needs discussion or clarification", "color": "D876E3" }
]
},
"status": {
"description": "What's the current state?",
"labels": [
{ "name": "status:needs-triage", "description": "New, not yet reviewed", "color": "FBCA04" },
{ "name": "status:ready", "description": "Ready to work on", "color": "C2E0C6" },
{ "name": "status:in-progress", "description": "Currently being worked on", "color": "1D76DB" },
{ "name": "status:blocked", "description": "Waiting on something", "color": "B60205" },
{ "name": "status:stale", "description": "No activity, needs review", "color": "D4C5F9" }
]
},
"priority": {
"description": "How urgent is this?",
"labels": [
{ "name": "priority:critical", "description": "Must be done immediately", "color": "B60205" },
{ "name": "priority:high", "description": "Should be done soon", "color": "D93F0B" },
{ "name": "priority:medium", "description": "Normal priority", "color": "FBCA04" },
{ "name": "priority:low", "description": "Nice to have, can wait", "color": "0E8A16" }
]
},
"special": {
"description": "Special-purpose labels",
"labels": [
{ "name": "icebox", "description": "Deferred indefinitely", "color": "EDEDED" },
{ "name": "decision", "description": "Contains a key decision", "color": "5319E7" },
{ "name": "good-first-issue", "description": "Good for newcomers", "color": "7057FF" }
]
},
"team": {
"description": "Team-mode only labels (add with --mode team)",
"labels": [
{ "name": "status:needs-review", "description": "Waiting for code review", "color": "FBCA04" },
{ "name": "status:changes-requested", "description": "Reviewer requested changes", "color": "D93F0B" },
{ "name": "needs:discussion", "description": "Requires team input", "color": "D876E3" }
]
}
}
Summary
<!-- Brief description of what this PR does -->
Related Issue
<!-- Use closing keyword: Closes #, Fixes #, or Resolves # --> Closes #
Changes
<!-- Bullet list of specific changes made -->
- - -
Why
<!-- Motivation for these changes. What problem does this solve? -->
How to Test
<!-- Steps for reviewer to verify the changes work -->
1. 2. 3.
Context References
<!-- Link to ADR, requirements doc, or context/decisions.md if architectural --> <!-- Example: See context/decisions.md#api-design for background -->
Checklist
- [ ] Tests pass
- [ ] Code follows project conventions
- [ ] PR is appropriately scoped (not too large)
- [ ] Issue is linked with closing keyword
- [ ] Self-reviewed (read diff as if someone else wrote it)
Screenshots (if applicable)
<!-- Before/after screenshots for UI changes -->
#!/usr/bin/env -S deno run --allow-run
/**
* GitHub Workflow Audit
*
* Audits current GitHub state against healthy workflow indicators.
* Use this for state diagnosis and health checks.
*
* Usage:
* deno run --allow-run gh-audit.ts
* deno run --allow-run gh-audit.ts --json
* deno run --allow-run gh-audit.ts --stale 30
* deno run --allow-run gh-audit.ts --verbose
*/
// === INTERFACES ===
interface Issue {
number: number;
title: string;
state: string;
createdAt: string;
updatedAt: string;
labels: { name: string }[];
milestone: { title: string } | null;
assignees: { login: string }[];
}
interface PullRequest {
number: number;
title: string;
state: string;
isDraft: boolean;
createdAt: string;
updatedAt: string;
headRefName: string;
baseRefName: string;
body: string;
}
interface Commit {
sha: string;
message: string;
authoredDate: string;
authors: { name: string }[];
}
interface AuditCheck {
category: string;
check: string;
status: "pass" | "fail" | "warn" | "info";
message: string;
details?: string[];
recommendation?: string;
}
interface AuditReport {
timestamp: string;
repository: string;
summary: {
total: number;
passed: number;
failed: number;
warnings: number;
};
score: number;
state: string;
checks: AuditCheck[];
staleItems: {
issues: { number: number; title: string; daysSinceUpdate: number }[];
prs: { number: number; title: string; daysSinceUpdate: number }[];
};
}
interface AuditOptions {
staleDays: number;
verbose: boolean;
json: boolean;
}
// === UTILITIES ===
async function runCommand(
cmd: string[]
): Promise<{ success: boolean; output: string; error: string }> {
try {
const command = new Deno.Command(cmd[0], {
args: cmd.slice(1),
stdout: "piped",
stderr: "piped",
});
const { success, stdout, stderr } = await command.output();
return {
success,
output: new TextDecoder().decode(stdout).trim(),
error: new TextDecoder().decode(stderr).trim(),
};
} catch {
return {
success: false,
output: "",
error: "Command not found or failed to execute",
};
}
}
function daysSince(dateString: string): number {
const date = new Date(dateString);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
return Math.floor(diffMs / (1000 * 60 * 60 * 24));
}
// === DATA FETCHING ===
async function getRepoName(): Promise<string> {
const result = await runCommand([
"gh",
"repo",
"view",
"--json",
"nameWithOwner",
"--jq",
".nameWithOwner",
]);
return result.output || "unknown";
}
async function getOpenIssues(): Promise<Issue[]> {
const result = await runCommand([
"gh",
"issue",
"list",
"--state",
"open",
"--limit",
"100",
"--json",
"number,title,state,createdAt,updatedAt,labels,milestone,assignees",
]);
if (!result.success || !result.output) {
return [];
}
try {
return JSON.parse(result.output);
} catch {
return [];
}
}
async function getOpenPRs(): Promise<PullRequest[]> {
const result = await runCommand([
"gh",
"pr",
"list",
"--state",
"open",
"--limit",
"50",
"--json",
"number,title,state,isDraft,createdAt,updatedAt,headRefName,baseRefName,body",
]);
if (!result.success || !result.output) {
return [];
}
try {
return JSON.parse(result.output);
} catch {
return [];
}
}
async function getRecentCommits(): Promise<Commit[]> {
const result = await runCommand([
"gh",
"api",
"repos/{owner}/{repo}/commits",
"--jq",
".[0:20] | map({sha: .sha, message: .commit.message, authoredDate: .commit.author.date, authors: [{name: .commit.author.name}]})",
]);
if (!result.success || !result.output) {
return [];
}
try {
return JSON.parse(result.output);
} catch {
return [];
}
}
async function getDefaultBranch(): Promise<string> {
const result = await runCommand([
"gh",
"repo",
"view",
"--json",
"defaultBranchRef",
"--jq",
".defaultBranchRef.name",
]);
return result.output || "main";
}
async function getLabels(): Promise<string[]> {
const result = await runCommand([
"gh",
"label",
"list",
"--limit",
"100",
"--json",
"name",
"--jq",
".[].name",
]);
if (!result.success || !result.output) {
return [];
}
return result.output.split("\n").filter(Boolean);
}
async function getMilestones(): Promise<{ title: string; state: string }[]> {
const result = await runCommand([
"gh",
"api",
"repos/{owner}/{repo}/milestones",
"--jq",
"map({title: .title, state: .state})",
]);
if (!result.success || !result.output) {
return [];
}
try {
return JSON.parse(result.output);
} catch {
return [];
}
}
// === AUDIT CHECKS ===
function auditIssues(issues: Issue[], staleDays: number): AuditCheck[] {
const checks: AuditCheck[] = [];
// Issue count check
const issueCount = issues.length;
if (issueCount === 0) {
checks.push({
category: "Issues",
check: "Open issue count",
status: "info",
message: "No open issues",
});
} else if (issueCount > 50) {
checks.push({
category: "Issues",
check: "Open issue count",
status: "warn",
message: `${issueCount} open issues (consider grooming)`,
recommendation: "Review and close or icebox stale issues",
});
} else {
checks.push({
category: "Issues",
check: "Open issue count",
status: "pass",
message: `${issueCount} open issues`,
});
}
// Label coverage
const unlabeledIssues = issues.filter(i => i.labels.length === 0);
if (unlabeledIssues.length === 0) {
checks.push({
category: "Issues",
check: "Label coverage",
status: "pass",
message: "All issues have labels",
});
} else {
checks.push({
category: "Issues",
check: "Label coverage",
status: "warn",
message: `${unlabeledIssues.length} issues without labels`,
details: unlabeledIssues.slice(0, 5).map(i => `#${i.number}: ${i.title}`),
recommendation: "Add type and priority labels to unlabeled issues",
});
}
// Milestone usage
const issuesWithMilestone = issues.filter(i => i.milestone !== null);
const milestoneRatio = issues.length > 0 ? issuesWithMilestone.length / issues.length : 1;
if (issues.length === 0 || milestoneRatio >= 0.5) {
checks.push({
category: "Issues",
check: "Milestone usage",
status: milestoneRatio >= 0.7 ? "pass" : "info",
message: `${issuesWithMilestone.length}/${issues.length} issues assigned to milestones`,
});
} else {
checks.push({
category: "Issues",
check: "Milestone usage",
status: "warn",
message: `Only ${issuesWithMilestone.length}/${issues.length} issues have milestones`,
recommendation: "Assign high-priority issues to a milestone",
});
}
// Stale issues
const staleIssues = issues.filter(i => daysSince(i.updatedAt) > staleDays);
if (staleIssues.length === 0) {
checks.push({
category: "Issues",
check: `Stale issues (>${staleDays} days)`,
status: "pass",
message: "No stale issues",
});
} else {
checks.push({
category: "Issues",
check: `Stale issues (>${staleDays} days)`,
status: "warn",
message: `${staleIssues.length} stale issues`,
details: staleIssues.slice(0, 5).map(i => `#${i.number}: ${i.title} (${daysSince(i.updatedAt)} days)`),
recommendation: "Review stale issues: close, icebox, or update",
});
}
return checks;
}
function auditPRs(prs: PullRequest[], staleDays: number): AuditCheck[] {
const checks: AuditCheck[] = [];
// PR count
const prCount = prs.length;
if (prCount === 0) {
checks.push({
category: "Pull Requests",
check: "Open PR count",
status: "pass",
message: "No open PRs",
});
} else if (prCount > 10) {
checks.push({
category: "Pull Requests",
check: "Open PR count",
status: "warn",
message: `${prCount} open PRs (consider merging or closing)`,
recommendation: "Review and merge or close stale PRs",
});
} else {
checks.push({
category: "Pull Requests",
check: "Open PR count",
status: "pass",
message: `${prCount} open PRs`,
});
}
// Draft PRs
const draftPRs = prs.filter(p => p.isDraft);
const oldDrafts = draftPRs.filter(p => daysSince(p.createdAt) > 14);
if (oldDrafts.length > 0) {
checks.push({
category: "Pull Requests",
check: "Old draft PRs",
status: "warn",
message: `${oldDrafts.length} draft PRs older than 14 days`,
details: oldDrafts.map(p => `#${p.number}: ${p.title}`),
recommendation: "Complete or close old draft PRs",
});
} else if (draftPRs.length > 0) {
checks.push({
category: "Pull Requests",
check: "Draft PRs",
status: "info",
message: `${draftPRs.length} draft PRs (all recent)`,
});
}
// PR descriptions
const shortDescriptionPRs = prs.filter(p => !p.body || p.body.length < 50);
if (shortDescriptionPRs.length > 0) {
checks.push({
category: "Pull Requests",
check: "PR descriptions",
status: "warn",
message: `${shortDescriptionPRs.length} PRs with short/missing descriptions`,
details: shortDescriptionPRs.map(p => `#${p.number}: ${p.title}`),
recommendation: "Add context to PR descriptions (what, why, how to test)",
});
} else if (prs.length > 0) {
checks.push({
category: "Pull Requests",
check: "PR descriptions",
status: "pass",
message: "All PRs have descriptions",
});
}
// Issue linkage (check for "Closes #", "Fixes #", etc. in body)
const unlinkedPRs = prs.filter(p => {
if (!p.body) return true;
const linkPattern = /(close[sd]?|fix(es|ed)?|resolve[sd]?)\s+#\d+/i;
return !linkPattern.test(p.body);
});
if (unlinkedPRs.length > 0 && prs.length > 0) {
checks.push({
category: "Pull Requests",
check: "Issue linkage",
status: "warn",
message: `${unlinkedPRs.length} PRs not linked to issues`,
details: unlinkedPRs.slice(0, 5).map(p => `#${p.number}: ${p.title}`),
recommendation: "Link PRs to issues with 'Closes #' or 'Fixes #'",
});
} else if (prs.length > 0) {
checks.push({
category: "Pull Requests",
check: "Issue linkage",
status: "pass",
message: "All PRs linked to issues",
});
}
return checks;
}
async function auditBranches(commits: Commit[], defaultBranch: string): Promise<AuditCheck[]> {
const checks: AuditCheck[] = [];
// Get recent commits to default branch
const result = await runCommand([
"git",
"log",
`origin/${defaultBranch}`,
"--oneline",
"-20",
"--format=%H %s",
]);
if (!result.success) {
checks.push({
category: "Branches",
check: "Branch analysis",
status: "info",
message: "Could not analyze branch commits",
});
return checks;
}
const commitLines = result.output.split("\n").filter(Boolean);
// Check for merge commits (indicates PR workflow)
const mergeCommits = commitLines.filter(line =>
line.includes("Merge pull request") || line.includes("Merge branch")
);
// Check for direct commits (non-merge)
const directCommits = commitLines.filter(line =>
!line.includes("Merge pull request") &&
!line.includes("Merge branch") &&
!line.includes("Merge remote")
);
if (directCommits.length === 0 && mergeCommits.length > 0) {
checks.push({
category: "Branches",
check: "Feature branch workflow",
status: "pass",
message: "All recent changes via PRs",
});
} else if (directCommits.length > mergeCommits.length) {
checks.push({
category: "Branches",
check: "Feature branch workflow",
status: "fail",
message: `${directCommits.length} direct commits vs ${mergeCommits.length} merges`,
details: directCommits.slice(0, 3).map(c => c.substring(0, 80)),
recommendation: "Use feature branches and PRs instead of direct commits",
});
} else {
checks.push({
category: "Branches",
check: "Feature branch workflow",
status: "warn",
message: `${directCommits.length} direct commits, ${mergeCommits.length} merges`,
recommendation: "Prefer PRs over direct commits for traceability",
});
}
return checks;
}
function auditLabelsAndMilestones(labels: string[], milestones: { title: string; state: string }[]): AuditCheck[] {
const checks: AuditCheck[] = [];
// Check for type labels
const hasTypeLabels = labels.some(l => l.startsWith("type:") || ["bug", "feature", "enhancement"].includes(l));
if (hasTypeLabels) {
checks.push({
category: "Organization",
check: "Type labels",
status: "pass",
message: "Type labels configured",
});
} else {
checks.push({
category: "Organization",
check: "Type labels",
status: "warn",
message: "No type labels found",
recommendation: "Create type labels (type:feature, type:bug, type:task)",
});
}
// Check for priority labels
const hasPriorityLabels = labels.some(l => l.startsWith("priority:") || l.includes("priority"));
if (hasPriorityLabels) {
checks.push({
category: "Organization",
check: "Priority labels",
status: "pass",
message: "Priority labels configured",
});
} else {
checks.push({
category: "Organization",
check: "Priority labels",
status: "info",
message: "No priority labels found",
recommendation: "Consider adding priority labels for triage",
});
}
// Check for active milestones
const activeMilestones = milestones.filter(m => m.state === "open");
if (activeMilestones.length > 0) {
checks.push({
category: "Organization",
check: "Active milestones",
status: "pass",
message: `${activeMilestones.length} active milestone(s)`,
details: activeMilestones.map(m => m.title),
});
} else {
checks.push({
category: "Organization",
check: "Active milestones",
status: "info",
message: "No active milestones",
recommendation: "Create a milestone for current work focus",
});
}
return checks;
}
// === REPORT GENERATION ===
function determineState(checks: AuditCheck[]): string {
const failures = checks.filter(c => c.status === "fail");
const warnings = checks.filter(c => c.status === "warn");
// Check for specific state indicators
const branchFail = failures.some(c => c.check.includes("branch workflow"));
const prDescriptionWarn = warnings.some(c => c.check.includes("PR descriptions"));
const staleWarn = warnings.some(c => c.check.includes("Stale"));
const labelWarn = warnings.some(c => c.check.includes("Label coverage"));
if (branchFail) {
return "GH4: Feature Branch Violations";
}
if (prDescriptionWarn || warnings.some(c => c.check.includes("Issue linkage"))) {
return "GH5: PR Without Context";
}
if (staleWarn) {
return "GH6: Stale Issues/PRs";
}
if (labelWarn || warnings.some(c => c.check.includes("Milestone usage"))) {
return "GH3: Backlog Chaos";
}
if (failures.length === 0 && warnings.length <= 2) {
return "GH8: Workflow Healthy";
}
return "Multiple issues detected";
}
function calculateScore(checks: AuditCheck[]): number {
const total = checks.filter(c => c.status !== "info").length;
if (total === 0) return 100;
const passed = checks.filter(c => c.status === "pass").length;
const warnings = checks.filter(c => c.status === "warn").length;
const score = (passed + warnings * 0.5) / total;
return Math.round(score * 100);
}
async function generateReport(options: AuditOptions): Promise<AuditReport> {
const repoName = await getRepoName();
const issues = await getOpenIssues();
const prs = await getOpenPRs();
const commits = await getRecentCommits();
const defaultBranch = await getDefaultBranch();
const labels = await getLabels();
const milestones = await getMilestones();
const checks: AuditCheck[] = [
...auditIssues(issues, options.staleDays),
...auditPRs(prs, options.staleDays),
...(await auditBranches(commits, defaultBranch)),
...auditLabelsAndMilestones(labels, milestones),
];
// Collect stale items
const staleIssues = issues
.filter(i => daysSince(i.updatedAt) > options.staleDays)
.map(i => ({
number: i.number,
title: i.title,
daysSinceUpdate: daysSince(i.updatedAt),
}));
const stalePRs = prs
.filter(p => daysSince(p.updatedAt) > options.staleDays)
.map(p => ({
number: p.number,
title: p.title,
daysSinceUpdate: daysSince(p.updatedAt),
}));
return {
timestamp: new Date().toISOString(),
repository: repoName,
summary: {
total: checks.length,
passed: checks.filter(c => c.status === "pass").length,
failed: checks.filter(c => c.status === "fail").length,
warnings: checks.filter(c => c.status === "warn").length,
},
score: calculateScore(checks),
state: determineState(checks),
checks,
staleItems: {
issues: staleIssues,
prs: stalePRs,
},
};
}
function formatReport(report: AuditReport, verbose: boolean): string {
const lines: string[] = [];
lines.push("=".repeat(60));
lines.push("GITHUB WORKFLOW AUDIT");
lines.push("=".repeat(60));
lines.push("");
lines.push(`Repository: ${report.repository}`);
lines.push(`Timestamp: ${report.timestamp}`);
lines.push("");
lines.push("-".repeat(60));
lines.push("SUMMARY");
lines.push("-".repeat(60));
lines.push(`Score: ${report.score}/100`);
lines.push(`State: ${report.state}`);
lines.push("");
lines.push(`Checks: ${report.summary.total}`);
lines.push(` Passed: ${report.summary.passed}`);
lines.push(` Failed: ${report.summary.failed}`);
lines.push(` Warnings: ${report.summary.warnings}`);
lines.push("");
// Group by category
const categories = [...new Set(report.checks.map(c => c.category))];
for (const category of categories) {
const categoryChecks = report.checks.filter(c => c.category === category);
lines.push("-".repeat(60));
lines.push(category.toUpperCase());
lines.push("-".repeat(60));
for (const check of categoryChecks) {
const statusIcon = {
pass: "[PASS]",
fail: "[FAIL]",
warn: "[WARN]",
info: "[INFO]",
}[check.status];
lines.push(`${statusIcon} ${check.check}`);
lines.push(` ${check.message}`);
if (verbose && check.details) {
for (const detail of check.details) {
lines.push(` - ${detail}`);
}
}
if (check.recommendation && check.status !== "pass") {
lines.push(` Recommendation: ${check.recommendation}`);
}
lines.push("");
}
}
// Failures and warnings summary
const actionItems = report.checks.filter(c => c.status === "fail" || c.status === "warn");
if (actionItems.length > 0) {
lines.push("=".repeat(60));
lines.push("ACTION ITEMS");
lines.push("=".repeat(60));
for (const item of actionItems) {
const icon = item.status === "fail" ? "[FAIL]" : "[WARN]";
lines.push(`${icon} ${item.check}: ${item.recommendation || item.message}`);
}
lines.push("");
}
return lines.join("\n");
}
// === ARGUMENT PARSING ===
function parseArgs(args: string[]): AuditOptions {
const options: AuditOptions = {
staleDays: 30,
verbose: false,
json: false,
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
const nextArg = args[i + 1];
switch (arg) {
case "--help":
case "-h":
printHelp();
Deno.exit(0);
break;
case "--stale":
case "-s":
if (nextArg && !isNaN(parseInt(nextArg))) {
options.staleDays = parseInt(nextArg);
i++;
}
break;
case "--verbose":
case "-v":
options.verbose = true;
break;
case "--json":
options.json = true;
break;
}
}
return options;
}
function printHelp(): void {
console.log(`
GitHub Workflow Audit
Audits current GitHub state against healthy workflow indicators.
USAGE:
gh-audit.ts [OPTIONS]
OPTIONS:
--stale, -s <days> Days threshold for stale items (default: 30)
--verbose, -v Show detailed item-by-item analysis
--json Output as JSON
--help, -h Show this help
CHECKS PERFORMED:
Issues:
- Open issue count
- Label coverage
- Milestone assignment
- Stale issue detection
Pull Requests:
- Open PR count
- Draft PR age
- Description quality
- Issue linkage
Branches:
- Feature branch workflow usage
- Direct commits to main
Organization:
- Type labels configured
- Priority labels configured
- Active milestones
EXAMPLES:
# Basic audit
gh-audit.ts
# Verbose output with 60-day stale threshold
gh-audit.ts --verbose --stale 60
# JSON output for CI/CD
gh-audit.ts --json
`);
}
// === MAIN ===
async function main(): Promise<void> {
const options = parseArgs(Deno.args);
const report = await generateReport(options);
if (options.json) {
console.log(JSON.stringify(report, null, 2));
} else {
console.log(formatReport(report, options.verbose));
}
// Exit with error if failures
if (report.summary.failed > 0) {
Deno.exit(1);
}
}
main();
#!/usr/bin/env -S deno run --allow-run --allow-read --allow-write
/**
* GitHub Project Initialization
*
* Initializes GitHub project with labels, issue/PR templates, and branch protection.
* Use this to resolve GH2 state (Workflow Not Established).
*
* Usage:
* deno run --allow-run --allow-read --allow-write gh-init-project.ts
* deno run --allow-run --allow-read --allow-write gh-init-project.ts --labels standard --templates --protection
* deno run --allow-run --allow-read --allow-write gh-init-project.ts --mode team --labels standard
* deno run --allow-run --allow-read --allow-write gh-init-project.ts --dry-run
*/
// === INTERFACES ===
interface Label {
name: string;
description: string;
color: string;
}
interface LabelScheme {
name: string;
description: string;
labels: Label[];
}
interface InitOptions {
labels: "standard" | "simple" | "minimal" | "none";
templates: boolean;
protection: boolean;
mode: "solo" | "team";
dryRun: boolean;
}
interface InitResult {
labelsCreated: string[];
labelsFailed: string[];
templatesCreated: string[];
protectionEnabled: boolean;
errors: string[];
}
// === LABEL SCHEMES ===
const LABEL_SCHEMES: Record<string, LabelScheme> = {
standard: {
name: "Standard",
description: "Comprehensive label scheme for structured workflows",
labels: [
// Type labels
{ name: "type:feature", description: "New functionality", color: "0E8A16" },
{ name: "type:bug", description: "Something broken", color: "D73A4A" },
{ name: "type:task", description: "Maintenance, docs, infrastructure", color: "0075CA" },
{ name: "type:question", description: "Needs discussion or clarification", color: "D876E3" },
// Status labels
{ name: "status:needs-triage", description: "New, not yet reviewed", color: "FBCA04" },
{ name: "status:ready", description: "Ready to work on", color: "C2E0C6" },
{ name: "status:in-progress", description: "Currently being worked on", color: "1D76DB" },
{ name: "status:blocked", description: "Waiting on something", color: "B60205" },
{ name: "status:stale", description: "No activity, needs review", color: "D4C5F9" },
// Priority labels
{ name: "priority:critical", description: "Must be done immediately", color: "B60205" },
{ name: "priority:high", description: "Should be done soon", color: "D93F0B" },
{ name: "priority:medium", description: "Normal priority", color: "FBCA04" },
{ name: "priority:low", description: "Nice to have, can wait", color: "0E8A16" },
// Special labels
{ name: "icebox", description: "Deferred indefinitely", color: "EDEDED" },
{ name: "decision", description: "Contains a key decision", color: "5319E7" },
{ name: "good-first-issue", description: "Good for newcomers", color: "7057FF" },
],
},
simple: {
name: "Simple",
description: "Minimal labels for solo developers",
labels: [
{ name: "type:feature", description: "New functionality", color: "0E8A16" },
{ name: "type:bug", description: "Something broken", color: "D73A4A" },
{ name: "type:task", description: "Maintenance, docs, infrastructure", color: "0075CA" },
{ name: "priority:high", description: "Do this first", color: "D93F0B" },
{ name: "priority:low", description: "Nice to have", color: "0E8A16" },
{ name: "icebox", description: "Deferred indefinitely", color: "EDEDED" },
],
},
minimal: {
name: "Minimal",
description: "Bare minimum labels",
labels: [
{ name: "bug", description: "Something broken", color: "D73A4A" },
{ name: "feature", description: "New functionality", color: "0E8A16" },
{ name: "icebox", description: "Deferred", color: "EDEDED" },
],
},
};
// Team-specific labels to add when mode is team
const TEAM_LABELS: Label[] = [
{ name: "status:needs-review", description: "Waiting for code review", color: "FBCA04" },
{ name: "status:changes-requested", description: "Reviewer requested changes", color: "D93F0B" },
{ name: "needs:discussion", description: "Requires team input", color: "D876E3" },
];
// === TEMPLATES ===
const FEATURE_TEMPLATE = `---
name: Feature Request
about: Propose a new feature or enhancement
title: '[FEATURE] '
labels: ['type:feature', 'status:needs-triage']
assignees: ''
---
## Problem
<!-- What problem does this feature solve? Who has this problem? -->
## Proposed Solution
<!-- How should this work? Be specific about behavior. -->
## Alternatives Considered
<!-- What other approaches did you consider? Why not those? -->
## Acceptance Criteria
- [ ] Criterion 1
- [ ] Criterion 2
## Context References
<!-- Link to requirements doc, discussion, or context/decisions.md if relevant -->
`;
const BUG_TEMPLATE = `---
name: Bug Report
about: Report something that's broken
title: '[BUG] '
labels: ['type:bug', 'status:needs-triage']
assignees: ''
---
## Description
<!-- What's broken? Be specific. -->
## Steps to Reproduce
1.
2.
3.
## Expected Behavior
<!-- What should happen? -->
## Actual Behavior
<!-- What actually happens? Include error messages if any. -->
## Environment
- OS:
- Version/Commit:
## Additional Context
<!-- Screenshots, logs, or other relevant information -->
`;
const TASK_TEMPLATE = `---
name: Task
about: Maintenance, documentation, or infrastructure work
title: '[TASK] '
labels: ['type:task']
assignees: ''
---
## Description
<!-- What needs to be done? -->
## Motivation
<!-- Why is this needed? What problem does it solve? -->
## Acceptance Criteria
- [ ] Criterion 1
- [ ] Criterion 2
## Notes
<!-- Any additional context or constraints -->
`;
const PR_TEMPLATE = `## Summary
<!-- Brief description of what this PR does -->
## Related Issue
<!-- Use closing keyword: Closes #, Fixes #, or Resolves # -->
Closes #
## Changes
<!-- Bullet list of specific changes made -->
-
-
## Why
<!-- Motivation for these changes. What problem does this solve? -->
## How to Test
<!-- Steps for reviewer to verify the changes work -->
1.
2.
## Context References
<!-- Link to ADR, requirements doc, or context/decisions.md if architectural -->
## Checklist
- [ ] Tests pass
- [ ] Code follows project conventions
- [ ] PR is appropriately scoped (not too large)
- [ ] Issue is linked with closing keyword
- [ ] Self-reviewed (read diff as if someone else wrote it)
`;
// === UTILITIES ===
async function runCommand(
cmd: string[]
): Promise<{ success: boolean; output: string; error: string }> {
try {
const command = new Deno.Command(cmd[0], {
args: cmd.slice(1),
stdout: "piped",
stderr: "piped",
});
const { success, stdout, stderr } = await command.output();
return {
success,
output: new TextDecoder().decode(stdout).trim(),
error: new TextDecoder().decode(stderr).trim(),
};
} catch {
return {
success: false,
output: "",
error: "Command not found or failed to execute",
};
}
}
async function ensureDirectory(path: string): Promise<void> {
try {
await Deno.mkdir(path, { recursive: true });
} catch (error) {
if (!(error instanceof Deno.errors.AlreadyExists)) {
throw error;
}
}
}
async function writeFile(path: string, content: string, dryRun: boolean): Promise<boolean> {
if (dryRun) {
console.log(`[DRY RUN] Would write: ${path}`);
return true;
}
try {
await Deno.writeTextFile(path, content);
return true;
} catch (error) {
console.error(`Failed to write ${path}: ${error}`);
return false;
}
}
// === INITIALIZATION FUNCTIONS ===
async function createLabels(
scheme: LabelScheme,
mode: "solo" | "team",
dryRun: boolean
): Promise<{ created: string[]; failed: string[] }> {
const created: string[] = [];
const failed: string[] = [];
let labels = [...scheme.labels];
// Add team labels if in team mode
if (mode === "team") {
labels = [...labels, ...TEAM_LABELS];
}
for (const label of labels) {
if (dryRun) {
console.log(`[DRY RUN] Would create label: ${label.name} (${label.description})`);
created.push(label.name);
continue;
}
const result = await runCommand([
"gh",
"label",
"create",
label.name,
"--description",
label.description,
"--color",
label.color,
"--force", // Update if exists
]);
if (result.success || result.error.includes("already exists")) {
created.push(label.name);
} else {
failed.push(`${label.name}: ${result.error}`);
}
}
return { created, failed };
}
async function createTemplates(dryRun: boolean): Promise<string[]> {
const created: string[] = [];
// Ensure .github/ISSUE_TEMPLATE directory exists
if (!dryRun) {
await ensureDirectory(".github/ISSUE_TEMPLATE");
}
// Create issue templates
const templates = [
{ path: ".github/ISSUE_TEMPLATE/feature.md", content: FEATURE_TEMPLATE },
{ path: ".github/ISSUE_TEMPLATE/bug.md", content: BUG_TEMPLATE },
{ path: ".github/ISSUE_TEMPLATE/task.md", content: TASK_TEMPLATE },
{ path: ".github/pull_request_template.md", content: PR_TEMPLATE },
];
for (const template of templates) {
if (await writeFile(template.path, template.content, dryRun)) {
created.push(template.path);
}
}
return created;
}
async function enableBranchProtection(dryRun: boolean): Promise<boolean> {
if (dryRun) {
console.log("[DRY RUN] Would enable branch protection on main/master");
return true;
}
// Get default branch
const branchResult = await runCommand([
"gh",
"repo",
"view",
"--json",
"defaultBranchRef",
"--jq",
".defaultBranchRef.name",
]);
const defaultBranch = branchResult.output || "main";
// Enable basic branch protection
// Note: This requires admin access to the repository
const result = await runCommand([
"gh",
"api",
`repos/{owner}/{repo}/branches/${defaultBranch}/protection`,
"-X",
"PUT",
"-H",
"Accept: application/vnd.github+json",
"-f",
"required_status_checks=null",
"-f",
"enforce_admins=null",
"-f",
"required_pull_request_reviews=null",
"-f",
"restrictions=null",
"-F",
"required_linear_history=true",
"-F",
"allow_force_pushes=false",
"-F",
"allow_deletions=false",
]);
return result.success;
}
// === MAIN INITIALIZATION ===
async function initialize(options: InitOptions): Promise<InitResult> {
const result: InitResult = {
labelsCreated: [],
labelsFailed: [],
templatesCreated: [],
protectionEnabled: false,
errors: [],
};
console.log("");
console.log("=".repeat(50));
console.log("GITHUB PROJECT INITIALIZATION");
console.log("=".repeat(50));
console.log("");
if (options.dryRun) {
console.log("[DRY RUN MODE - No changes will be made]");
console.log("");
}
// Create labels
if (options.labels !== "none") {
const scheme = LABEL_SCHEMES[options.labels];
if (scheme) {
console.log(`Creating labels (${scheme.name} scheme)...`);
const { created, failed } = await createLabels(scheme, options.mode, options.dryRun);
result.labelsCreated = created;
result.labelsFailed = failed;
console.log(` Created: ${created.length} labels`);
if (failed.length > 0) {
console.log(` Failed: ${failed.length} labels`);
result.errors.push(...failed);
}
console.log("");
}
}
// Create templates
if (options.templates) {
console.log("Creating issue and PR templates...");
result.templatesCreated = await createTemplates(options.dryRun);
console.log(` Created: ${result.templatesCreated.length} templates`);
console.log("");
}
// Enable branch protection
if (options.protection) {
console.log("Enabling branch protection...");
result.protectionEnabled = await enableBranchProtection(options.dryRun);
if (result.protectionEnabled) {
console.log(" Branch protection enabled");
} else {
console.log(" Failed to enable branch protection (may require admin access)");
result.errors.push("Branch protection requires admin access");
}
console.log("");
}
// Summary
console.log("-".repeat(50));
console.log("SUMMARY");
console.log("-".repeat(50));
console.log(`Labels created: ${result.labelsCreated.length}`);
console.log(`Templates created: ${result.templatesCreated.length}`);
console.log(`Branch protection: ${result.protectionEnabled ? "enabled" : "not enabled"}`);
if (result.errors.length > 0) {
console.log("");
console.log("Errors:");
for (const error of result.errors) {
console.log(` - ${error}`);
}
}
console.log("");
console.log("-".repeat(50));
console.log("NEXT STEPS");
console.log("-".repeat(50));
console.log("1. Review created templates in .github/");
console.log("2. Customize templates if needed");
console.log("3. Document workflow in context/architecture.md");
console.log("4. Create first milestone for current work phase");
console.log("");
return result;
}
// === ARGUMENT PARSING ===
function parseArgs(args: string[]): InitOptions {
const options: InitOptions = {
labels: "standard",
templates: false,
protection: false,
mode: "solo",
dryRun: false,
};
// If no args, use sensible defaults
if (args.length === 0 || (args.length === 1 && args[0] === "--help")) {
// Will show help or run with defaults
}
for (let i = 0; i < args.length; i++) {
const arg = args[i];
const nextArg = args[i + 1];
switch (arg) {
case "--help":
case "-h":
printHelp();
Deno.exit(0);
break;
case "--labels":
case "-l":
if (nextArg && ["standard", "simple", "minimal", "none"].includes(nextArg)) {
options.labels = nextArg as "standard" | "simple" | "minimal" | "none";
i++;
}
break;
case "--templates":
case "-t":
options.templates = true;
break;
case "--protection":
case "-p":
options.protection = true;
break;
case "--mode":
case "-m":
if (nextArg && ["solo", "team"].includes(nextArg)) {
options.mode = nextArg as "solo" | "team";
i++;
}
break;
case "--dry-run":
options.dryRun = true;
break;
case "--all":
options.templates = true;
options.protection = true;
break;
}
}
return options;
}
function printHelp(): void {
console.log(`
GitHub Project Initialization
Initialize GitHub project with labels, templates, and branch protection.
USAGE:
gh-init-project.ts [OPTIONS]
OPTIONS:
--labels, -l <scheme> Label scheme: standard, simple, minimal, none (default: standard)
--templates, -t Create issue and PR templates
--protection, -p Enable branch protection on default branch
--mode, -m <mode> Mode: solo or team (default: solo)
--all Enable templates and protection
--dry-run Show what would be done without making changes
--help, -h Show this help
LABEL SCHEMES:
standard Full scheme: type, status, priority labels (16 labels)
simple Minimal for solo: type, priority, icebox (6 labels)
minimal Bare minimum: bug, feature, icebox (3 labels)
none Don't create any labels
MODE DIFFERENCES:
solo Standard labels only
team Adds status:needs-review, status:changes-requested, needs:discussion
TEMPLATES CREATED:
.github/ISSUE_TEMPLATE/feature.md Feature request
.github/ISSUE_TEMPLATE/bug.md Bug report
.github/ISSUE_TEMPLATE/task.md Task/chore
.github/pull_request_template.md PR template with context references
EXAMPLES:
# Initialize with standard labels and templates
gh-init-project.ts --labels standard --templates
# Full initialization for team
gh-init-project.ts --mode team --all
# Preview what would be created
gh-init-project.ts --all --dry-run
# Minimal solo setup
gh-init-project.ts --labels simple --templates
`);
}
// === MAIN ===
async function main(): Promise<void> {
// Check if we can access gh
const ghCheck = await runCommand(["gh", "auth", "status"]);
if (!ghCheck.success && !ghCheck.error.includes("Logged in")) {
console.error("Error: GitHub CLI not authenticated. Run 'gh auth login' first.");
Deno.exit(1);
}
// Check if we're in a repo
const repoCheck = await runCommand(["gh", "repo", "view", "--json", "name"]);
if (!repoCheck.success) {
console.error("Error: Not in a GitHub repository. Run 'gh repo create' or add a remote first.");
Deno.exit(1);
}
const options = parseArgs(Deno.args);
// If no specific options, show interactive prompt or use defaults
if (!options.templates && !options.protection && options.labels === "standard") {
console.log("No options specified. Using defaults:");
console.log(" --labels standard --templates");
console.log("");
console.log("Use --help to see all options, or --all for full initialization.");
console.log("");
options.templates = true;
}
const result = await initialize(options);
// Exit with error if there were failures
if (result.errors.length > 0 && result.labelsCreated.length === 0 && result.templatesCreated.length === 0) {
Deno.exit(1);
}
}
main();
#!/usr/bin/env -S deno run --allow-run --allow-read --allow-write
/**
* GitHub to Context Network Sync
*
* Generates context network updates from GitHub state.
* Bridges GitHub work items with context/status.md and context/decisions.md.
*
* Usage:
* deno run --allow-run --allow-read --allow-write gh-sync-context.ts
* deno run --allow-run gh-sync-context.ts --dry-run
* deno run --allow-run --allow-read --allow-write gh-sync-context.ts --status --decisions
* deno run --allow-run --allow-read --allow-write gh-sync-context.ts --output ./context
*/
// === INTERFACES ===
interface Issue {
number: number;
title: string;
state: string;
createdAt: string;
updatedAt: string;
closedAt: string | null;
labels: { name: string }[];
milestone: { title: string } | null;
body: string | null;
url: string;
}
interface PullRequest {
number: number;
title: string;
state: string;
mergedAt: string | null;
createdAt: string;
headRefName: string;
body: string | null;
url: string;
}
interface Milestone {
title: string;
state: string;
description: string | null;
dueOn: string | null;
openIssues: number;
closedIssues: number;
}
interface SyncOptions {
status: boolean;
decisions: boolean;
outputDir: string;
dryRun: boolean;
}
interface StatusUpdate {
currentMilestone: string | null;
activeIssues: { number: number; title: string; labels: string[] }[];
activePRs: { number: number; title: string; branch: string }[];
recentlyCompleted: { number: number; title: string; closedAt: string }[];
timestamp: string;
}
interface DecisionCandidate {
source: "issue" | "pr";
number: number;
title: string;
body: string | null;
closedAt: string;
url: string;
}
// === UTILITIES ===
async function runCommand(
cmd: string[]
): Promise<{ success: boolean; output: string; error: string }> {
try {
const command = new Deno.Command(cmd[0], {
args: cmd.slice(1),
stdout: "piped",
stderr: "piped",
});
const { success, stdout, stderr } = await command.output();
return {
success,
output: new TextDecoder().decode(stdout).trim(),
error: new TextDecoder().decode(stderr).trim(),
};
} catch {
return {
success: false,
output: "",
error: "Command not found or failed to execute",
};
}
}
async function fileExists(path: string): Promise<boolean> {
try {
await Deno.stat(path);
return true;
} catch {
return false;
}
}
async function ensureDirectory(path: string): Promise<void> {
try {
await Deno.mkdir(path, { recursive: true });
} catch (error) {
if (!(error instanceof Deno.errors.AlreadyExists)) {
throw error;
}
}
}
function formatDate(dateString: string): string {
const date = new Date(dateString);
return date.toISOString().split("T")[0];
}
// === DATA FETCHING ===
async function getRepoInfo(): Promise<{ name: string; owner: string }> {
const result = await runCommand([
"gh",
"repo",
"view",
"--json",
"name,owner",
]);
if (!result.success || !result.output) {
return { name: "unknown", owner: "unknown" };
}
try {
const data = JSON.parse(result.output);
return { name: data.name, owner: data.owner?.login || "unknown" };
} catch {
return { name: "unknown", owner: "unknown" };
}
}
async function getOpenIssues(): Promise<Issue[]> {
const result = await runCommand([
"gh",
"issue",
"list",
"--state",
"open",
"--limit",
"50",
"--json",
"number,title,state,createdAt,updatedAt,closedAt,labels,milestone,body,url",
]);
if (!result.success || !result.output) {
return [];
}
try {
return JSON.parse(result.output);
} catch {
return [];
}
}
async function getRecentlyClosedIssues(days: number = 14): Promise<Issue[]> {
// Get closed issues, then filter by date client-side
const result = await runCommand([
"gh",
"issue",
"list",
"--state",
"closed",
"--limit",
"50",
"--json",
"number,title,state,createdAt,updatedAt,closedAt,labels,milestone,body,url",
]);
if (!result.success || !result.output) {
return [];
}
try {
const issues: Issue[] = JSON.parse(result.output);
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
return issues.filter(i => {
if (!i.closedAt) return false;
return new Date(i.closedAt) >= cutoff;
});
} catch {
return [];
}
}
async function getOpenPRs(): Promise<PullRequest[]> {
const result = await runCommand([
"gh",
"pr",
"list",
"--state",
"open",
"--limit",
"20",
"--json",
"number,title,state,mergedAt,createdAt,headRefName,body,url",
]);
if (!result.success || !result.output) {
return [];
}
try {
return JSON.parse(result.output);
} catch {
return [];
}
}
async function getRecentlyMergedPRs(days: number = 14): Promise<PullRequest[]> {
const result = await runCommand([
"gh",
"pr",
"list",
"--state",
"merged",
"--limit",
"30",
"--json",
"number,title,state,mergedAt,createdAt,headRefName,body,url",
]);
if (!result.success || !result.output) {
return [];
}
try {
const prs: PullRequest[] = JSON.parse(result.output);
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
return prs.filter(p => {
if (!p.mergedAt) return false;
return new Date(p.mergedAt) >= cutoff;
});
} catch {
return [];
}
}
async function getActiveMilestone(): Promise<Milestone | null> {
const result = await runCommand([
"gh",
"api",
"repos/{owner}/{repo}/milestones",
"--jq",
"map(select(.state == \"open\")) | sort_by(.due_on) | .[0] | {title: .title, state: .state, description: .description, dueOn: .due_on, openIssues: .open_issues, closedIssues: .closed_issues}",
]);
if (!result.success || !result.output || result.output === "null") {
return null;
}
try {
return JSON.parse(result.output);
} catch {
return null;
}
}
// === STATUS GENERATION ===
async function generateStatusUpdate(): Promise<StatusUpdate> {
const openIssues = await getOpenIssues();
const openPRs = await getOpenPRs();
const recentlyClosed = await getRecentlyClosedIssues(7);
const milestone = await getActiveMilestone();
// Sort issues by priority (critical > high > medium > low > unlabeled)
const priorityOrder = ["priority:critical", "priority:high", "priority:medium", "priority:low"];
const sortedIssues = openIssues.sort((a, b) => {
const aLabels = a.labels.map(l => l.name);
const bLabels = b.labels.map(l => l.name);
const aPriority = priorityOrder.findIndex(p => aLabels.includes(p));
const bPriority = priorityOrder.findIndex(p => bLabels.includes(p));
// -1 means no priority label, put at end
const aScore = aPriority === -1 ? 999 : aPriority;
const bScore = bPriority === -1 ? 999 : bPriority;
return aScore - bScore;
});
return {
currentMilestone: milestone?.title || null,
activeIssues: sortedIssues.slice(0, 10).map(i => ({
number: i.number,
title: i.title,
labels: i.labels.map(l => l.name),
})),
activePRs: openPRs.map(p => ({
number: p.number,
title: p.title,
branch: p.headRefName,
})),
recentlyCompleted: recentlyClosed.slice(0, 5).map(i => ({
number: i.number,
title: i.title,
closedAt: i.closedAt!,
})),
timestamp: new Date().toISOString(),
};
}
function formatStatusMarkdown(status: StatusUpdate, repoInfo: { name: string; owner: string }): string {
const lines: string[] = [];
lines.push("## GitHub Sync");
lines.push("");
lines.push(`*Last synced: ${formatDate(status.timestamp)}*`);
lines.push("");
// Current Milestone
if (status.currentMilestone) {
lines.push(`### Current Milestone: ${status.currentMilestone}`);
lines.push("");
}
// Active Issues
lines.push("### Active Issues");
lines.push("");
if (status.activeIssues.length === 0) {
lines.push("No open issues.");
} else {
for (const issue of status.activeIssues) {
const labels = issue.labels.length > 0 ? ` (${issue.labels.join(", ")})` : "";
lines.push(`- [#${issue.number}](https://github.com/${repoInfo.owner}/${repoInfo.name}/issues/${issue.number}): ${issue.title}${labels}`);
}
}
lines.push("");
// Active PRs
if (status.activePRs.length > 0) {
lines.push("### Active Pull Requests");
lines.push("");
for (const pr of status.activePRs) {
lines.push(`- [#${pr.number}](https://github.com/${repoInfo.owner}/${repoInfo.name}/pull/${pr.number}): ${pr.title} (\`${pr.branch}\`)`);
}
lines.push("");
}
// Recently Completed
if (status.recentlyCompleted.length > 0) {
lines.push("### Recently Completed (Last 7 Days)");
lines.push("");
for (const issue of status.recentlyCompleted) {
lines.push(`- [#${issue.number}](https://github.com/${repoInfo.owner}/${repoInfo.name}/issues/${issue.number}): ${issue.title} (${formatDate(issue.closedAt)})`);
}
lines.push("");
}
return lines.join("\n");
}
// === DECISION EXTRACTION ===
async function extractDecisionCandidates(): Promise<DecisionCandidate[]> {
const candidates: DecisionCandidate[] = [];
// Get closed issues with "decision" label
const issueResult = await runCommand([
"gh",
"issue",
"list",
"--state",
"closed",
"--label",
"decision",
"--limit",
"20",
"--json",
"number,title,body,closedAt,url",
]);
if (issueResult.success && issueResult.output) {
try {
const issues = JSON.parse(issueResult.output);
for (const issue of issues) {
candidates.push({
source: "issue",
number: issue.number,
title: issue.title,
body: issue.body,
closedAt: issue.closedAt,
url: issue.url,
});
}
} catch {
// Ignore parse errors
}
}
// Also look for PRs with "decision" or "architecture" in title/body
const prResult = await runCommand([
"gh",
"pr",
"list",
"--state",
"merged",
"--limit",
"30",
"--json",
"number,title,body,mergedAt,url",
]);
if (prResult.success && prResult.output) {
try {
const prs = JSON.parse(prResult.output);
for (const pr of prs) {
// Check if PR body contains decision-related keywords
const bodyLower = (pr.body || "").toLowerCase();
const titleLower = pr.title.toLowerCase();
if (
bodyLower.includes("decision") ||
bodyLower.includes("adr") ||
bodyLower.includes("architecture") ||
titleLower.includes("decision") ||
titleLower.includes("adr")
) {
candidates.push({
source: "pr",
number: pr.number,
title: pr.title,
body: pr.body,
closedAt: pr.mergedAt,
url: pr.url,
});
}
}
} catch {
// Ignore parse errors
}
}
// Sort by date, most recent first
return candidates.sort((a, b) =>
new Date(b.closedAt).getTime() - new Date(a.closedAt).getTime()
);
}
function formatDecisionCandidates(candidates: DecisionCandidate[]): string {
const lines: string[] = [];
lines.push("## Decision Candidates from GitHub");
lines.push("");
lines.push("*These items were found in GitHub and may contain decisions worth documenting.*");
lines.push("*Review and extract key decisions to `context/decisions.md`.*");
lines.push("");
if (candidates.length === 0) {
lines.push("No decision candidates found.");
lines.push("");
lines.push("Tip: Use the `decision` label on issues that contain key decisions.");
return lines.join("\n");
}
for (const candidate of candidates) {
const sourceLabel = candidate.source === "issue" ? "Issue" : "PR";
lines.push(`### ${sourceLabel} #${candidate.number}: ${candidate.title}`);
lines.push("");
lines.push(`- **Closed:** ${formatDate(candidate.closedAt)}`);
lines.push(`- **Link:** ${candidate.url}`);
lines.push("");
if (candidate.body) {
// Extract first 500 chars of body as preview
const preview = candidate.body.substring(0, 500);
const truncated = candidate.body.length > 500 ? "..." : "";
lines.push("**Preview:**");
lines.push("```");
lines.push(preview + truncated);
lines.push("```");
lines.push("");
}
lines.push("---");
lines.push("");
}
return lines.join("\n");
}
// === FILE OPERATIONS ===
async function updateStatusFile(
statusContent: string,
outputDir: string,
dryRun: boolean
): Promise<boolean> {
const statusPath = `${outputDir}/status.md`;
if (dryRun) {
console.log(`[DRY RUN] Would update: ${statusPath}`);
console.log("");
console.log("--- Content Preview ---");
console.log(statusContent);
console.log("--- End Preview ---");
return true;
}
// Check if status.md exists
const exists = await fileExists(statusPath);
if (exists) {
// Read existing file
const existing = await Deno.readTextFile(statusPath);
// Find or create GitHub Sync section
const syncMarker = "## GitHub Sync";
const syncIndex = existing.indexOf(syncMarker);
let newContent: string;
if (syncIndex !== -1) {
// Find the next ## heading or end of file
const nextHeading = existing.indexOf("\n## ", syncIndex + 1);
const endIndex = nextHeading !== -1 ? nextHeading : existing.length;
// Replace the GitHub Sync section
newContent = existing.substring(0, syncIndex) + statusContent + existing.substring(endIndex);
} else {
// Append to end
newContent = existing.trimEnd() + "\n\n" + statusContent;
}
await Deno.writeTextFile(statusPath, newContent);
} else {
// Create new file with header
await ensureDirectory(outputDir);
const header = "# Status\n\nCurrent project state and recent changes.\n\n";
await Deno.writeTextFile(statusPath, header + statusContent);
}
console.log(`Updated: ${statusPath}`);
return true;
}
async function writeDecisionCandidates(
content: string,
outputDir: string,
dryRun: boolean
): Promise<boolean> {
const date = new Date().toISOString().split("T")[0];
const candidatesPath = `${outputDir}/decision-candidates-${date}.md`;
if (dryRun) {
console.log(`[DRY RUN] Would write: ${candidatesPath}`);
console.log("");
console.log("--- Content Preview ---");
console.log(content);
console.log("--- End Preview ---");
return true;
}
await ensureDirectory(outputDir);
await Deno.writeTextFile(candidatesPath, content);
console.log(`Created: ${candidatesPath}`);
return true;
}
// === MAIN SYNC ===
async function sync(options: SyncOptions): Promise<void> {
console.log("");
console.log("=".repeat(50));
console.log("GITHUB TO CONTEXT NETWORK SYNC");
console.log("=".repeat(50));
console.log("");
if (options.dryRun) {
console.log("[DRY RUN MODE - No files will be modified]");
console.log("");
}
const repoInfo = await getRepoInfo();
console.log(`Repository: ${repoInfo.owner}/${repoInfo.name}`);
console.log(`Output directory: ${options.outputDir}`);
console.log("");
// Generate and write status update
if (options.status) {
console.log("-".repeat(50));
console.log("SYNCING STATUS");
console.log("-".repeat(50));
const status = await generateStatusUpdate();
const statusMarkdown = formatStatusMarkdown(status, repoInfo);
await updateStatusFile(statusMarkdown, options.outputDir, options.dryRun);
console.log("");
console.log("Status summary:");
console.log(` Current milestone: ${status.currentMilestone || "none"}`);
console.log(` Open issues: ${status.activeIssues.length}`);
console.log(` Open PRs: ${status.activePRs.length}`);
console.log(` Recently completed: ${status.recentlyCompleted.length}`);
console.log("");
}
// Extract and write decision candidates
if (options.decisions) {
console.log("-".repeat(50));
console.log("EXTRACTING DECISION CANDIDATES");
console.log("-".repeat(50));
const candidates = await extractDecisionCandidates();
const decisionsMarkdown = formatDecisionCandidates(candidates);
await writeDecisionCandidates(decisionsMarkdown, options.outputDir, options.dryRun);
console.log("");
console.log(`Found ${candidates.length} decision candidates`);
if (candidates.length > 0) {
console.log("Review the generated file and extract key decisions to decisions.md");
}
console.log("");
}
console.log("-".repeat(50));
console.log("SYNC COMPLETE");
console.log("-".repeat(50));
console.log("");
console.log("Next steps:");
console.log("1. Review generated/updated files");
console.log("2. Move key decisions to context/decisions.md");
console.log("3. Update context/architecture.md if workflow changed");
}
// === ARGUMENT PARSING ===
function parseArgs(args: string[]): SyncOptions {
const options: SyncOptions = {
status: false,
decisions: false,
outputDir: "context",
dryRun: false,
};
// Default to status if no specific options
let hasSpecificOption = false;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
const nextArg = args[i + 1];
switch (arg) {
case "--help":
case "-h":
printHelp();
Deno.exit(0);
break;
case "--status":
case "-s":
options.status = true;
hasSpecificOption = true;
break;
case "--decisions":
case "-d":
options.decisions = true;
hasSpecificOption = true;
break;
case "--output":
case "-o":
if (nextArg) {
options.outputDir = nextArg;
i++;
}
break;
case "--dry-run":
options.dryRun = true;
break;
case "--all":
options.status = true;
options.decisions = true;
hasSpecificOption = true;
break;
}
}
// Default to status sync if no specific option given
if (!hasSpecificOption) {
options.status = true;
}
return options;
}
function printHelp(): void {
console.log(`
GitHub to Context Network Sync
Generates context network updates from GitHub state.
USAGE:
gh-sync-context.ts [OPTIONS]
OPTIONS:
--status, -s Sync status.md with current GitHub state
--decisions, -d Extract decision candidates from labeled issues/PRs
--all Sync both status and decisions
--output, -o <dir> Output directory (default: context)
--dry-run Show what would be written without writing
--help, -h Show this help
WHAT IT DOES:
Status sync (--status):
- Updates or creates ## GitHub Sync section in status.md
- Lists current milestone, active issues, active PRs
- Shows recently completed items (last 7 days)
- Preserves other content in status.md
Decision extraction (--decisions):
- Finds closed issues with "decision" label
- Finds merged PRs with decision/architecture keywords
- Creates decision-candidates-{date}.md for review
- You manually move relevant decisions to decisions.md
EXAMPLES:
# Sync status to context/status.md
gh-sync-context.ts
# Preview what would be synced
gh-sync-context.ts --dry-run
# Sync both status and extract decisions
gh-sync-context.ts --all
# Custom output directory
gh-sync-context.ts --status --output ./docs/context
TIPS:
- Use the "decision" label on issues that contain key decisions
- Run weekly as part of context sync ceremony
- Review decision candidates and extract to decisions.md
`);
}
// === MAIN ===
async function main(): Promise<void> {
// Check gh is available
const ghCheck = await runCommand(["gh", "auth", "status"]);
if (!ghCheck.success && !ghCheck.error.includes("Logged in")) {
console.error("Error: GitHub CLI not authenticated. Run 'gh auth login' first.");
Deno.exit(1);
}
const options = parseArgs(Deno.args);
await sync(options);
}
main();
#!/usr/bin/env -S deno run --allow-run
/**
* GitHub CLI Verification
*
* Verifies GitHub CLI installation and authentication status.
* Use this to check GH0 state before any GitHub operations.
*
* Usage:
* deno run --allow-run gh-verify.ts
* deno run --allow-run gh-verify.ts --json
*
* Exit codes:
* 0 - All good (gh installed, logged in, in repo)
* 1 - gh CLI not installed
* 2 - gh CLI installed but not logged in
* 3 - Logged in but not in a git repository
*/
// === INTERFACES ===
interface VerifyResult {
ghInstalled: boolean;
ghVersion: string | null;
authenticated: boolean;
username: string | null;
authMethod: string | null;
inRepo: boolean;
repoName: string | null;
repoOwner: string | null;
defaultBranch: string | null;
recommendations: string[];
}
// === UTILITIES ===
async function runCommand(
cmd: string[]
): Promise<{ success: boolean; output: string; error: string }> {
try {
const command = new Deno.Command(cmd[0], {
args: cmd.slice(1),
stdout: "piped",
stderr: "piped",
});
const { success, stdout, stderr } = await command.output();
return {
success,
output: new TextDecoder().decode(stdout).trim(),
error: new TextDecoder().decode(stderr).trim(),
};
} catch {
return {
success: false,
output: "",
error: "Command not found or failed to execute",
};
}
}
// === VERIFICATION CHECKS ===
async function checkGhInstalled(): Promise<{
installed: boolean;
version: string | null;
}> {
const result = await runCommand(["gh", "--version"]);
if (!result.success) {
return { installed: false, version: null };
}
// Parse version from output like "gh version 2.40.1 (2024-01-01)"
const versionMatch = result.output.match(/gh version (\S+)/);
const version = versionMatch ? versionMatch[1] : result.output.split("\n")[0];
return { installed: true, version };
}
async function checkAuthentication(): Promise<{
authenticated: boolean;
username: string | null;
authMethod: string | null;
}> {
const result = await runCommand(["gh", "auth", "status"]);
if (!result.success && !result.output && !result.error) {
return { authenticated: false, username: null, authMethod: null };
}
// gh auth status outputs to stderr on success (weird but true)
const output = result.error || result.output;
// Check for "Logged in to" pattern
const loggedIn =
output.includes("Logged in to") || output.includes("logged in");
if (!loggedIn) {
return { authenticated: false, username: null, authMethod: null };
}
// Parse username from "Logged in to github.com account username"
const usernameMatch = output.match(/account\s+(\S+)/i);
const username = usernameMatch ? usernameMatch[1] : null;
// Parse auth method
let authMethod: string | null = null;
if (output.includes("oauth_token")) authMethod = "oauth_token";
else if (output.includes("ssh")) authMethod = "ssh";
else if (output.includes("token")) authMethod = "token";
return { authenticated: true, username, authMethod };
}
async function checkRepository(): Promise<{
inRepo: boolean;
repoName: string | null;
repoOwner: string | null;
defaultBranch: string | null;
}> {
// First check if we're in a git repo
const gitCheck = await runCommand(["git", "rev-parse", "--is-inside-work-tree"]);
if (!gitCheck.success || gitCheck.output !== "true") {
return { inRepo: false, repoName: null, repoOwner: null, defaultBranch: null };
}
// Try to get repo info from gh
const repoResult = await runCommand([
"gh",
"repo",
"view",
"--json",
"name,owner,defaultBranchRef",
]);
if (!repoResult.success) {
// In a git repo but not linked to GitHub or no remote
return { inRepo: true, repoName: null, repoOwner: null, defaultBranch: null };
}
try {
const repoInfo = JSON.parse(repoResult.output);
return {
inRepo: true,
repoName: repoInfo.name || null,
repoOwner: repoInfo.owner?.login || null,
defaultBranch: repoInfo.defaultBranchRef?.name || null,
};
} catch {
return { inRepo: true, repoName: null, repoOwner: null, defaultBranch: null };
}
}
// === MAIN VERIFICATION ===
async function verify(): Promise<VerifyResult> {
const recommendations: string[] = [];
// Check gh installation
const { installed: ghInstalled, version: ghVersion } = await checkGhInstalled();
if (!ghInstalled) {
recommendations.push("Install GitHub CLI: https://cli.github.com/");
recommendations.push(" macOS: brew install gh");
recommendations.push(" Linux: sudo apt install gh");
recommendations.push(" Windows: winget install --id GitHub.cli");
return {
ghInstalled: false,
ghVersion: null,
authenticated: false,
username: null,
authMethod: null,
inRepo: false,
repoName: null,
repoOwner: null,
defaultBranch: null,
recommendations,
};
}
// Check authentication
const { authenticated, username, authMethod } = await checkAuthentication();
if (!authenticated) {
recommendations.push("Authenticate with GitHub: gh auth login");
return {
ghInstalled: true,
ghVersion,
authenticated: false,
username: null,
authMethod: null,
inRepo: false,
repoName: null,
repoOwner: null,
defaultBranch: null,
recommendations,
};
}
// Check repository context
const { inRepo, repoName, repoOwner, defaultBranch } = await checkRepository();
if (!inRepo) {
recommendations.push("Initialize git repository: git init");
recommendations.push("Or navigate to an existing git repository");
} else if (!repoName) {
recommendations.push("Link repository to GitHub: gh repo create --source=. --push");
recommendations.push("Or add remote: git remote add origin <url>");
}
return {
ghInstalled,
ghVersion,
authenticated,
username,
authMethod,
inRepo,
repoName,
repoOwner,
defaultBranch,
recommendations,
};
}
// === OUTPUT FORMATTING ===
function formatResult(result: VerifyResult): string {
const lines: string[] = [];
lines.push("=".repeat(50));
lines.push("GITHUB CLI VERIFICATION");
lines.push("=".repeat(50));
lines.push("");
// Installation status
const installIcon = result.ghInstalled ? "[OK]" : "[FAIL]";
lines.push(`${installIcon} GitHub CLI installed`);
if (result.ghVersion) {
lines.push(` Version: ${result.ghVersion}`);
}
lines.push("");
// Authentication status
if (result.ghInstalled) {
const authIcon = result.authenticated ? "[OK]" : "[FAIL]";
lines.push(`${authIcon} Authentication`);
if (result.authenticated) {
lines.push(` User: ${result.username || "unknown"}`);
if (result.authMethod) {
lines.push(` Method: ${result.authMethod}`);
}
}
lines.push("");
}
// Repository status
if (result.authenticated) {
const repoIcon = result.repoName ? "[OK]" : result.inRepo ? "[WARN]" : "[FAIL]";
lines.push(`${repoIcon} Repository context`);
if (result.repoName) {
lines.push(` Repo: ${result.repoOwner}/${result.repoName}`);
if (result.defaultBranch) {
lines.push(` Default branch: ${result.defaultBranch}`);
}
} else if (result.inRepo) {
lines.push(" Git repo exists but not linked to GitHub");
} else {
lines.push(" Not in a git repository");
}
lines.push("");
}
// Recommendations
if (result.recommendations.length > 0) {
lines.push("-".repeat(50));
lines.push("RECOMMENDATIONS");
lines.push("-".repeat(50));
for (const rec of result.recommendations) {
lines.push(rec);
}
lines.push("");
}
// Overall status
lines.push("-".repeat(50));
if (!result.ghInstalled) {
lines.push("Status: GH0 - No GitHub CLI");
lines.push("Next: Install GitHub CLI");
} else if (!result.authenticated) {
lines.push("Status: GH0 - Not authenticated");
lines.push("Next: Run 'gh auth login'");
} else if (!result.inRepo) {
lines.push("Status: GH1 - No repository");
lines.push("Next: Initialize or navigate to a git repository");
} else if (!result.repoName) {
lines.push("Status: GH1 - Repository not linked to GitHub");
lines.push("Next: Link with 'gh repo create' or add remote");
} else {
lines.push("Status: Ready for GitHub operations");
}
return lines.join("\n");
}
// === ARGUMENT PARSING ===
function parseArgs(args: string[]): { json: boolean } {
let json = false;
for (const arg of args) {
switch (arg) {
case "--help":
case "-h":
printHelp();
Deno.exit(0);
break;
case "--json":
json = true;
break;
}
}
return { json };
}
function printHelp(): void {
console.log(`
GitHub CLI Verification
Verifies GitHub CLI installation and authentication status.
USAGE:
gh-verify.ts [OPTIONS]
OPTIONS:
--json Output as JSON
--help, -h Show this help
EXIT CODES:
0 - All good (gh installed, logged in, in repo with remote)
1 - gh CLI not installed
2 - gh CLI installed but not logged in
3 - Logged in but not in a git repository or no GitHub remote
EXAMPLES:
# Check status
gh-verify.ts
# JSON output for scripting
gh-verify.ts --json
`);
}
// === MAIN ===
async function main(): Promise<void> {
const { json } = parseArgs(Deno.args);
const result = await verify();
if (json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(formatResult(result));
}
// Set exit code based on status
if (!result.ghInstalled) {
Deno.exit(1);
} else if (!result.authenticated) {
Deno.exit(2);
} else if (!result.inRepo || !result.repoName) {
Deno.exit(3);
}
Deno.exit(0);
}
main();
Related skills
How it compares
Choose github-agile over generic git skills when execution tracking must live in GitHub Issues and milestones while architectural rationale persists in a separate context-network directory.
FAQ
What GitHub artifacts does github-agile create?
github-agile creates .github/ISSUE_TEMPLATE files for features, bugs, and tasks, a pull_request_template.md, GitHub labels from standard/simple/minimal schemes, branch protection on main, and context-network updates in status.md and decisions.md.
Which scripts ship with github-agile?
github-agile bundles four Deno scripts: gh-verify.ts checks CLI auth, gh-init-project.ts scaffolds labels and templates, gh-audit.ts scores workflow health 0–100, and gh-sync-context.ts writes milestone summaries into the context network.
When should a developer invoke github-agile?
Invoke github-agile when GitHub Issues lack labels or milestones, PRs omit issue links and context, commits land directly on main, or context/status.md no longer reflects open work—each maps to a named diagnostic state GH0 through GH8.