Skip to content

Commit cb4053d

Browse files
committed
feat: close ship-blockers #4 (preflight state) and #20 (uninstall.sh)
#4 — cmd_preflight now emits a `recommended_action` field with one of: recover | run_setup | generate_prd | parse_prd | resume | complete The previously-ambiguous row (prd exists + task_count == 0) resolves explicitly to `parse_prd`. Decision logic lives in the deterministic layer; callers no longer have to re-derive state from raw fields. #20 — new uninstall.sh mirrors install.sh conventions: - --yes for non-interactive, --dry-run for preview, --help prints usage - Removes ~/.claude/skills/prd-taskmaster-v2/ only - Best-effort prune of the updates.json tracker entry (jq-gated, non-fatal) - Explicit non-goal: leaves downstream .taskmaster/ artifacts untouched (user data, not skill data) - Status output goes to stderr to match install.sh's pipe-through behavior Tests: 5 new recommended_action cases cover all preflight state transitions. One pre-existing crash-state test had its trailing assertions restored after they were accidentally split by a prior edit. Full suite: 222 passed / 1 skip. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f97e714 commit cb4053d

File tree

3 files changed

+146
-2
lines changed

3 files changed

+146
-2
lines changed

script.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,24 @@ def cmd_preflight(args: argparse.Namespace) -> None:
154154

155155
# Check crash state
156156
crash_state = _read_execution_state()
157+
has_crash = crash_state.get("has_incomplete", False)
158+
159+
# Decision table — single authoritative mapping of state -> next action.
160+
# Closes ship-blocker #4: callers no longer have to assemble the 4-way
161+
# ambiguity (prd×tasks) themselves. Order matters: crash recovery wins,
162+
# then setup gap, then the prd/tasks axis.
163+
if has_crash:
164+
recommended_action = "recover"
165+
elif not has_taskmaster:
166+
recommended_action = "run_setup"
167+
elif prd_path is None:
168+
recommended_action = "generate_prd"
169+
elif task_count == 0:
170+
recommended_action = "parse_prd"
171+
elif tasks_pending > 0:
172+
recommended_action = "resume"
173+
else:
174+
recommended_action = "complete"
157175

158176
emit({
159177
"ok": True,
@@ -164,8 +182,9 @@ def cmd_preflight(args: argparse.Namespace) -> None:
164182
"tasks_pending": tasks_pending,
165183
"taskmaster_method": tm_method["method"],
166184
"has_claude_md": has_claude_md,
167-
"has_crash_state": crash_state.get("has_incomplete", False),
168-
"crash_state": crash_state if crash_state.get("has_incomplete") else None,
185+
"has_crash_state": has_crash,
186+
"crash_state": crash_state if has_crash else None,
187+
"recommended_action": recommended_action,
169188
})
170189

171190

tests/test_script.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,45 @@ def test_preflight_with_crash_state(self, tmp_project, execution_state):
6363
assert out["crash_state"]["mode"] == "sequential"
6464
assert out["crash_state"]["checkpoint"] == "2"
6565

66+
# Closes ship-blocker #4 — recommended_action disambiguates state.
67+
def test_recommended_action_run_setup(self, tmp_path):
68+
rc, out = run_script(SCRIPT_PY, ["preflight"], cwd=str(tmp_path))
69+
assert rc == 0 and out["recommended_action"] == "run_setup"
70+
71+
def test_recommended_action_generate_prd(self, tmp_path):
72+
(tmp_path / ".taskmaster" / "docs").mkdir(parents=True)
73+
(tmp_path / ".taskmaster" / "tasks").mkdir(parents=True)
74+
rc, out = run_script(SCRIPT_PY, ["preflight"], cwd=str(tmp_path))
75+
assert rc == 0 and out["recommended_action"] == "generate_prd"
76+
77+
def test_recommended_action_parse_prd_when_prd_but_no_tasks(self, tmp_path):
78+
"""The original ambiguous row — prd exists, task_count == 0."""
79+
docs = tmp_path / ".taskmaster" / "docs"
80+
docs.mkdir(parents=True)
81+
(tmp_path / ".taskmaster" / "tasks").mkdir(parents=True)
82+
(docs / "prd.md").write_text("# stub\n")
83+
rc, out = run_script(SCRIPT_PY, ["preflight"], cwd=str(tmp_path))
84+
assert rc == 0 and out["recommended_action"] == "parse_prd"
85+
86+
def test_recommended_action_resume_when_tasks_pending(self, tmp_path):
87+
docs = tmp_path / ".taskmaster" / "docs"
88+
tasks = tmp_path / ".taskmaster" / "tasks"
89+
docs.mkdir(parents=True); tasks.mkdir(parents=True)
90+
(docs / "prd.md").write_text("# stub\n")
91+
(tasks / "tasks.json").write_text(
92+
'{"tasks":[{"id":1,"status":"pending"},{"id":2,"status":"done"}]}')
93+
rc, out = run_script(SCRIPT_PY, ["preflight"], cwd=str(tmp_path))
94+
assert rc == 0 and out["recommended_action"] == "resume"
95+
96+
def test_recommended_action_complete_when_no_pending(self, tmp_path):
97+
docs = tmp_path / ".taskmaster" / "docs"
98+
tasks = tmp_path / ".taskmaster" / "tasks"
99+
docs.mkdir(parents=True); tasks.mkdir(parents=True)
100+
(docs / "prd.md").write_text("# stub\n")
101+
(tasks / "tasks.json").write_text('{"tasks":[{"id":1,"status":"done"}]}')
102+
rc, out = run_script(SCRIPT_PY, ["preflight"], cwd=str(tmp_path))
103+
assert rc == 0 and out["recommended_action"] == "complete"
104+
66105
def test_preflight_detects_claude_md(self, tmp_project):
67106
"""Detects CLAUDE.md in project root."""
68107
(tmp_project / "CLAUDE.md").write_text("# Project guide")

