원클릭으로
sync-plan
Cycle-aware bidirectional sync between plan.md and Linear. Discovers all cycle tickets, detects drift, updates markers.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Cycle-aware bidirectional sync between plan.md and Linear. Discovers all cycle tickets, detects drift, updates markers.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Force a full-file rewrite (Write) instead of patch (Edit) when Edit keeps failing or diff is large. Context rot mitigation — use when Edit has failed ≥2× on same file, or when estimated diff >30% of file size.
STOA PR Guardian — advisory three-axis review (ADR compliance, security, AI code smell) with binary GO/NO-GO verdict + confidence. Never approves, never blocks merge.
Analyze cycle capacity gap and propose backlog items to fill the sprint to ~80% of proven velocity.
Visual roadmap snapshot — theme progress, milestones, velocity. Zero new infra (Linear MCP + velocity.json).
Debug GitHub Actions CI pipeline failures. Analyse les logs de workflow, identifie la cause racine et propose un fix.
Run 8-persona Council validation on a feature/ADR, then auto-create a Linear ticket if score >= 8/10.
| name | sync-plan |
| description | Cycle-aware bidirectional sync between plan.md and Linear. Discovers all cycle tickets, detects drift, updates markers. |
| argument-hint | [CAB-XXXX | --push | --cycles | --velocity | empty for full sync] |
Synchronize plan.md with Linear ticket statuses using cycle-driven discovery.
Target: $ARGUMENTS
| Argument | Mode | Description |
|---|---|---|
| (empty) | Full sync | Fetch current + next cycle, detect all drift |
CAB-XXXX | Single ticket | Fetch one ticket, update plan.md marker |
--push | Push to Linear | Push plan.md markers → Linear statuses |
--cycles | Cycle discovery | Show current + next cycle summary only |
--velocity | Velocity report | Show velocity history table + trend + capacity forecast |
Always start by discovering the active cycles:
linear.list_cycles(teamId: "<LINEAR_TEAM_ID>")
Identify:
type: "current" or dates containing todaytype: "upcoming" or starts after current endsExtract: cycle ID, name, start date, end date, scope (total issues), completed count.
For each active cycle (current + next):
linear.list_issues(
team: "<LINEAR_TEAM_ID>",
cycle: "<cycle_id>",
first: 50
)
For each issue, extract: identifier, title, status, priority (value + name), estimate (value), cycleId, completedAt.
Important: The status field is a string ("Done", "In Progress", "Todo", "Backlog", "Canceled", "Duplicate"), NOT a nested object.
Priority mapping: priority is {value: N, name: "..."} where 1=Urgent, 2=High, 3=Normal, 4=Low.
Estimate mapping: estimate is {value: N, name: "..."} where N is fibonacci points (1,2,3,5,8,13,21,34,55).
Read plan.md and extract all CAB-XXXX references with their current markers:
| Marker | Meaning |
|---|---|
- [x] | Done locally |
- [~] | Partially done |
- [ ] | Not started or pending |
Extract ticket IDs using pattern: CAB-\d{3,4}
Also identify which cycle section each ticket is in (e.g., "Cycle 7 — CURRENT" vs "Cycle 8 — NEXT").
| plan.md | Linear | Drift? | Action |
|---|---|---|---|
[x] | Done | No | Skip |
[x] | In Progress | Yes | Warning: plan.md says done but Linear disagrees |
[ ] | Done | Yes | Update plan.md → [x] |
[ ] | In Progress | Yes | Update plan.md → [~] |
[~] | Done | Yes | Update plan.md → [x] |
[~] | Todo | Yes | Update Linear → In Progress |
| any | Canceled/Duplicate | Yes | Strikethrough in plan.md |
This is the most critical category — the root cause of past drift.
For each issue in the cycle that has NO matching CAB-XXXX in plan.md:
CAB-XXXX: NOT IN PLAN (Linear: <status>, <points> pts, <priority>)For each file in .claude/claims/*.json:
For each phase with owner != null:
1. Check claimed_at > 2 hours ago
2. Grep operations.log for SESSION-START with matching instance ID in last 2h
3. Check PID alive: kill -0 $PID (same machine only)
4. If ALL checks fail → STALE
5. Report: CAB-XXXX Phase N: STALE CLAIM (claimed by <instance> at <time>, no activity)
6. Propose: auto-release (clear owner, claimed_at, pid in claim file)
7. Log: RELEASE | task=<ID> phase=<N> instance=<old_owner> reason=stale
For standalone claims (.claude/claims/<CAB-XXXX>.json):
Same 3 checks. If stale → report + propose auto-release.
See phase-ownership.md for full stale detection protocol.
For each CAB-XXXX in plan.md that isn't in current or next cycle:
cycleId changed--push)[x] in plan.md → linear.update_issue(status="Done")[~] in plan.md → linear.update_issue(status="In Progress")[ ] with priority change → linear.update_issue(priority=N)plan.md follows this exact structure:
# Sprint Plan — STOA Platform
> Auto-synced with Linear via `/sync-plan`. Source of truth: Linear cycles.
> Last sync: YYYY-MM-DD
## Cycle N (dates) — CURRENT
**Scope**: X pts | **Done**: Y pts | **Velocity**: Z issues closed
**Theme**: <from cycle description or memory.md>
### In Progress
- [~] CAB-XXXX: Title (N pts, PN)
- ✅ completed sub-item
- [ ] remaining sub-item
### Todo
- [ ] CAB-XXXX: Title (N pts, PN)
### Done (N issues)
- [x] CAB-XXXX: Title (N pts) — PR #N
---
## Cycle N+1 (dates) — NEXT
**Theme**: <description>
### Todo
- [ ] CAB-XXXX: Title (N pts, PN)
### Backlog (parked in cycle, not committed)
- CAB-XXXX: Title (PN)
---
## Milestones
(preserved from existing plan.md)
## KPIs Demo
(preserved from existing plan.md)
## Regles
1. **Linear is source of truth** — plan.md is a view, not the master
2-N. (preserved from existing plan.md)
Preservation rules:
[~] tickets (they contain PR references and manual progress)[owner: X]) from plan.md during regenerationcompleted_at != null) → update plan.md marker to [x]- [ ] CAB-XXXX: <title> (<estimate> pts, P<priority>)— PR #N suffixCalculate and display:
Read .claude/state/velocity.json once.
Compute the set of cycles that should be recorded:
last_recorded = max id in cycles[]previous_cycle = number of the most recent closed cycle (from list_cycles(type: "previous"))(last_recorded, previous_cycle] that has no entry in cycles[]completion_pct == 100 OR today > end_date (the original close trigger)For each cycle in the missing set + current-if-closed, in ascending order:
a. Fetch its issues:
linear.list_issues(team: "<LINEAR_TEAM_ID>", cycle: "<N>", limit: 250)
b. Build the entry (same schema as before):
{
"id": <cycle_number>,
"name": "Cycle <N>",
"start": "<startsAt date>",
"end": "<endsAt date>",
"days": <duration_days>,
"points_committed": <scope; prefer Linear's scopeHistory[-1] if it includes sub-issues>,
"points_done": <sum of estimate.value for status=="Done"; cross-check with completedScopeHistory[-1]>,
"issues_total": <total>,
"issues_done": <done count>,
"prs_merged": <gh pr list --search "merged:<start>..<end>" --json number | jq length, else null>,
"pts_per_day": <done_pts / days>,
"completion_pct": <done_pts / scope * 100>,
"note": "<set if scopeHistory grew >1.2x mid-cycle, else omit>"
}
c. Append to cycles[] only if id not already present (idempotent).
After processing the missing set, recompute rolling_avg_3 from the last 3 entries (averages of pts_per_day, issues_done/days, completion_pct). Update last_synced to today. Write the file in a single atomic operation.
Rules:
prs_merged is best-effort; null is acceptablescopeHistory[-1] / completedScopeHistory[-1] over list_issues sums when both are availableIf mode is --velocity, read velocity.json and display:
Velocity History — STOA Platform
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
| Cycle | Dates | Pts | Done | Velocity | Completion |
|-------|-------|-----|------|----------|------------|
| C7 | Feb 9-15 | 505 | 505 | 72.1/day | 100% |
| C8 | Feb 16-22 | 86 | 5 | ... | 6% |
Rolling 3-cycle avg: XX.X pts/day
Target utilization: 80%
Capacity forecast (next cycle, 7 days): XXX pts target
Trend: [↑|↓|→] <description>
Then exit (no sync needed).
Sync Results (YYYY-MM-DD):
━━━━━━━━━━━━━━━━━━━━━━━━━
Cycle N (current): X/Y pts done (Z%)
Cycle N+1 (next): A issues planned
Drift detected: N items
CAB-XXXX: [ ] → [x] (Linear: Done)
CAB-YYYY: [~] → [x] (Linear: Done)
Missing from plan.md: N items
CAB-ZZZZ: Added (Linear: Todo, 8 pts, P2)
CAB-WWWW: Added (Linear: In Progress, 3 pts, P1)
Orphans (in plan, not in cycle): N items
CAB-VVVV: In Done section (OK — historical)
Stale Claims: N
CAB-XXXX Phase 2: claimed by t4821 at 14:00, no activity (auto-released)
CAB-YYYY: claimed by t9012 at 13:30, PID dead (auto-released)
Updates applied: N (Linear → plan.md)
Updates pushed: N (plan.md → Linear) [only if --push]
plan.md updated. Review changes with `git diff plan.md`.
After every sync (all modes except --velocity), compute utilization:
.claude/state/velocity.jsoncapacity = rolling_avg_3.pts_per_day × cycle_daystarget = capacity × target_utilizationutilization = pts_committed / target × 100If utilization < 70%:
⚠️ Cycle under-loaded: XXX/YYY pts (ZZ% of target capacity)
Run `/fill-cycle` to analyze backlog and propose fill plan.
If utilization 70-100%:
✅ Cycle healthy: XXX/YYY pts (ZZ% of target capacity)
If utilization > 100%:
🔴 Cycle over-loaded: XXX/YYY pts (ZZ% of target capacity)
Consider deferring lower-priority items to next cycle.
[~] tickets are sacredMCP-CALL | service=linear action=sync-plan tickets=N cycles=current+nextget_issue calls per sync (use list_issues batch)> Last sync: line in plan.md header