Bug fixes (items 1-5 in proposal_v2_40.md):
1. Hook resolution order inverted across UserPromptSubmit, PreToolUse, PreCompact.
Slug-mode now wins over legacy root task_plan.md when both exist. Closes the
bug where an active .planning/<slug>/ plan was silently bypassed in favor of
the root file. Resolution chain: PLAN_ID env > .planning/.active_plan >
newest .planning/<slug>/ by mtime > root fallback.
2. .active_plan target dir is validated. Dead pointers fall through cleanly.
3. .active_plan content is regex-validated as ^[A-Za-z0-9_][A-Za-z0-9._-]*$.
Whitespace-only and path-traversal corruption fall through instead of
producing weird path lookups. Tests cover whitespace, ../escape, and
leading-dot cases.
4. check-complete.sh wires into resolve-plan-dir.sh when no path arg given.
Restores slug-mode parity for any caller that invokes the script without an
explicit plan path.
5. Pi extension dangerous-command detection uses word-boundary regex. Benign
git push origin <branch> no longer triggers the warning; only --force,
--mirror, +ref, plus rm -rf, sudo, chmod 777/a+rwx, git reset --hard, git
clean -fd, fork bombs, dd to /dev/[sh]d[a-z].
Performance (items 6, 7):
6. mtime-keyed SHA-256 cache under ${TMPDIR:-/tmp}/pwf-sha cuts redundant hash
work on warm cache. Caches the result keyed by absolute plan-file path,
stores mtime + hash, skips sha256sum when mtime unchanged.
7. KV-cache hygiene on injected progress.md tail. Sub-second timestamps and
timezone-suffix forms are normalized to a stable epoch-zero form so the
model's prefix cache stays warm across turns. Manus-aligned.
Portability + races (items 19, 20):
19. resolve-plan-dir.sh portable mtime fallback chain: GNU stat -> BSD stat ->
date -r -> python3 -> python -> perl -> 0. Multi-plan workflows stop
silently degrading on non-GNU systems.
20. attest-plan.sh uses atomic temp+rename with optional flock guard.
Concurrent legacy-mode attestations stop producing truncated files that
triggered false [PLAN TAMPERED] warnings.
Tests: 130 pass / 2 pre-existing Windows exec-bit failures unchanged. +20 new
tests vs v2.39.0. See CHANGELOG for full inventory.
Version bumped to 2.40.0 across 14 SKILL.md variants + plugin.json +
marketplace.json + CITATION.cff via scripts/bump-version.py. .continue,
.gemini, .pi, .kiro lag intentionally.
116 lines
3.8 KiB
Bash
Executable File
116 lines
3.8 KiB
Bash
Executable File
#!/bin/sh
|
|
# planning-with-files: resolve active plan directory.
|
|
#
|
|
# Resolution order:
|
|
# 1. $PLAN_ID env var → ./.planning/$PLAN_ID/ if exists
|
|
# 2. ./.planning/.active_plan content → matching dir if exists
|
|
# 3. Newest ./.planning/<dir>/ by mtime
|
|
# 4. Otherwise empty stdout (caller falls back to legacy ./task_plan.md)
|
|
#
|
|
# Always exits 0. Never errors out the agent loop.
|
|
#
|
|
# Usage:
|
|
# PLAN_DIR="$(sh scripts/resolve-plan-dir.sh)"
|
|
# PLAN_FILE="${PLAN_DIR:+$PLAN_DIR/}task_plan.md"
|
|
|
|
set -u
|
|
|
|
PLAN_ROOT="${1:-${PWD}/.planning}"
|
|
ACTIVE_FILE="${PLAN_ROOT}/.active_plan"
|
|
|
|
# Plan-id safe-identifier check. Rejects whitespace, path separators, leading
|
|
# dots, and empty strings; accepts the YYYY-MM-DD-<slug> shape from
|
|
# init-session.sh as well as legacy hand-created names like "alpha" or
|
|
# "feature-foo". The intent is to filter garbage content (e.g. a corrupt
|
|
# .active_plan file containing only whitespace or random text) without
|
|
# enforcing a date prefix that would break backward compatibility.
|
|
SLUG_RE='^[A-Za-z0-9_][A-Za-z0-9._-]*$'
|
|
|
|
slug_is_valid() {
|
|
case "$1" in
|
|
'') return 1 ;;
|
|
esac
|
|
printf "%s" "$1" | grep -Eq "${SLUG_RE}"
|
|
}
|
|
|
|
# Portable mtime resolver. Tries GNU stat, BSD stat, BSD/macOS date -r,
|
|
# python3, then perl. Returns "0" on full miss so callers can sort.
|
|
mtime_of() {
|
|
target="$1"
|
|
out="$(stat -c '%Y' "${target}" 2>/dev/null)"
|
|
if [ -n "${out}" ]; then printf "%s\n" "${out}"; return 0; fi
|
|
out="$(stat -f '%m' "${target}" 2>/dev/null)"
|
|
if [ -n "${out}" ]; then printf "%s\n" "${out}"; return 0; fi
|
|
out="$(date -r "${target}" +%s 2>/dev/null)"
|
|
if [ -n "${out}" ]; then printf "%s\n" "${out}"; return 0; fi
|
|
if command -v python3 >/dev/null 2>&1; then
|
|
out="$(python3 -c "import os,sys;print(int(os.stat(sys.argv[1]).st_mtime))" "${target}" 2>/dev/null)"
|
|
if [ -n "${out}" ]; then printf "%s\n" "${out}"; return 0; fi
|
|
fi
|
|
if command -v python >/dev/null 2>&1; then
|
|
out="$(python -c "import os,sys;print(int(os.stat(sys.argv[1]).st_mtime))" "${target}" 2>/dev/null)"
|
|
if [ -n "${out}" ]; then printf "%s\n" "${out}"; return 0; fi
|
|
fi
|
|
if command -v perl >/dev/null 2>&1; then
|
|
out="$(perl -e 'print((stat shift)[9])' "${target}" 2>/dev/null)"
|
|
if [ -n "${out}" ]; then printf "%s\n" "${out}"; return 0; fi
|
|
fi
|
|
printf "0\n"
|
|
}
|
|
|
|
resolve_from_env() {
|
|
plan_id="${PLAN_ID:-}"
|
|
slug_is_valid "${plan_id}" || return 1
|
|
candidate="${PLAN_ROOT}/${plan_id}"
|
|
if [ -d "${candidate}" ]; then
|
|
printf "%s\n" "${candidate}"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
resolve_from_active_file() {
|
|
[ -f "${ACTIVE_FILE}" ] || return 1
|
|
plan_id="$(tr -d '\r\n[:space:]' < "${ACTIVE_FILE}")"
|
|
slug_is_valid "${plan_id}" || return 1
|
|
candidate="${PLAN_ROOT}/${plan_id}"
|
|
if [ -d "${candidate}" ]; then
|
|
printf "%s\n" "${candidate}"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
resolve_latest_dir() {
|
|
[ -d "${PLAN_ROOT}" ] || return 1
|
|
# Portable newest-mtime selector. Skips hidden dirs, slug-invalid names,
|
|
# and dirs without task_plan.md (e.g. sessions/).
|
|
latest=""
|
|
latest_mtime=0
|
|
for entry in "${PLAN_ROOT}"/*/; do
|
|
[ -d "${entry}" ] || continue
|
|
clean="${entry%/}"
|
|
name="$(basename "${clean}")"
|
|
case "${name}" in
|
|
.*) continue ;;
|
|
esac
|
|
slug_is_valid "${name}" || continue
|
|
[ -f "${clean}/task_plan.md" ] || continue
|
|
mtime="$(mtime_of "${clean}")"
|
|
if [ "${mtime}" -gt "${latest_mtime}" ] 2>/dev/null; then
|
|
latest_mtime="${mtime}"
|
|
latest="${clean}"
|
|
fi
|
|
done
|
|
if [ -n "${latest}" ]; then
|
|
printf "%s\n" "${latest}"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
if resolve_from_env; then exit 0; fi
|
|
if resolve_from_active_file; then exit 0; fi
|
|
if resolve_latest_dir; then exit 0; fi
|
|
exit 0
|