uninstall.sh

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env bash
2+
# ============================================================================
3+
# prd-taskmaster-v2 uninstaller (closes ship-readiness issue #20)
4+
# ============================================================================
5+
# Usage:
6+
# bash uninstall.sh # interactive confirmation
7+
# bash uninstall.sh --yes # non-interactive
8+
# bash uninstall.sh --dry-run
9+
#
10+
# Removes ~/.claude/skills/prd-taskmaster-v2/ and the update-tracker entry
11+
# created by install.sh. Leaves user-generated .taskmaster/ artifacts in
12+
# downstream projects alone — those are user data, not skill data.
13+
# ============================================================================
14+
15+
set -euo pipefail
16+
17+
SKILL_NAME="prd-taskmaster-v2"
18+
SKILL_DIR="${SKILL_DIR:-${HOME}/.claude/skills/${SKILL_NAME}}"
19+
UPDATES_FILE="${HOME}/.config/claude-skills/updates.json"
20+
21+
YES=0
22+
DRY_RUN=0
23+
for arg in "$@"; do
24+
case "$arg" in
25+
--yes|-y) YES=1 ;;
26+
--dry-run) DRY_RUN=1 ;;
27+
-h|--help)
28+
sed -n '2,14p' "$0"; exit 0 ;;
29+
*) echo "unknown arg: $arg" >&2; exit 2 ;;
30+
esac
31+
done
32+
33+
if { [[ -t 1 ]] || [[ -t 2 ]]; } && [[ -z "${CI:-}" ]] && [[ -z "${NO_COLOR:-}" ]]; then
34+
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'
35+
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
36+
else
37+
RED=''; GREEN=''; YELLOW=''; CYAN=''; BOLD=''; RESET=''
38+
fi
39+
40+
info() { printf "${CYAN}[info]${RESET} %s\n" "$*" >&2; }
41+
ok() { printf "${GREEN}[ok]${RESET} %s\n" "$*" >&2; }
42+
warn() { printf "${YELLOW}[warn]${RESET} %s\n" "$*" >&2; }
43+
err() { printf "${RED}[error]${RESET} %s\n" "$*" >&2; }
44+
45+
if [[ ! -d "${SKILL_DIR}" ]]; then
46+
warn "Skill not installed at ${SKILL_DIR} — nothing to do."
47+
exit 0
48+
fi
49+
50+
info "Found skill at ${BOLD}${SKILL_DIR}${RESET}"
51+
if [[ -f "${SKILL_DIR}/.version" ]]; then
52+
info "Installed version: $(head -1 "${SKILL_DIR}/.version" 2>/dev/null || echo unknown)"
53+
fi
54+
55+
if [[ ${YES} -eq 0 ]] && [[ ${DRY_RUN} -eq 0 ]]; then
56+
printf "${YELLOW}Remove ${SKILL_DIR}? [y/N]${RESET} " >&2
57+
read -r reply
58+
case "$reply" in
59+
y|Y|yes|YES) ;;
60+
*) info "Aborted."; exit 0 ;;
61+
esac
62+
fi
63+
64+
if [[ ${DRY_RUN} -eq 1 ]]; then
65+
info "[dry-run] would remove: ${SKILL_DIR}"
66+
[[ -f "${UPDATES_FILE}" ]] && info "[dry-run] would prune ${SKILL_NAME} entry from ${UPDATES_FILE}"
67+
exit 0
68+
fi
69+
70+
rm -rf "${SKILL_DIR}"
71+
ok "Removed ${SKILL_DIR}"
72+
73+
# Best-effort prune of the updates tracker entry. Non-fatal if jq is missing
74+
# or the file doesn't exist — worst case a stale entry survives.
75+
if [[ -f "${UPDATES_FILE}" ]] && command -v jq >/dev/null 2>&1; then
76+
tmp="$(mktemp)"
77+
if jq "del(.[\"${SKILL_NAME}\"])" "${UPDATES_FILE}" > "${tmp}" 2>/dev/null; then
78+
mv "${tmp}" "${UPDATES_FILE}"
79+
ok "Pruned ${SKILL_NAME} from ${UPDATES_FILE}"
80+
else
81+
rm -f "${tmp}"
82+
warn "Could not prune updates tracker (non-fatal)"
83+
fi
84+
fi
85+
86+
ok "Uninstall complete. Downstream .taskmaster/ directories in your projects were left untouched."

0 commit comments

Comments
 (0)