diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index bd35aa9..7f349f7 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,6 +8,12 @@ "pluginRoot": "." }, "plugins": [ + { + "name": "todo", + "source": "./commands/todo", + "description": "0.1.0 (2026-06-07) /todo — cross-domain TODO + multi-track launcher. Reads the repo's `DOMAINS.tape` roster + each domain snapshot, ranks domains by OPEN milestone count (`- [ ]`), and renders a numbered dashboard (★ active · 5-cell progress bar · open count · @goal snippet, sorted open-desc). Select a subset to drive in parallel: `/todo …` picks by number; `/todo track` auto-picks the top-N most-open domains as N tracks (e.g. `/todo 2 track` → the two domains with the most open work). The pick PERSISTS a track set under `~/.sidecar/todo/.tracks` and prints a per-track launch plan (`track i: → /domain set && /cycle-bg`); the MODEL then fans out ONE background Agent per track — each runs `/domain set ` then autonomously drains that domain's open milestones (isolation worktree · one `/pr-cycle` per logical unit), the same command-prints-plan → model-fans-out contract as `/cycle`. Verbs — bare/`ls`/`list` (dashboard) · `` or ` track` (select + plan) · `go`/`plan` (re-print the saved plan) · `status` (show track set) · `clear` (drop it). Pairs with the `domain` plugin (roster + snapshots are its SSOT) but is strictly READ-ONLY over the repo — it never edits a domain doc / verdict / log; the only state written is the track set. Implemented in bash (`bin/_todo.sh`); the `/domain todo ` log verb is unrelated (this is a separate launcher plugin).", + "version": "0.1.0" + }, { "name": "commons", "source": "./hooks/commons", diff --git a/CHANGELOG.md b/CHANGELOG.md index 9008f48..d526e15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,21 @@ For the full audit trail, see `git log`. --- +## 2026-06-07 — 📋 todo 0.1.0 — 크로스도메인 TODO + 멀티트랙 런처 (신규) + +28개 도메인을 한눈에 보고 N개를 골라 병렬로 굴리는 런처. `DOMAINS.tape` 로스터 + +각 스냅샷을 읽어 **열린 마일스톤(`- [ ]`) 많은 순**으로 번호 매긴 대시보드를 그림. + +- 📋 **대시보드** — `/todo` = `★ 활성 · 진행바 · open 수 · @goal` 표 (open desc 정렬). +- 🚦 **선택 → 멀티트랙** — `/todo …` 번호 선택 · `/todo track` = open 최다 + top-N 자동 선택 (예 `/todo 2 track`). 선택은 트랙셋(`~/.sidecar/todo/.tracks`)에 + 저장되고, 트랙별 런치플랜(`track i: → /domain set && /cycle-bg`)을 출력. +- 🤖 **모델 팬아웃 계약** — /cycle 과 동일: 커맨드는 플랜만 찍고, 모델이 트랙당 + background Agent 1개씩 팬아웃(도메인별 자율 드레인 · isolation worktree · track별 /pr-cycle). +- 🔒 **읽기 전용** — 도메인 doc/verdict/log 절대 미수정. 쓰는 상태는 트랙셋 파일 1개뿐. +- 검증: 28도메인 대시보드 렌더 · `2 track` top-2 선택 · go 재출력 · clear · sh 문법 · + g22 lockstep(plugin 0.1.0 ↔ marketplace 0.1.0). `/domain todo ` 로그 verb 와는 무관. + ## 2026-06-07 — 🧹 pr-cycle 0.5.0 — 머지 후 worktree sweep (누적 누수 차단) `/cycle-bg` 를 돌릴수록 `.claude/worktrees/agent-*` worktree 디렉터리가 계속 쌓이던 diff --git a/commands/todo/.claude-plugin/plugin.json b/commands/todo/.claude-plugin/plugin.json new file mode 100644 index 0000000..8d28e7e --- /dev/null +++ b/commands/todo/.claude-plugin/plugin.json @@ -0,0 +1,9 @@ +{ + "name": "todo", + "description": "0.1.0 (2026-06-07) /todo — cross-domain TODO + multi-track launcher. Reads the repo's `DOMAINS.tape` roster + each domain snapshot, ranks domains by OPEN milestone count (`- [ ]`), and renders a numbered dashboard (★ active · 5-cell progress bar · open count · @goal snippet, sorted open-desc). Select a subset to drive in parallel: `/todo …` picks by number; `/todo track` auto-picks the top-N most-open domains as N tracks (e.g. `/todo 2 track` → the two domains with the most open work). The pick PERSISTS a track set under `~/.sidecar/todo/.tracks` and prints a per-track launch plan (`track i: → /domain set && /cycle-bg`); the MODEL then fans out ONE background Agent per track — each runs `/domain set ` then autonomously drains that domain's open milestones (isolation worktree · one `/pr-cycle` per logical unit), the same command-prints-plan → model-fans-out contract as `/cycle`. Verbs — bare/`ls`/`list` (dashboard) · `` or ` track` (select + plan) · `go`/`plan` (re-print the saved plan) · `status` (show track set) · `clear` (drop it). Pairs with the `domain` plugin (roster + snapshots are its SSOT) but is strictly READ-ONLY over the repo — it never edits a domain doc / verdict / log; the only state written is the track set. Implemented in bash (`bin/_todo.sh`); the `/domain todo ` log verb is unrelated (this is a separate launcher plugin).", + "version": "0.1.0", + "author": { "name": "dancinlab" }, + "repository": "https://github.com/dancinlab/sidecar", + "license": "MIT", + "keywords": ["claude-code", "command", "todo", "domain", "multi-track", "launcher", "orchestration"] +} diff --git a/commands/todo/bin/_todo.sh b/commands/todo/bin/_todo.sh new file mode 100644 index 0000000..3a86705 --- /dev/null +++ b/commands/todo/bin/_todo.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# _todo — cross-domain TODO + multi-track launcher for hexa-codex-style repos. +# Reads the DOMAINS.tape roster + each domain snapshot, ranks domains by OPEN +# milestone count, lets the user pick a subset (by number, or top-N via +# ` track`), persists the track set, and prints a per-track launch plan for +# the model to fan out (one background Agent per track, /domain set + drain). +# Read-only over the repo; the only state it writes is the track set under +# $TODO_HOME. No domain doc / verdict / log is ever modified (advisory only). +set -uo pipefail + +ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || { echo "✗ /todo: not a git repo"; exit 0; } +ROSTER="$ROOT/DOMAINS.tape" +STATE_DIR="${TODO_HOME:-$HOME/.sidecar/todo}" +mkdir -p "$STATE_DIR" +KEY="$(printf '%s' "$ROOT" | cksum | cut -d' ' -f1)" +TRACKS="$STATE_DIR/$KEY.tracks" + +# Best-effort active domain for (root) from the domain plugin's store (cosmetic ★). +_active() { + local tsv="$HOME/.claude/domain-active.tsv" + [ -f "$tsv" ] || return 0 + awk -F'\t' -v r="$ROOT" '$1==r{n=$NF} END{print n}' "$tsv" 2>/dev/null +} + +# Emit one row per domain: NAME|open|done|pct|goal — sorted by OPEN desc, then NAME. +_rows() { + [ -f "$ROSTER" ] || return 0 + grep -E '^@domain ' "$ROSTER" \ + | sed -E 's/^@domain +([A-Za-z0-9_+-]+) *:= *"([^"]+)".*/\1\t\2/' \ + | while IFS="$(printf '\t')" read -r name path; do + local snap="$ROOT/${path#./}" + [ -f "$snap" ] || continue + local open done tot pct goal + open=$(grep -cE '^- \[ \]' "$snap" 2>/dev/null); open=${open:-0} + done=$(grep -ciE '^- \[x\]' "$snap" 2>/dev/null); done=${done:-0} + tot=$((open + done)); pct=0; [ "$tot" -gt 0 ] && pct=$((done * 100 / tot)) + goal=$(grep -m1 -E '^@goal:' "$snap" 2>/dev/null | sed -E 's/^@goal:[*[:space:]]*//; s/\*\*//g' | cut -c1-44) + printf '%s|%s|%s|%s|%s\n' "$name" "$open" "$done" "$pct" "$goal" + done | sort -t'|' -k2,2nr -k1,1 +} + +_bar() { # pct -> 5-cell ▓/░ bar + local p=$1 i s="" f + f=$(((p * 5 + 50) / 100)) + for i in 1 2 3 4 5; do [ "$i" -le "$f" ] && s="${s}▓" || s="${s}░"; done + printf '%s' "$s" +} + +dashboard() { + local active; active="$(_active)" + echo "📋 /todo — cross-domain TODO (도메인별 열린 작업 · open 많은 순)" + echo " # 도메인 진행 open 목표" + local n=0 + while IFS='|' read -r name open done pct goal; do + [ -z "$name" ] && continue + n=$((n + 1)) + local star=" "; [ "$name" = "$active" ] && star="★" + printf ' %s%2d %-17s %s %3d%% %4d %s\n' "$star" "$n" "$name" "$(_bar "$pct")" "$pct" "$open" "$goal" + done < <(_rows) + [ "$n" = 0 ] && echo " (DOMAINS.tape 없음 또는 도메인 0개)" + if [ -s "$TRACKS" ]; then + echo " ── 현재 트랙셋 ──"; sed 's/^/ 🚦 /' "$TRACKS" + fi + echo " 선택: /todo <번호…> · 자동 top-N: /todo track · 실행계획: /todo go · 해제: /todo clear" +} + +plan() { + [ -s "$TRACKS" ] || { echo "트랙셋이 비어있음 — 먼저 /todo <번호…> 또는 /todo track"; return 0; } + local rows; rows="$(_rows)" + local n; n=$(grep -c . "$TRACKS") + echo "🚦 멀티트랙 — $n track (도메인별 병렬 자율 드레인)" + local i=0 name open + while read -r name; do + [ -z "$name" ] && continue + i=$((i + 1)) + open=$(printf '%s\n' "$rows" | grep "^$name|" | cut -d'|' -f2) + printf ' track %d: %-17s → /domain set %s && /cycle-bg (%s open)\n' "$i" "$name" "$name" "${open:-?}" + done < "$TRACKS" + echo "▶ 모델: 각 track 을 background Agent 로 팬아웃 — 도메인별 자율 드레인 · isolation worktree · track별 /pr-cycle." +} + +pick() { # $@ = " …" OR " track" + local rows; rows="$(_rows)" + [ -n "$rows" ] || { echo "✗ /todo: 도메인 없음 (DOMAINS.tape 확인)"; return 0; } + local names=() i nm + if [ "$#" -ge 2 ] && [ "$2" = "track" ] && printf '%s' "$1" | grep -qE '^[0-9]+$'; then + while IFS= read -r nm; do [ -n "$nm" ] && names+=("$nm"); done < <(printf '%s\n' "$rows" | head -n "$1" | cut -d'|' -f1) + else + for i in "$@"; do + printf '%s' "$i" | grep -qE '^[0-9]+$' || continue + nm="$(printf '%s\n' "$rows" | sed -n "${i}p" | cut -d'|' -f1)" + [ -n "$nm" ] && names+=("$nm") + done + fi + [ "${#names[@]}" -gt 0 ] || { echo "✗ /todo: 선택된 도메인 없음 — /todo 로 번호 먼저 확인"; return 0; } + printf '%s\n' "${names[@]}" > "$TRACKS" + plan +} + +case "${1:-}" in + ""|ls|list) dashboard ;; + go|plan) plan ;; + status) [ -s "$TRACKS" ] && { echo "🚦 현재 트랙셋:"; sed 's/^/ /' "$TRACKS"; } || echo "트랙셋 비어있음 — /todo <번호…>" ;; + clear) : > "$TRACKS"; echo "✅ 트랙셋 해제됨" ;; + *) pick "$@" ;; +esac diff --git a/commands/todo/commands/todo.md b/commands/todo/commands/todo.md new file mode 100644 index 0000000..60ca270 --- /dev/null +++ b/commands/todo/commands/todo.md @@ -0,0 +1,14 @@ +--- +description: /todo — cross-domain TODO + multi-track launcher. Reads the repo's DOMAINS.tape roster + each domain snapshot, ranks domains by OPEN milestone count (`- [ ]`), and renders a numbered dashboard (★ active · progress bar · open count · @goal). Select a subset to drive in parallel — `/todo …` picks by number, `/todo track` auto-picks the top-N most-open domains as N tracks (e.g. `/todo 2 track`). The pick persists a TRACK SET (under ~/.sidecar/todo) and prints a per-track launch plan; the MODEL then fans out one background Agent per track (each runs `/domain set ` then autonomously drains that domain's open milestones, isolation worktree, one /pr-cycle per logical unit). Verbs — bare/ls (dashboard) · `` / ` track` (select) · go/plan (re-print plan) · status (show track set) · clear. Read-only over the repo; never edits a domain doc/verdict/log. +argument-hint: "[ … | track | go | status | clear]" +allowed-tools: Bash +--- + +!`ROOT="$CLAUDE_PLUGIN_ROOT" +if [ ! -d "$ROOT/bin" ]; then + V="$(ls -1 "$HOME/.claude/plugins/cache/sidecar/todo" 2>/dev/null | sort -V | tail -1)" + [ -n "$V" ] && ROOT="$HOME/.claude/plugins/cache/sidecar/todo/$V" +fi +S="$ROOT/bin/_todo.sh" +[ -f "$S" ] || { echo "✗ _todo.sh not found — /reload-plugins or hx install sidecar"; exit 1; } +bash "$S" $ARGUMENTS`