<objective>
Reconcile the codebase state with Linear. Detect mismatches between git history and issue statuses
across 10 gap categories. Consolidate project-level objectives across Linear, GSD, and CLAUDE.md.
Surface gaps and ask the user how to resolve each one. Clean up stale branches, duplicate issues,
and orphaned refs. End by routing to the most impactful next action.
</objective>
<context>
Input: $ARGUMENTS — mode selector
- (empty) or "full" → all gap categories + consolidation + interactive resolution (default)
- "quick" → report only, no interactive resolution or consolidation
- "--consolidate" → only run the objectives alignment step
Requires: git repo, `linear` CLI authenticated
Optional: .planning/ directory (GSD state), CLAUDE.md, .linear-project
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
</context>
<process>
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 or cd into your project."
- Run
git remote -v 2>/dev/null
→ store HAS_REMOTE (true if any remote configured)
Check Linear CLI:
- Run
- 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 exists:
- Read file and trim whitespace/newlines:
PROJECT=$(cat .linear-project | tr -d '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
- IF trimmed value is empty → warn: " exists but is empty. Continuing unscoped."
- ELIF LINEAR_AVAILABLE:
- Validate project exists:
linear project list 2>&1 | grep -qi "$PROJECT"
- IF not found → warn:
⚠ 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)
Set PROJECT="" (clear it)
- ELSE → note: "Scoped to Linear project: {PROJECT}"
- Build PROJECT_FLAG:
${PROJECT:+--project '$PROJECT'}
- IF missing → warn:
"No found — showing issues from all projects. Run to 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
- → local branches
git 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 for current phase context
- Read for phase list and completion status
- Read for 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 |
| 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., 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.,
in 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
(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 --oneline
that are still "Started" in Linear but have no open feature branch.
-
Rebased/force-pushed: Parse
for 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
.
-
Unassigned in-progress: From Linear data, check started issues for missing assignee field (parse
output).
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 → extract phase names and completion status
- Read → 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 → 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
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 )
- 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:"
⚠ ENG-111 "Login page redesign" ↔ ENG-222 "Redesign login page"
"Merge them in Linear? (This just flags them — you'll need to merge manually in Linear)"
- 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
Route to Next Action
Based on the final state after resolving gaps:
- IF consolidation found misalignment → suggest for a full project status review
- IF there are unstarted high/urgent issues → suggest
- IF there's an active in-progress issue with no branch → suggest creating one
- IF GSD state exists with an incomplete phase → suggest
- IF many gaps were found and resolved → suggest running again to verify, or for a deeper health check
- IF everything is synced and nothing urgent → "All clear. Pick up the next task when ready."
</process>
<success_criteria>