sync
Error handling convention for ALL bash commands in this skill:
- IF command exits non-zero → capture stderr, print diagnostic, then retry or fallback:
✗ {command} failed: {stderr summary} → Try: {suggested fix} → Fallback: {what the skill does instead} - IF command returns empty stdout when a result is expected → treat as ⚠ warn, note it, proceed with fallback
- Never stop the entire skill for a single command failure — degrade gracefully and skip the affected section
0. Parse Arguments & Validate Environment
Parse mode from $ARGUMENTS:
- IF empty or "full" → MODE=full
- IF "quick" → MODE=quick
- IF "--consolidate" → MODE=consolidate
- IF unrecognized → show usage and stop: "Usage: /product:sync [quick|full|--consolidate]"
Check git repo:
- Run
git rev-parse --is-inside-work-tree 2>&1 - IF exit code non-zero → stop:
"✗ Not inside a git repo. Run
git initor cd into your project." - Run
git remote -v 2>/dev/null→ store HAS_REMOTE (true if any remote configured)
Check Linear CLI:
- Run
linear auth whoami 2>&1 - IF command not found → set LINEAR_AVAILABLE=false, warn:
✗ linear CLI not found → Try: Install from https://github.com/schpet/linear-cli → Fallback: Running git-only sync (Linear checks will be skipped) - ELIF output contains "not authenticated" or "unauthorized" or exit code non-zero → set LINEAR_AVAILABLE=false, warn:
✗ linear CLI not authenticated → Try: Run `linear auth` to log in → Fallback: Running git-only sync (Linear checks will be skipped) - ELSE → LINEAR_AVAILABLE=true
Read project scope:
- IF
.linear-projectexists:- Read file and trim whitespace/newlines:
PROJECT=$(cat .linear-project | tr -d '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') - IF trimmed value is empty → warn: "
.linear-projectexists but is empty. Continuing unscoped." - ELIF LINEAR_AVAILABLE:
- Validate project exists:
linear project list 2>&1 | grep -qi "$PROJECT" - IF not found → warn:
Set PROJECT="" (clear it)⚠ Project '{PROJECT}' not found in Linear. It may have been renamed or archived. → Try: Update `.linear-project` or run `/product:init-project` to relink → Fallback: Continuing unscoped (showing all projects) - ELSE → note: "Scoped to Linear project: {PROJECT}"
- Validate project exists:
- Build PROJECT_FLAG:
${PROJECT:+--project '$PROJECT'}
- Read file and trim whitespace/newlines:
- IF
.linear-projectmissing → warn: "No.linear-projectfound — showing issues from all projects. Run/product:init-projectto link one."
Print header:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PRODUCT ► SYNC [{PROJECT or "All Projects"}] ({MODE})
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
IF MODE=consolidate → skip to Step 3.
1. Gather State
Collect data from all available sources. Run independent commands in parallel where possible.
From Linear (skip entirely if LINEAR_AVAILABLE=false):
linear issue list -s started --limit 20 $PROJECT_FLAG 2>&1→ in-progress issues- IF fails → warn and set STARTED_ISSUES=empty
linear issue list -s unstarted --limit 20 $PROJECT_FLAG 2>&1→ backlog- IF fails → warn and set BACKLOG_ISSUES=empty
linear issue list -s completed --limit 10 $PROJECT_FLAG 2>&1→ recently completed- IF fails → warn and set COMPLETED_ISSUES=empty
linear issue list -s cancelled --limit 10 $PROJECT_FLAG 2>&1→ recently cancelled (for orphan detection)- IF fails → warn and set CANCELLED_ISSUES=empty
From Git:
git log --oneline -30 2>&1→ recent commits- IF repo has no commits → note "Empty repo, no commits yet" and skip git-based gap detection
git log --oneline --first-parent main -30 2>/dev/null || git log --oneline --first-parent master -30 2>/dev/null→ commits on default branch only (for squash-merge detection)- IF fails (no main/master) → skip squash-merge detection, note which default branch was tried
git branch --list 2>&1→ local branchesgit branch -r --merged main 2>/dev/null || git branch -r --merged master 2>/dev/null→ remote branches merged into default (only if HAS_REMOTE)git reflog --since="7 days ago" --no-decorate 2>/dev/null | head -50→ detect force-pushes/rebases- Current branch name:
git branch --show-current 2>&1
From GSD (if .planning/ exists):
- Read
.planning/STATE.mdfor current phase context - Read
.planning/ROADMAP.mdfor phase list and completion status - Read
.planning/PROJECT.mdfor project objectives - IF any file missing → skip silently (GSD is optional)
2. Detect Gaps
IF LINEAR_AVAILABLE=false → only detect git-only gaps (working tree, stale branches). Skip all Linear-dependent categories.
Compare sources and classify each finding into exactly one bucket:
| Status | Definition |
|---|---|
| Synced | Linear status matches git state (e.g., in-progress + recent commits on branch) |
| Stale in Linear | Issue is "In Progress" but no commits referencing it in 48h+ |
| Untracked work | Commits reference an issue ID but Linear still shows "Unstarted" |
| Done but open | Branch merged or squash-merged into default branch, but Linear issue not moved to Done |
| Orphan branch | Local branch references a Linear issue that is cancelled, done, or doesn't exist |
| Missing branch | Linear issue is "In Progress" but no matching local branch exists |
| Squash-merged | Issue ID appears in default branch's first-parent commit messages, but Linear status is still "Started" — detected via git log --first-parent |
| Rebased/force-pushed | Branch reflog shows rebase or force-push entries for a branch mapped to an active issue — informational, flagged as ⚠ |
| Direct-to-main | Commits on default branch reference an issue ID (e.g., ENG-\d+ pattern) but no feature branch exists for that issue |
| Unassigned in-progress | Linear issue is "In Progress" but has no assignee |
Detection logic:
-
Synced: For each in-progress Linear issue, check if a local branch contains the issue ID (e.g.,
eng-123in branch name) AND has commits within the last 48h referencing it. -
Stale in Linear: In-progress issues where
git log --all --oneline --since="48 hours ago" | grep -i "$ISSUE_ID"returns empty. -
Untracked work:
git log --oneline -30 | grep -oiE '[A-Z]+-[0-9]+'extracts issue IDs from recent commits, cross-reference against Linear unstarted list. -
Done but open: Check
git branch --merged main(or master) for branches containing issue IDs that are still "Started" in Linear. Also check first-parent log for squash-merge references. -
Orphan branch: Extract issue IDs from branch names (
git branch --list | grep -oiE '[a-z]+-[0-9]+'), check against cancelled/done issues list. -
Missing branch: In-progress Linear issues where no local branch contains the issue ID.
-
Squash-merged: Issue IDs found in
git log --first-parent main --onelinethat are still "Started" in Linear but have no open feature branch. -
Rebased/force-pushed: Parse
git reflogfor lines containing "rebase" or "reset" on branches that match active issue IDs. Flag as informational. -
Direct-to-main: Issue IDs in default branch commits that have no corresponding branch in
git branch --list. -
Unassigned in-progress: From Linear data, check started issues for missing assignee field (parse
linear issue listoutput).
3. Consolidate Objectives
IF MODE=quick → skip this step.
Compare three sources of truth for project-level alignment:
Source 1 — Linear project (if LINEAR_AVAILABLE and PROJECT is set):
- Run
linear project view "$PROJECT" 2>&1→ extract milestones, description, target dates - IF command fails or project not set → note "Linear project metadata unavailable" and skip this source
Source 2 — GSD planning (if .planning/ exists):
- Read
.planning/ROADMAP.md→ extract phase names and completion status - Read
.planning/PROJECT.md→ extract stated objectives and timeline - IF files missing → note "GSD planning state unavailable" and skip this source
Source 3 — CLAUDE.md (if exists at repo root):
- Read
CLAUDE.md→ extract any goals, objectives, or project description sections - IF missing → skip this source
Cross-reference:
- Does every Linear milestone have a corresponding GSD phase?
- Does CLAUDE.md mention goals not tracked in Linear or GSD?
- Are there GSD phases with no matching Linear milestone (future work is okay)?
Present alignment table:
Objective Alignment:
─────────────────────
✓ "Auth system" — Linear milestone ↔ GSD Phase 3 ↔ CLAUDE.md
⚠ "API redesign" — Linear milestone, no GSD phase planned
⚠ "Performance audit" — In CLAUDE.md goals, not in Linear
○ "Onboarding flow" — GSD Phase 5, no Linear milestone (future work)
IF misalignments found AND MODE != quick: Ask: "How do you want to resolve these misalignments?"
- Create Linear issues for untracked objectives
- Update CLAUDE.md to match current Linear state
- Note them and move on (address later)
- Skip — alignment is fine as-is
IF only one source is available → note: "Only one source of truth available — consolidation needs at least two of: Linear project, GSD planning, CLAUDE.md"
4. Present Summary
Print a formatted sync report:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SYNC REPORT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Synced (N):
✓ ENG-123 — Feature description
Gaps Found (N):
⚠ ENG-456 — Stale: in-progress but no commits in 3 days
⚠ ENG-789 — Untracked: commits exist but Linear shows unstarted
⚠ ENG-012 — Done but open: branch merged, issue still in-progress
⚠ ENG-345 — Squash-merged: appears on main, still marked Started
○ ENG-678 — Direct-to-main: commits on main, no feature branch
○ ENG-901 — Unassigned: in-progress but no assignee
○ eng-999-old-feature — Rebased: force-push detected (informational)
Backlog (N):
○ ENG-234 — Urgent — Unstarted
○ ENG-567 — High — Unstarted
{IF consolidation ran:}
Objective Alignment:
✓ N aligned ⚠ N misaligned ○ N untracked
IF MODE=quick → print summary and skip to Step 6 (route only, no interactive resolution).
5. Resolve Gaps (Interactive)
IF zero gaps found → skip to Step 6.
Walk through each gap and ask what to do:
For "Stale in Linear": Ask: "ENG-456 has been in-progress for N days with no commits. What happened?"
- Still working on it → keep as-is
- Blocked → add a blocker comment to Linear:
linear issue comment add $ID -b "Blocked: {reason}" 2>&1 - Abandoned → move back to backlog:
linear issue update $ID -s "Triage" 2>&1 - Actually done → move to Done:
linear issue update $ID -s "Done" 2>&1
For "Untracked work": Ask: "Found commits referencing ENG-789 but it's still Unstarted in Linear. Update it?"
- Yes, move to In Progress:
linear issue update $ID -s "In Progress" 2>&1 - Yes, it's actually Done:
linear issue update $ID -s "Done" 2>&1 - No, those commits were reverted → skip
For "Done but open": Ask: "ENG-012 looks complete (branch merged). Close it in Linear?"
- Yes, move to Done with auto-generated summary:
linear issue update $ID -s "Done" 2>&1 - No, there's still remaining work → skip
For "Orphan branch":
Ask: "Branch eng-999-old-feature references a cancelled/done issue. Clean up?"
- Delete the branch:
git branch -d $BRANCH 2>&1(safe delete; if fails, note it may have unmerged work and ask about-D) - Keep it → skip
For "Missing branch": Ask: "ENG-345 is In Progress but has no local branch. What happened?"
- Create one now →
git checkout -b $BRANCH_NAME 2>&1(generate slug from issue title) - It's on a different machine → skip
- It's done, just not tracked → move to Done
For "Squash-merged": Ask: "ENG-345 appears squash-merged into main but is still In Progress. Close it?"
- Yes, move to Done with merge reference
- No, there's still remaining work on another branch
For "Direct-to-main": Ask: "Found commits on main referencing ENG-678 but no feature branch was used. Track this?"
- Move to Done (work is on main)
- Ignore (was a hotfix, already tracked)
For "Unassigned in-progress": Ask: "ENG-901 is In Progress but has no assignee. Assign to you?"
- Yes, assign to me:
linear issue update $ID --assignee me 2>&1 - Assign to someone else → prompt for name
- Move back to backlog:
linear issue update $ID -s "Triage" 2>&1
For "Rebased/force-pushed": (informational only) Note: "Branch for ENG-XXX was rebased/force-pushed recently. Commit references in Linear comments may be stale."
- No action needed unless user asks
For ALL Linear update commands: check exit code. If update fails, print:
✗ Failed to update ENG-XXX: {stderr}
→ Try: Run the command manually: linear issue update ...
6. Cleanup & Route
Cleanup (ask before each action)
Stale branch cleanup:
- List branches from "Orphan branch" gaps that user agreed to delete
- Also check for any local branches already merged into default branch:
git branch --merged main 2>/dev/null | grep -v 'main\|master\|\*' - Ask: "Found N merged/orphan branches. Delete them all, pick which to delete, or skip?"
Suggest issue consolidation:
- From the gathered Linear data, check for issues with very similar titles (compare words, look for near-duplicates)
- IF potential duplicates found → "These issues might be duplicates:"
"Merge them in Linear? (This just flags them — you'll need to merge manually in Linear)"⚠ ENG-111 "Login page redesign" ↔ ENG-222 "Redesign login page" - IF none → skip silently
Prune remote refs (only if HAS_REMOTE):
- Check for stale remote tracking branches:
git remote prune origin --dry-run 2>&1 - IF stale refs found → ask: "Found N stale remote refs. Prune them?"
- Yes →
git remote prune origin 2>&1 - No → skip
- Yes →
Route to Next Action
Based on the final state after resolving gaps:
- IF consolidation found misalignment → suggest
/product:updatefor a full project status review - IF there are unstarted high/urgent issues → suggest
/product:start-task - IF there's an active in-progress issue with no branch → suggest creating one
- IF GSD state exists with an incomplete phase → suggest
/gsd:progress - IF many gaps were found and resolved → suggest running
/product:syncagain to verify, or/product:maintainfor a deeper health check - IF everything is synced and nothing urgent → "All clear. Pick up the next task when ready."
<success_criteria>
- Environment validated with explicit error messages for every failure mode
- Linear CLI auth checked (not just existence) — graceful fallback if unavailable
-
.linear-projecttrimmed, validated against Linear, special chars handled - All 10 gap categories detected (or skipped gracefully with reason)
- Squash-merged and direct-to-main work correctly identified
- Objectives consolidated across Linear, GSD, and CLAUDE.md (in full/consolidate mode)
- Every gap surfaced and user made an explicit decision
- All Linear update commands have exit code checking
- Cleanup offered: stale branches, duplicate issues, remote ref pruning
- Clear next action suggested based on final state </success_criteria>
More from seangjr/product-skills
daily-standup
Morning standup — surface blockers, stale work, and today's focus
8maintain
Codebase health check — type errors, lint, git hygiene, deps, Linear, build, and auto-fix
7start-task
Pick up the next Linear issue — validate context, create branch, plan approach
7init-project
Scaffold a new project with Next.js, GSD, skills, Linear integration, and MCP config
7finish-task
Wrap up current task — commit, update Linear, optionally create PR, route to next
7update
Project update & risk report — gather state, surface risks, generate updates for stakeholders
6