Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Structured planning workflow that uses files to track tasks, decisions, and project progress.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/resolve-plan-dir.sh
1#!/bin/sh2# planning-with-files: resolve active plan directory.3#4# Resolution order:5# 1. $PLAN_ID env var → ./.planning/$PLAN_ID/ if exists6# 2. ./.planning/.active_plan content → matching dir if exists7# 3. Newest ./.planning/<dir>/ by mtime8# 4. Otherwise empty stdout (caller falls back to legacy ./task_plan.md)9#10# Always exits 0. Never errors out the agent loop.11#12# Usage:13# PLAN_DIR="$(sh scripts/resolve-plan-dir.sh)"14# PLAN_FILE="${PLAN_DIR:+$PLAN_DIR/}task_plan.md"1516set -u1718PLAN_ROOT="${1:-${PWD}/.planning}"19ACTIVE_FILE="${PLAN_ROOT}/.active_plan"2021# Plan-id safe-identifier check. Rejects whitespace, path separators, leading22# dots, and empty strings; accepts the YYYY-MM-DD-<slug> shape from23# init-session.sh as well as legacy hand-created names like "alpha" or24# "feature-foo". The intent is to filter garbage content (e.g. a corrupt25# .active_plan file containing only whitespace or random text) without26# enforcing a date prefix that would break backward compatibility.27SLUG_RE='^[A-Za-z0-9_][A-Za-z0-9._-]*$'2829slug_is_valid() {30case "$1" in31'') return 1 ;;32esac33printf "%s" "$1" | grep -Eq "${SLUG_RE}"34}3536# Portable path canonicalizer. realpath first (Linux, modern coreutils),37# then readlink -f (older GNU), then python3/python os.path.realpath. Prints38# the canonical absolute path on success; prints nothing and returns 1 on a39# full miss so the caller can decide what to do. No python spawn on the happy40# path: realpath/readlink cover Linux, WSL, Git-Bash, and modern macOS.41canonicalize() {42target="$1"43if command -v realpath >/dev/null 2>&1; then44out="$(realpath "${target}" 2>/dev/null)" && [ -n "${out}" ] && {45printf "%s\n" "${out}"; return 0; }46fi47if command -v readlink >/dev/null 2>&1; then48out="$(readlink -f "${target}" 2>/dev/null)" && [ -n "${out}" ] && {49printf "%s\n" "${out}"; return 0; }50fi51if command -v python3 >/dev/null 2>&1; then52out="$(python3 -c "import os,sys;print(os.path.realpath(sys.argv[1]))" "${target}" 2>/dev/null)" \53&& [ -n "${out}" ] && { printf "%s\n" "${out}"; return 0; }54fi55if command -v python >/dev/null 2>&1; then56out="$(python -c "import os,sys;print(os.path.realpath(sys.argv[1]))" "${target}" 2>/dev/null)" \57&& [ -n "${out}" ] && { printf "%s\n" "${out}"; return 0; }58fi59return 160}6162# Containment guard (security A1.3): a resolved plan dir must canonicalize to a63# path under the project root (the CWD the script runs from). A symlink inside64# a valid slug dir pointing at /etc or outside the workspace would otherwise let65# the hooks hash and inject an arbitrary file. On any violation we return 1 so66# the caller treats the candidate as unresolved and falls back safely. If67# canonicalization is unavailable for BOTH paths we fail open (return 0) to keep68# legacy behavior byte-equivalent on minimal shells that lack realpath/readlink69# and python; the SLUG_RE check already blocks traversal in the slug name.70is_within_root() {71candidate="$1"72root_real="$(canonicalize "${PWD}")" || root_real=""73cand_real="$(canonicalize "${candidate}")" || cand_real=""74if [ -z "${root_real}" ] || [ -z "${cand_real}" ]; then75return 076fi77case "${cand_real}" in78"${root_real}"|"${root_real}"/*) return 0 ;;79*) return 1 ;;80esac81}8283# Portable mtime resolver. Tries GNU stat, BSD stat, BSD/macOS date -r,84# python3, then perl. Returns "0" on full miss so callers can sort.85mtime_of() {86target="$1"87out="$(stat -c '%Y' "${target}" 2>/dev/null)"88if [ -n "${out}" ]; then printf "%s\n" "${out}"; return 0; fi89out="$(stat -f '%m' "${target}" 2>/dev/null)"90if [ -n "${out}" ]; then printf "%s\n" "${out}"; return 0; fi91out="$(date -r "${target}" +%s 2>/dev/null)"92if [ -n "${out}" ]; then printf "%s\n" "${out}"; return 0; fi93if command -v python3 >/dev/null 2>&1; then94out="$(python3 -c "import os,sys;print(int(os.stat(sys.argv[1]).st_mtime))" "${target}" 2>/dev/null)"95if [ -n "${out}" ]; then printf "%s\n" "${out}"; return 0; fi96fi97if command -v python >/dev/null 2>&1; then98out="$(python -c "import os,sys;print(int(os.stat(sys.argv[1]).st_mtime))" "${target}" 2>/dev/null)"99if [ -n "${out}" ]; then printf "%s\n" "${out}"; return 0; fi100fi101if command -v perl >/dev/null 2>&1; then102out="$(perl -e 'print((stat shift)[9])' "${target}" 2>/dev/null)"103if [ -n "${out}" ]; then printf "%s\n" "${out}"; return 0; fi104fi105printf "0\n"106}107108resolve_from_env() {109plan_id="${PLAN_ID:-}"110slug_is_valid "${plan_id}" || return 1111candidate="${PLAN_ROOT}/${plan_id}"112if [ -d "${candidate}" ] && is_within_root "${candidate}"; then113printf "%s\n" "${candidate}"114return 0115fi116return 1117}118119resolve_from_active_file() {120[ -f "${ACTIVE_FILE}" ] || return 1121plan_id="$(tr -d '\r\n[:space:]' < "${ACTIVE_FILE}")"122slug_is_valid "${plan_id}" || return 1123candidate="${PLAN_ROOT}/${plan_id}"124if [ -d "${candidate}" ] && is_within_root "${candidate}"; then125printf "%s\n" "${candidate}"126return 0127fi128return 1129}130131resolve_latest_dir() {132[ -d "${PLAN_ROOT}" ] || return 1133# Portable newest-mtime selector. Skips hidden dirs, slug-invalid names,134# and dirs without task_plan.md (e.g. sessions/).135latest=""136latest_mtime=0137for entry in "${PLAN_ROOT}"/*/; do138[ -d "${entry}" ] || continue139clean="${entry%/}"140name="$(basename "${clean}")"141case "${name}" in142.*) continue ;;143esac144slug_is_valid "${name}" || continue145[ -f "${clean}/task_plan.md" ] || continue146is_within_root "${clean}" || continue147mtime="$(mtime_of "${clean}")"148if [ "${mtime}" -gt "${latest_mtime}" ] 2>/dev/null; then149latest_mtime="${mtime}"150latest="${clean}"151fi152done153if [ -n "${latest}" ]; then154printf "%s\n" "${latest}"155return 0156fi157return 1158}159160if resolve_from_env; then exit 0; fi161if resolve_from_active_file; then exit 0; fi162if resolve_latest_dir; then exit 0; fi163exit 0164