Skip to content
Back to skills

Project Sync

BSecurity

Bulk-sync existing repository issues to GitHub Project boards with proper

  • 8 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 5, 2026
developmentpythongoshellbashnodegitapiperformance

Works with

  • claude code
  • cursor
  • vscode
  • terminal
  • cli
  • api

Security analysis

B88/100
  • criticalSends environment variables or credentials to an external URL

Pro scans all 2 files and shows the line behind each finding

Scanned November 5, 2026

npx -y skills add nightgauge/nightgauge --skill project-sync --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Project Sync?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Project Sync
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/nightgauge-project-sync/badge)](https://www.skillsdirectory.com/skills/nightgauge-project-sync)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: project-sync
description: Bulk-sync existing repository issues to GitHub Project boards with proper
  field mappings. Use during onboarding or catch-up to sync dates from
  milestones and the Status field.
license: Apache-2.0
metadata:
  author: nightgauge
  version: "1.1.1"
  source: https://github.com/nightgauge/nightgauge
allowed-tools: Read Bash Task AskUserQuestion
disable-model-invocation: true
---

# Nightgauge Project Sync

## Description

Bulk-synchronize existing repository issues to GitHub Project boards with proper
field mappings. Designed for onboarding scenarios where a repository already has
many issues that need to be synced to the Nightgauge project board.

**Key Features:**

- Bulk date sync from milestones (milestone dates → project Start/Target Date
  fields) via `nightgauge project set-field --start-date/--target-date`
- Status field sync (for all issues at once)
- Dry-run report mode (preview changes without applying)
- Idempotent (safe to run multiple times)
- Clear output showing changes made

**Use Cases:**

- Onboarding existing repositories to Nightgauge
- Catch-up after manual issue creation
- Post-migration field synchronization
- Periodic maintenance to fix field drift

**When to Use:**

- After setting up `.nightgauge/config.yaml` for a new repository
- When bulk-creating issues outside the pipeline
- After milestone changes that affect multiple issues
- When the Status field gets out of sync with issue states

## Invocation

| Tool        | Command                                 |
| ----------- | --------------------------------------- |
| Claude Code | `/nightgauge:project-sync [options]`    |
| Copilot     | Invoke via Agent Skills extension       |
| Cursor      | Run via Agent Skills or direct SKILL.md |
| Headless    | `claude -p "/nightgauge:project-sync"`  |

## Arguments

### Core Options

| Argument           | Description                                                | Default |
| ------------------ | ---------------------------------------------------------- | ------- |
| `--mode MODE`      | Sync mode: full/dates-only/status-only/report              | `full`  |
| `--dry-run`        | Preview changes without applying (alias for --mode=report) | `false` |
| `--milestone NAME` | Filter by milestone name                                   | (all)   |
| `--label PATTERN`  | Filter by label pattern (can repeat)                       | (all)   |

### Sync Modes

| Mode          | Description                                        |
| ------------- | -------------------------------------------------- |
| `full`        | Sync both dates and status fields (default)        |
| `dates-only`  | Sync only Start/Target Date fields from milestones |
| `status-only` | Sync only Status field                             |
| `report`      | Dry-run mode - preview changes without applying    |

### Examples

```bash
# Dry run to preview all changes
/nightgauge:project-sync --dry-run

# Full sync of all open issues
/nightgauge:project-sync --mode full

# Sync only dates from milestones
/nightgauge:project-sync --mode dates-only

# Sync only status labels
/nightgauge:project-sync --mode status-only

# Filter by milestone
/nightgauge:project-sync --milestone "Sprint 23"

# Filter by label pattern
/nightgauge:project-sync --label "priority:high"

# Combine filters
/nightgauge:project-sync --milestone "v2.0" --label "type:feature"
```

## Philosophy

### Deterministic vs Probabilistic Split

This skill follows the Nightgauge architecture principle of using
deterministic operations where possible:

| Operation                 | Type          | Rationale                               |
| ------------------------- | ------------- | --------------------------------------- |
| Label→field mapping       | Deterministic | Fixed mapping, no interpretation needed |
| Milestone date extraction | Deterministic | Direct field access from GitHub API     |
| Issue fetching/pagination | Deterministic | GraphQL query with fixed parameters     |
| Field comparison          | Deterministic | Simple equality check                   |
| Progress display          | Probabilistic | AI formats output for readability       |
| Error messages            | Probabilistic | AI provides context-aware explanations  |

**Cost Efficiency**: All bulk operations handled by the Go binary
(`nightgauge project add`), minimizing LLM token consumption. SKILL.md only handles
user interaction and output formatting.

### Context Isolation

This is a **standalone utility skill**, not part of the main pipeline. It:

- Does NOT read pipeline context files (`nightgauge layout path pipeline`)
- Does NOT write pipeline handoff files
- Does NOT affect pipeline state
- Can be run at any time without affecting pipeline execution

## Configuration

Configuration is read from `.nightgauge/config.yaml`:

```yaml
# Project board configuration
project:
  number: 1 # GitHub Project number
  id: "PVT_kwHOABC123" # Project global ID
  status_field_id: "PVTSSF_lAHOABC123" # Status field ID
  priority_field_id: "PVTSSF_lAHODEF456" # Priority field ID
  size_field_id: "PVTSSF_lAHOGHI789" # Size field ID
  start_date_field_id: "PVTF_lAHOJKL012" # Start Date field ID
  target_date_field_id: "PVTF_lAHOMNO345" # Target Date field ID

# Sync behavior (optional)
project_sync:
  # Issues without milestones will be skipped (not an error)
  skip_no_milestone: true

  # Idempotency - only update if value differs
  compare_before_update: true

  # Pagination
  page_size: 100
```

**Field ID Discovery**:

To get your project field IDs, run:

```bash
nightgauge forge graphql -f query='query($owner:String!,$number:Int!){organization(login:$owner){projectV2(number:$number){fields(first:50){nodes{... on ProjectV2FieldCommon{id,name,dataType}}}}}}' -f owner=<org> -F number=<number>
```

**Defaults**: If `.nightgauge/config.yaml` is missing or incomplete, the
skill will gracefully skip operations and report what's needed.

## Prerequisites

- **nightgauge binary** - Required for forge API access (`nightgauge forge`)
- **jq** - JSON parsing in shell scripts
- **Bash 4+** - Shell scripting
- **Project board configured** - `.nightgauge/config.yaml` must have
  `project.number` and field IDs
- **Milestone dates** - Issues must have milestones with due dates for date sync

## Workflow

### Phase 0: Configuration Check

### Phase 0: Environment Preflight

**PURPOSE**: Verify the nightgauge binary is available and the environment
is healthy before executing any pipeline stage. Skills halt immediately with
actionable instructions when this check fails.

Three-layer check:

1. **Binary discovery** — shell-level, runs before invoking the binary
2. **Doctor check** — binary-level, full environment health via `doctor --json`
3. **Project environment** — the checks the project declares under
   `preflight.environment` (#101); opt-in, silent when none are declared

The binary discovery cascade below is **provider-neutral** (issue 4029): it works
identically under every adapter (Claude, Codex, Gemini, …) and references no
VSCode-extension-specific path. The host that spawns the skill (the VSCode
extension's `skillRunner`, or the Go `nightgauge` binary in auto/CLI mode)
resolves the binary authoritatively and exports `NIGHTGAUGE_BIN`, which
this cascade honors first. The remaining steps (PATH, the development-only repo
`bin/` and worktree canonical-repo `bin/`, `~/go/bin`) are fallbacks for direct
or terminal invocation.

This intentionally **diverges** from
`claude-plugins/nightgauge/hooks/lib/guard.sh` (issue 3262 → issue 4029): guard.sh is
Claude-Code-only and retains a trailing `~/.vscode/extensions/...` glob to serve
the standalone-terminal-Claude case (where no host exports `NIGHTGAUGE_BIN`).
Skills must stay portable Markdown and cannot reference that VSCode-only path —
so the shared resolution order (`NIGHTGAUGE_BIN` → PATH → gated repo → gated
canonical → `~/go/bin`) stays in sync, and only guard.sh carries the extra Claude-only
fallback. The `nightgauge preflight skill-portability` gate enforces that
no skill reintroduces a `.vscode/extensions` path, and that every cascade keeps
the `NIGHTGAUGE_DEV_REPO_BIN` gate (#2758): a foreign repository's
`bin/nightgauge` must never run.

After the cascade resolves `$BINARY`, the preflight prepends
`dirname($BINARY)` to `PATH` so subsequent bare `nightgauge ...`
invocations later in the same skill body resolve through PATH rather than
failing with "command not found" (issue 3262).

```bash
# Layer 1: provider-neutral binary discovery (NIGHTGAUGE_BIN → PATH → gated repo
# bin → gated canonical-repo bin → ~/go/bin). No VSCode-extension path — see issue 4029.
# Repo bins need NIGHTGAUGE_DEV_REPO_BIN=1 and a verified core checkout (#2758).
BINARY="${NIGHTGAUGE_BIN:-}"
[ -n "$BINARY" ] && [ ! -x "$BINARY" ] && BINARY=""
[ -z "$BINARY" ] && BINARY=$(command -v nightgauge 2>/dev/null || echo "")
if [ -z "$BINARY" ] && [ "${NIGHTGAUGE_DEV_REPO_BIN:-}" = "1" ]; then
  for NG_ROOT in "$(git rev-parse --show-toplevel 2>/dev/null)" \
    "$(cd "$(git rev-parse --git-common-dir 2>/dev/null)/.." 2>/dev/null && pwd)"; do
    [ -x "$NG_ROOT/bin/nightgauge" ] || continue
    grep -qx 'module github.com/nightgauge/nightgauge' "$NG_ROOT/go.mod" 2>/dev/null || continue
    NG_ORIGIN="$(git -C "$NG_ROOT" config --get remote.origin.url 2>/dev/null)"
    case "${NG_ORIGIN%.git}" in
      git@github.com:nightgauge/nightgauge | https://github.com/nightgauge/nightgauge | \
        ssh://git@github.com/nightgauge/nightgauge) BINARY="$NG_ROOT/bin/nightgauge"; break ;;
    esac
  done
fi
[ -z "$BINARY" ] && [ -x "$HOME/go/bin/nightgauge" ] && BINARY="$HOME/go/bin/nightgauge"
if [ -z "$BINARY" ]; then
  echo "ERROR: nightgauge binary not found via NIGHTGAUGE_BIN, PATH or ~/go/bin" >&2
  echo "" >&2
  echo "Install via: go install github.com/nightgauge/nightgauge/cmd/nightgauge@latest" >&2
  echo "Or download from: https://github.com/nightgauge/nightgauge/releases" >&2
  echo "Run \`nightgauge doctor\` after installing to verify your environment." >&2
  exit 1
fi
# Make the resolved binary callable via bare `nightgauge ...` later in this skill body (issue 3262).
export PATH="$(dirname "$BINARY"):$PATH"

# Per-repo GitHub token: export GH_TOKEN so every `gh` call in this skill
# authenticates as the *current repo's* configured user instead of the machine-
# global `gh auth` active account. `forge auth token` resolves per-repo (config
# github_user/token → GITHUB_TOKEN → `gh auth token --user <github_user>`), so
# concurrent sessions/workspaces owned by different GitHub users each use their
# own token. GH_TOKEN outranks GITHUB_TOKEN and the gh keyring. Only resolve when
# unset (an upstream env value — e.g. the VSCode extension's terminal env —
# wins); never fail preflight over it. Mirrors hooks/lib/guard.sh (sync, issue 3262).
if [ -z "${GH_TOKEN:-}" ]; then
  IB_REPO_TOKEN="$("$BINARY" forge auth token 2>/dev/null || true)"
  if [ -n "$IB_REPO_TOKEN" ]; then
    export GH_TOKEN="$IB_REPO_TOKEN"
    export GITHUB_TOKEN="${GITHUB_TOKEN:-$IB_REPO_TOKEN}"
  fi
  unset IB_REPO_TOKEN
fi

# Layer 2: full environment health check via doctor --json
DOCTOR_RESULT=$("$BINARY" doctor --json 2>/dev/null)
DOCTOR_EXIT=$?
if [ "$DOCTOR_EXIT" -eq 2 ]; then
  echo "ERROR: Environment check failed — nightgauge doctor reports broken environment." >&2
  FAILED_CHECKS=$(printf '%s\n' "$DOCTOR_RESULT" | jq -r '.failed_checks[]? // empty' 2>/dev/null)
  if [ -n "$FAILED_CHECKS" ]; then
    echo "Failing check(s): $(echo "$FAILED_CHECKS" | tr '\n' ',' | sed 's/,$//')" >&2
  fi
  printf '%s\n' "$DOCTOR_RESULT" | jq -r '.errors[]' >&2 2>/dev/null || true
  INSTALL_MSG=$(printf '%s\n' "$DOCTOR_RESULT" | jq -r '.install_instructions // empty' 2>/dev/null)
  [ -n "$INSTALL_MSG" ] && echo "$INSTALL_MSG" >&2
  exit 1
fi
if [ "$DOCTOR_EXIT" -eq 1 ]; then
  echo "WARNING: Environment has non-critical issues:" >&2
  printf '%s\n' "$DOCTOR_RESULT" | jq -r '.warnings[]' >&2 2>/dev/null || true
  # Continue — warnings do not block skill execution
fi

# Layer 3: project-declared environment checks (preflight.environment, #101).
# Opt-in: with none declared this prints nothing and continues. The verb
# writes one `[environment-halt]` line per failed check, with its remedy.
if ! "$BINARY" preflight environment >/dev/null; then
  echo "[environment-halt] Project environment check failed — fix the environment, not the code." >&2
  exit 1
fi
```

**Exit codes from `nightgauge doctor`**:

| Code | Meaning                         | Skill behavior                          |
| ---- | ------------------------------- | --------------------------------------- |
| 0    | Healthy                         | Continue                                |
| 1    | Degraded (warnings only)        | Continue with warning printed to stderr |
| 2    | Broken (required checks failed) | Halt immediately with error details     |

**Layer 3** halts the stage when any check declared under
`preflight.environment` in the workspace's `.nightgauge/config.yaml` fails
(exit 1) or the declaration is invalid (exit 2). The failure is in the
operator's environment, not the code: do not edit code to work around it, do
not run the remedy yourself, and end the stage. The pipeline records it as
`environment_halt` and holds the issue for an operator.

Do not theorize about the cause beyond what is printed here — the failing
check name (`failed_checks`) and its `.errors[]` message are the complete
diagnosis (#277).


---

#### Step 0.1: Verify Prerequisites

```bash
# Check nightgauge binary installed and forge auth configured
if ! command -v nightgauge &> /dev/null; then
  echo "ERROR: nightgauge binary not installed"
  echo "See: docs/GO_BINARY.md"
  exit 1
fi

if ! nightgauge forge auth status &> /dev/null; then
  echo "ERROR: forge auth not configured"
  echo "Run: nightgauge forge auth login (or set GITHUB_TOKEN env var)"
  exit 1
fi

# Check jq installed
if ! command -v jq &> /dev/null; then
  echo "ERROR: jq not installed"
  echo "Install: brew install jq (macOS) or apt-get install jq (Linux)"
  exit 1
fi
```

#### Step 0.2: Load Configuration

```bash
# Check for .nightgauge/config.yaml
if [ ! -f .nightgauge/config.yaml ]; then
  echo "ERROR: No .nightgauge/config.yaml found"
  echo "Run /nightgauge:init to set up Nightgauge configuration"
  exit 1
fi

# Verify project configuration
PROJECT_NUMBER=$(yq eval '.project.number' .nightgauge/config.yaml 2>/dev/null)
if [ -z "$PROJECT_NUMBER" ]; then
  echo "ERROR: No project.number configured in .nightgauge/config.yaml"
  echo "Add: project.number: <your-project-number>"
  exit 1
fi

echo "✓ Configuration loaded"
echo "  Project: #$PROJECT_NUMBER"
```

#### Step 0.3: Parse Arguments

Parse command-line arguments into variables:

- `MODE` - full/dates-only/status-only/report
- `MILESTONE_FILTER` - Milestone name filter (optional)
- `LABEL_FILTERS` - Array of label patterns (optional)
- `DRY_RUN` - Boolean flag

---

### Phase 1: Mode Selection (If Not Provided)

If no `--mode` argument provided, use AskUserQuestion to select mode:

```json
{
  "questions": [
    {
      "question": "What would you like to sync?",
      "header": "Sync Mode",
      "multiSelect": false,
      "options": [
        {
          "label": "Preview changes (dry-run)",
          "description": "Show what would be synced without applying changes"
        },
        {
          "label": "Full sync (dates + status)",
          "description": "Sync both milestone dates and status labels"
        },
        {
          "label": "Dates only",
          "description": "Sync only Start/Target Date fields from milestones"
        },
        {
          "label": "Status only",
          "description": "Sync only Status field from status labels"
        }
      ]
    }
  ]
}
```

Map user selection to MODE variable.

---

### Phase 2: Filter Selection (Optional)

If user wants to filter issues, use AskUserQuestion:

```json
{
  "questions": [
    {
      "question": "Do you want to filter which issues to sync?",
      "header": "Filter",
      "multiSelect": false,
      "options": [
        {
          "label": "All open issues",
          "description": "Sync all open issues in the repository"
        },
        {
          "label": "By milestone",
          "description": "Sync only issues in a specific milestone"
        },
        {
          "label": "By labels",
          "description": "Sync only issues with specific labels"
        },
        {
          "label": "By milestone and labels",
          "description": "Combine both filters"
        }
      ]
    }
  ]
}
```

If milestone filter selected, fetch available milestones and ask user to choose:

```bash
# Get open milestones
REPO="${NIGHTGAUGE_REPO:-$(nightgauge git repo-slug)}"
nightgauge forge graphql -f query='query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { milestones(first: 100, states: OPEN) { nodes { title } } } }' \
  -f owner="${REPO%%/*}" -f name="${REPO#*/}" | jq -r '.data.repository.milestones.nodes[].title'
```

If label filter selected, ask user for label pattern (can use wildcards or exact
match).

---

### Phase 3: Execute Sync

#### Step 3.1: Sync Issues via Go Binary

Sync each open issue to the project board using the Go binary in a loop. For
full sync, fetch all open issues and call `project sync-status` per issue:

```bash
BINARY="${NIGHTGAUGE_BIN:-}"
[ -n "$BINARY" ] && [ ! -x "$BINARY" ] && BINARY=""
[ -z "$BINARY" ] && BINARY=$(command -v nightgauge 2>/dev/null || echo "")
if [ -z "$BINARY" ] && [ "${NIGHTGAUGE_DEV_REPO_BIN:-}" = "1" ]; then
  for NG_ROOT in "$(git rev-parse --show-toplevel 2>/dev/null)" \
    "$(cd "$(git rev-parse --git-common-dir 2>/dev/null)/.." 2>/dev/null && pwd)"; do
    [ -x "$NG_ROOT/bin/nightgauge" ] || continue
    grep -qx 'module github.com/nightgauge/nightgauge' "$NG_ROOT/go.mod" 2>/dev/null || continue
    NG_ORIGIN="$(git -C "$NG_ROOT" config --get remote.origin.url 2>/dev/null)"
    case "${NG_ORIGIN%.git}" in
      git@github.com:nightgauge/nightgauge | https://github.com/nightgauge/nightgauge | \
        ssh://git@github.com/nightgauge/nightgauge) BINARY="$NG_ROOT/bin/nightgauge"; break ;;
    esac
  done
fi
[ -z "$BINARY" ] && [ -x "$HOME/go/bin/nightgauge" ] && BINARY="$HOME/go/bin/nightgauge"
[ -n "$BINARY" ] && export PATH="$(dirname "$BINARY"):$PATH"

if [ "$DRY_RUN" = "true" ]; then
  echo "  [dry-run] Would bulk-add issues (milestone: ${MILESTONE_FILTER:-any}, labels: ${LABEL_FILTERS[*]:-any})"
  RESULT='{ "total_issues": 0, "synced": 0, "skipped": 0, "errors": 0, "dry_run": true }'
else
  # Build bulk add args
  BULK_ARGS=(--bulk --json)
  [ -n "$MILESTONE_FILTER" ] && BULK_ARGS+=(--milestone "$MILESTONE_FILTER")
  for label in "${LABEL_FILTERS[@]:-}"; do
    [ -n "$label" ] && BULK_ARGS+=(--label "$label")
  done

  BULK_OUTPUT=$("$BINARY" project add "${BULK_ARGS[@]}" 2>/dev/null)
  BULK_EXIT=$?

  SYNCED=$(printf '%s\n' "$BULK_OUTPUT" | jq -r '.added // 0')
  SKIPPED=$(printf '%s\n' "$BULK_OUTPUT" | jq -r '.skipped // 0')
  ERRORS=$(printf '%s\n' "$BULK_OUTPUT" | jq -r '.failed // 0')
  TOTAL=$(printf '%s\n' "$BULK_OUTPUT" | jq -r '.total // 0')

  RESULT=$(jq -n \
    --argjson total "$TOTAL" \
    --argjson synced "$SYNCED" \
    --argjson skipped "$SKIPPED" \
    --argjson errors "$ERRORS" \
    --argjson dry_run false \
    '{ total_issues: $total, synced: $synced, skipped: $skipped, errors: $errors, dry_run: $dry_run }')
fi
```

#### Step 3.2: Parse Hook Output

The hook script outputs JSON:

```json
{
  "total_issues": 42,
  "synced": 35,
  "skipped": 7,
  "errors": 0,
  "dry_run": false,
  "details": [
    {
      "issue": 123,
      "title": "Add user authentication",
      "changes": {
        "start_date": {
          "old": null,
          "new": "2026-02-01"
        },
        "target_date": {
          "old": null,
          "new": "2026-02-15"
        },
        "status": {
          "old": "Backlog",
          "new": "Ready"
        }
      },
      "skipped_reason": null
    },
    {
      "issue": 124,
      "title": "Fix login bug",
      "changes": {},
      "skipped_reason": "No milestone"
    }
  ]
}
```

Parse JSON using jq:

```bash
TOTAL=$(printf '%s\n' "$RESULT" | jq -r '.total_issues')
SYNCED=$(printf '%s\n' "$RESULT" | jq -r '.synced')
SKIPPED=$(printf '%s\n' "$RESULT" | jq -r '.skipped')
ERRORS=$(printf '%s\n' "$RESULT" | jq -r '.errors')
DRY_RUN_MODE=$(printf '%s\n' "$RESULT" | jq -r '.dry_run')
```

#### Step 3.3: Sync Date Fields from Milestones

For issues with milestone due dates, set the `Start date` and `Target date`
project board fields using the Go binary:

```bash
if [ "$MODE" = "full" ] || [ "$MODE" = "dates-only" ]; then
  # Fetch open issues with milestones (first 100; `forge issue list` carries no dueOn)
  REPO="${NIGHTGAUGE_REPO:-$(nightgauge git repo-slug)}"
  ISSUES_WITH_MILESTONES=$(nightgauge forge graphql -f query='query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { issues(first: 100, states: OPEN) { nodes { number milestone { dueOn } } } } }' \
    -f owner="${REPO%%/*}" -f name="${REPO#*/}" 2>/dev/null \
    | jq -r '.data.repository.issues.nodes[] | select(.milestone != null) | "\(.number) \(.milestone.dueOn // empty)"')

  while IFS=' ' read -r issue_number due_date; do
    [ -z "$issue_number" ] || [ -z "$due_date" ] && continue
    # Use milestone due date as target date; start date is optional
    if [ "$DRY_RUN" = "true" ]; then
      echo "  [dry-run] Would set target-date=$due_date on #$issue_number"
    else
      "$BINARY" project set-field "$issue_number" --target-date "$due_date" 2>/dev/null || true
    fi
  done <<< "$ISSUES_WITH_MILESTONES"
fi
```

---

### Phase 4: Display Results

#### Step 4.1: Summary Output

Format summary based on mode:

```
┌─────────────────────────────────────────────────────────────────┐
│  PROJECT SYNC COMPLETE                                          │
└─────────────────────────────────────────────────────────────────┘

Mode: Full Sync (Dates + Status)
Dry Run: No

## Summary
✓ Synced: 35 issues
⊘ Skipped: 7 issues
✗ Errors: 0 issues
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total: 42 issues processed
```

If dry-run mode:

```
┌─────────────────────────────────────────────────────────────────┐
│  PROJECT SYNC PREVIEW (DRY RUN)                                 │
└─────────────────────────────────────────────────────────────────┘

The following changes would be applied:

✓ 35 issues would be synced
⊘ 7 issues would be skipped

Run without --dry-run to apply these changes.
```

#### Step 4.2: Detailed Changes (Top 10)

Show details for up to 10 synced issues:

```markdown
## Recent Changes

### Issue #123: Add user authentication

- Start Date: null → 2026-02-01
- Target Date: null → 2026-02-15
- Status: Backlog → Ready

### Issue #125: Implement photo upload

- Status: Backlog → In progress

### Issue #127: Fix payment processing

- Target Date: 2026-02-10 → 2026-02-20
```

#### Step 4.3: Skipped Issues Summary

Show why issues were skipped:

```markdown
## Skipped Issues

7 issues were skipped:

- 4 issues: No milestone
- 2 issues: Not in project board
- 1 issue: Already up-to-date
```

#### Step 4.4: Error Summary (If Any)

If errors occurred:

```markdown
## Errors

2 issues failed to sync:

- Issue #130: API rate limit exceeded
- Issue #132: Invalid date format in milestone
```

---

### Phase 5: Recommendations

#### Step 5.1: Suggest Next Actions

Based on results, suggest next actions:

```
## Next Steps

✓ Project board is now synchronized

Recommendations:
- Run /nightgauge:issue-pickup to start working on ready issues
- Review skipped issues and add milestones if needed
- Set up automation to keep fields in sync:
  - Add project board sync hooks to .nightgauge/hooks/
```

If errors occurred:

```
## Action Required

Some issues failed to sync. Review errors above and:
1. Check API rate limits: nightgauge forge graphql -f query='query { rateLimit { remaining resetAt } }'
2. Verify milestone dates are valid ISO format
3. Re-run sync after resolving issues
```

If dry-run mode:

```
## Next Steps

Review the changes above. If they look correct:
1. Run the same command without --dry-run
2. Or run: /nightgauge:project-sync --mode full
```

---

### Self-Assessment Epilogue

### Self-Assessment Epilogue

**PURPOSE**: Evaluate whether this skill's instructions matched reality during
this execution. This phase is **non-blocking** — skip entirely if any main phase
failed. A perfectly working skill produces **no output** from this phase.

> See [docs/SKILL_SELF_ASSESSMENT.md](https://github.com/nightgauge/nightgauge/blob/main/docs/SKILL_SELF_ASSESSMENT.md) for
> the full strategy, synthesis algorithm, and integration architecture.

#### When to Skip

- Any prior phase exited with an error before completing
- The skill was cancelled or timed out
- Running in `--dry-run` mode

#### Step 1: Evaluate Execution Friction

Review the execution that just completed. Answer these questions honestly:

1. **Command failures**: Did any command, script, or binary call in the skill
   instructions fail? (e.g., script not found, wrong flags, missing binary)
2. **Workarounds**: Did you have to deviate from the skill instructions to
   accomplish the goal? (e.g., used `gh` directly because a referenced script
   didn't exist, skipped a step that referenced a nonexistent file)
3. **Stale references**: Did any file path, function name, API endpoint, or tool
   referenced in the instructions not exist in the current codebase?
4. **Unclear instructions**: Were any instructions ambiguous enough that you had
   to guess at the intended behavior?
5. **Missing instructions**: Was there a significant step you had to figure out
   on your own that should have been documented in this skill?

**If ALL answers are "no" — write nothing and complete the skill normally.** The
goal is silence when everything works.

#### Step 2: Write Assessment Record

Only if friction was detected in Step 1. Write a single JSON file,
`assessments/{STAGE_NAME}-${ISSUE_NUMBER}.json` in the clone's pipeline state
directory, through the binary (JSON on stdin; never a direct file write):

```bash
ISSUE_NUMBER="${NIGHTGAUGE_ISSUE_NUMBER:-$(git branch --show-current | sed -n 's#^[^/]*/\([0-9]*\)-.*#\1#p')}"
: "${ISSUE_NUMBER:?set NIGHTGAUGE_ISSUE_NUMBER or check out the issue branch}"
nightgauge layout write pipeline "assessments/{STAGE_NAME}-${ISSUE_NUMBER}.json" <<'JSON'
{ ... the record below ... }
JSON
```

The assessment record MUST follow this schema:

```json
{
  "schema_version": "1",
  "skill": "{STAGE_NAME}",
  "skill_file": "skills/nightgauge-{STAGE_NAME}/SKILL.md",
  "issue_number": 42,
  "timestamp": "2026-03-10T14:30:00Z",
  "friction": [
    {
      "type": "command_failure",
      "severity": "high",
      "description": "hooks/lib/add-to-project.sh not found — script was deleted",
      "skill_line_hint": "claude-plugins/nightgauge/hooks/lib/add-to-project.sh <issue-number>",
      "actual_resolution": "Used gh api graphql to add issue to project board directly",
      "suggested_fix": "Replace with: nightgauge project add <issue-number>"
    }
  ]
}
```

**Friction types**: `command_failure`, `workaround`, `stale_reference`,
`unclear_instruction`, `missing_instruction`

**Severity levels**:

- `high` — instruction is **broken**. Required manual workaround to complete.
- `medium` — instruction is **misleading**. Agent adapted without user help.
- `low` — instruction is **suboptimal**. No functional impact.

**Rules**:

- **One record per execution** — multiple friction items go in the `friction`
  array, not separate files.
- **Be specific** — quote the exact instruction text that was wrong. Not "some
  commands didn't work" but "Step 5.2 calls `hooks/lib/add-to-project.sh` but
  this script was deleted in commit 65915701."
- **Suggest the fix** — every finding MUST include `suggested_fix` with the
  concrete SKILL.md change needed. Not "update the docs" but "replace
  `hooks/lib/add-to-project.sh <N>` with `nightgauge project add <N>`."
- **Don't invent friction** — only report issues you actually encountered during
  this execution. Do not speculate about potential problems.

#### Step 3: Validate and Complete

```bash
# Validate JSON if written
ISSUE_NUMBER="${NIGHTGAUGE_ISSUE_NUMBER:-$(git branch --show-current | sed -n 's#^[^/]*/\([0-9]*\)-.*#\1#p')}"
: "${ISSUE_NUMBER:?set NIGHTGAUGE_ISSUE_NUMBER or check out the issue branch}"
ASSESSMENT_FILE="$(nightgauge layout path pipeline "assessments/{STAGE_NAME}-${ISSUE_NUMBER}.json")"
if [ -f "$ASSESSMENT_FILE" ]; then
  python3 -m json.tool "$ASSESSMENT_FILE" > /dev/null 2>&1 || \
    echo "WARNING: Assessment record is not valid JSON" >&2
fi
```


---

## Error Handling

### Common Errors

| Error                     | Cause                             | Solution                                   |
| ------------------------- | --------------------------------- | ------------------------------------------ |
| `gh: command not found`   | GitHub CLI not installed          | Install from https://cli.github.com        |
| `forge auth status` fails | Forge auth not configured         | Run `nightgauge forge auth login`          |
| `403 rate limit`          | Too many API requests             | Wait 1 hour or use authenticated token     |
| `No project configured`   | Missing `.nightgauge/config.yaml` | Run `/nightgauge:init` first               |
| `Missing field IDs`       | Incomplete project config         | Add field IDs to `.nightgauge/config.yaml` |
| `No open issues found`    | Repository has no open issues     | Normal - no sync needed                    |

### Graceful Degradation

The skill handles missing configuration gracefully:

- **No project configured**: Skips sync, reports what's needed
- **No milestones**: Skips date sync, continues with status sync
- **Issue not in project**: Skips that issue, continues with others
- **Missing field IDs**: Skips affected fields, syncs what's possible

---

## Integration

### Standalone Utility

This skill is **NOT** part of the main Nightgauge pipeline. It:

- Does not require pipeline context files
- Can be run at any time
- Does not modify pipeline state
- Does not block or depend on pipeline stages

### Recommended Usage

| Scenario            | When to Run                                |
| ------------------- | ------------------------------------------ |
| Initial onboarding  | After setting up `.nightgauge/config.yaml` |
| Bulk issue creation | After importing issues from another system |
| Milestone changes   | After updating milestone due dates         |
| Label drift         | Weekly/monthly maintenance                 |
| Pre-sprint planning | Before sprint planning to ensure accuracy  |

### Integration with Other Skills

| Skill                       | Relationship                              |
| --------------------------- | ----------------------------------------- |
| `/nightgauge:init`          | Run before project-sync to set up config  |
| `/nightgauge:issue-pickup`  | Run after project-sync to start work      |
| `/nightgauge:backlog-groom` | Complementary - groom then sync           |
| Pipeline skills             | Independent - project-sync doesn't affect |

---

## Performance

### Expected Performance

| Repository Size | Sync Time     | Notes                              |
| --------------- | ------------- | ---------------------------------- |
| 10 issues       | < 10 seconds  | Single page, minimal API calls     |
| 50 issues       | 20-30 seconds | Single page                        |
| 100 issues      | 40-60 seconds | Multiple pages, idempotency checks |
| 500+ issues     | 3-5 minutes   | Heavy pagination, consider filters |

### Optimization Tips

1. **Use filters** - Narrow scope with `--milestone` or `--label`
2. **Incremental sync** - Run periodically instead of full sync
3. **Dates-only or status-only** - Use specific mode if only one field changed
4. **Off-peak hours** - Run during low API usage times if rate-limited

### API Rate Limits

GitHub API rate limits:

- **Authenticated**: 5000 requests/hour
- **GraphQL**: 5000 points/hour (query complexity-based)

This skill uses GraphQL pagination (100 issues/query) for efficiency. A
500-issue sync uses ~10 requests.

---

## Dependencies

- **nightgauge binary** - Required for forge issue fetching
- **jq** - JSON parsing and manipulation
- **yq** (optional) - YAML parsing for config
- **Bash 4+** - Shell scripting

---

## Source

Part of the [Nightgauge](https://github.com/nightgauge/nightgauge) -
AI-Augmented SDLC Framework.

Files in this skill

  • CHANGELOG.md1.9 KB
  • SKILL.md32.8 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.