planning-with-files 3.10.2 → 3.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extensions/planning-with-files/__tests__/plan-anchor.test.ts +228 -228
- package/extensions/planning-with-files/package.json +17 -17
- package/extensions/planning-with-files/runtime.ts +788 -788
- package/package.json +1 -1
- package/scripts/attest-plan.ps1 +137 -137
- package/scripts/attest-plan.sh +206 -206
- package/scripts/check-complete.ps1 +253 -253
- package/scripts/check-complete.sh +253 -253
- package/scripts/gate-stop.sh +32 -32
- package/scripts/ledger-append.ps1 +180 -180
- package/scripts/ledger-append.sh +337 -337
- package/scripts/ledger-summary.ps1 +128 -128
- package/scripts/phase-status.ps1 +175 -175
- package/scripts/phase-status.sh +158 -158
- package/scripts/session-catchup.py +876 -876
- package/scripts/set-active-plan.ps1 +51 -51
- package/scripts/set-active-plan.sh +50 -50
|
@@ -1,253 +1,253 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Check if all phases in task_plan.md are complete
|
|
3
|
-
# Default invocation: advisory echo, always exits 0 (Stop hook status report).
|
|
4
|
-
# With --gate: deliberate completion gate, opt-in per plan via <plan-dir>/.mode.
|
|
5
|
-
# Used by Stop hook to report task completion status.
|
|
6
|
-
#
|
|
7
|
-
# Plan-file resolution (v2.40+):
|
|
8
|
-
# 1. $1 (explicit path) — first non-flag positional argument
|
|
9
|
-
# 2. resolve-plan-dir.sh: $PLAN_ID env → .planning/.active_plan → newest mtime
|
|
10
|
-
# 3. Legacy ./task_plan.md
|
|
11
|
-
#
|
|
12
|
-
# This restores slug-mode parity: the Stop hook and any caller invoking with
|
|
13
|
-
# zero args now respects the active plan dir instead of silently defaulting to
|
|
14
|
-
# the legacy root path.
|
|
15
|
-
#
|
|
16
|
-
# Gate mode (v3, --gate flag):
|
|
17
|
-
# The gate is OFF unless ALL of these hold (design "Gate decision table"):
|
|
18
|
-
# 1. <plan-dir>/.mode exists and contains "gate" (explicit opt-in)
|
|
19
|
-
# 2. an in_progress phase exists (not merely complete<total)
|
|
20
|
-
# 3. the Stop hook input JSON on stdin does not set stop_hook_active=true
|
|
21
|
-
# 4. the block counter (<plan-dir>/.stop_blocks) is below cap (PWF_GATE_CAP, default 20)
|
|
22
|
-
# 5. the ledger advanced since the last block (stall → allow stop)
|
|
23
|
-
# When all hold, it emits a single-line block-decision JSON on stdout and
|
|
24
|
-
# exits 0. Otherwise it falls back to advisory output and exits 0.
|
|
25
|
-
# Without --gate, or in non-gated mode, behavior is byte-equivalent to v2.43.
|
|
26
|
-
#
|
|
27
|
-
# Stdin handling: the Claude Code Stop hook pipes a JSON payload on stdin. To
|
|
28
|
-
# avoid hanging when nothing is piped, stdin is read ONLY when fd 0 is not a
|
|
29
|
-
# TTY ([ -t 0 ]). Hook-piped input is EOF-terminated, so the read returns; an
|
|
30
|
-
# interactive terminal (TTY) is skipped entirely. No data on stdin is treated
|
|
31
|
-
# as stop_hook_active=false.
|
|
32
|
-
|
|
33
|
-
# issue #195: per-invocation opt-out (PLANNING_DISABLED=1) for one-shot/CI
|
|
34
|
-
# sessions that share a cwd with a plan but never opted into it.
|
|
35
|
-
[ "${PLANNING_DISABLED:-}" = "1" ] && exit 0
|
|
36
|
-
|
|
37
|
-
GATE=0
|
|
38
|
-
PLAN_FILE=""
|
|
39
|
-
for _arg in "$@"; do
|
|
40
|
-
case "$_arg" in
|
|
41
|
-
--gate) GATE=1 ;;
|
|
42
|
-
*)
|
|
43
|
-
if [ -z "$PLAN_FILE" ]; then
|
|
44
|
-
PLAN_FILE="$_arg"
|
|
45
|
-
fi
|
|
46
|
-
;;
|
|
47
|
-
esac
|
|
48
|
-
done
|
|
49
|
-
|
|
50
|
-
PLAN_DIR=""
|
|
51
|
-
if [ -n "${PLAN_FILE}" ]; then
|
|
52
|
-
PLAN_DIR="$(dirname "${PLAN_FILE}")"
|
|
53
|
-
else
|
|
54
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd 2>/dev/null)" || SCRIPT_DIR="."
|
|
55
|
-
RESOLVER="${SCRIPT_DIR}/resolve-plan-dir.sh"
|
|
56
|
-
RESOLVED_DIR=""
|
|
57
|
-
if [ -f "${RESOLVER}" ]; then
|
|
58
|
-
RESOLVED_DIR="$(sh "${RESOLVER}" 2>/dev/null)"
|
|
59
|
-
fi
|
|
60
|
-
if [ -n "${RESOLVED_DIR}" ] && [ -f "${RESOLVED_DIR}/task_plan.md" ]; then
|
|
61
|
-
PLAN_FILE="${RESOLVED_DIR}/task_plan.md"
|
|
62
|
-
PLAN_DIR="${RESOLVED_DIR}"
|
|
63
|
-
else
|
|
64
|
-
PLAN_FILE="task_plan.md"
|
|
65
|
-
PLAN_DIR="."
|
|
66
|
-
fi
|
|
67
|
-
fi
|
|
68
|
-
|
|
69
|
-
if [ ! -f "$PLAN_FILE" ]; then
|
|
70
|
-
echo "[planning-with-files] No task_plan.md found — no active planning session."
|
|
71
|
-
exit 0
|
|
72
|
-
fi
|
|
73
|
-
|
|
74
|
-
# Count total phases
|
|
75
|
-
TOTAL=$(grep -c "### Phase" "$PLAN_FILE" || true)
|
|
76
|
-
|
|
77
|
-
# Count both formats per field and keep the larger of the two. A plan may mix
|
|
78
|
-
# '**Status:** pending' on one phase with '[in_progress]' on another; counting
|
|
79
|
-
# only the primary format (and falling back to inline ONLY when all three
|
|
80
|
-
# primaries are zero) lost the inline count and let an in_progress plan slip
|
|
81
|
-
# past the gate. Per-field max preserves the legacy single-format result
|
|
82
|
-
# (the other format contributes 0) while catching mixed plans.
|
|
83
|
-
COMPLETE_PRIMARY=$(grep -cF "**Status:** complete" "$PLAN_FILE" || true)
|
|
84
|
-
IN_PROGRESS_PRIMARY=$(grep -cF "**Status:** in_progress" "$PLAN_FILE" || true)
|
|
85
|
-
PENDING_PRIMARY=$(grep -cF "**Status:** pending" "$PLAN_FILE" || true)
|
|
86
|
-
|
|
87
|
-
COMPLETE_INLINE=$(grep -c "\[complete\]" "$PLAN_FILE" || true)
|
|
88
|
-
IN_PROGRESS_INLINE=$(grep -c "\[in_progress\]" "$PLAN_FILE" || true)
|
|
89
|
-
PENDING_INLINE=$(grep -c "\[pending\]" "$PLAN_FILE" || true)
|
|
90
|
-
|
|
91
|
-
: "${COMPLETE_PRIMARY:=0}"; : "${IN_PROGRESS_PRIMARY:=0}"; : "${PENDING_PRIMARY:=0}"
|
|
92
|
-
: "${COMPLETE_INLINE:=0}"; : "${IN_PROGRESS_INLINE:=0}"; : "${PENDING_INLINE:=0}"
|
|
93
|
-
|
|
94
|
-
if [ "$COMPLETE_INLINE" -gt "$COMPLETE_PRIMARY" ]; then COMPLETE="$COMPLETE_INLINE"; else COMPLETE="$COMPLETE_PRIMARY"; fi
|
|
95
|
-
if [ "$IN_PROGRESS_INLINE" -gt "$IN_PROGRESS_PRIMARY" ]; then IN_PROGRESS="$IN_PROGRESS_INLINE"; else IN_PROGRESS="$IN_PROGRESS_PRIMARY"; fi
|
|
96
|
-
if [ "$PENDING_INLINE" -gt "$PENDING_PRIMARY" ]; then PENDING="$PENDING_INLINE"; else PENDING="$PENDING_PRIMARY"; fi
|
|
97
|
-
|
|
98
|
-
# Default to 0 if empty
|
|
99
|
-
: "${TOTAL:=0}"
|
|
100
|
-
: "${COMPLETE:=0}"
|
|
101
|
-
: "${IN_PROGRESS:=0}"
|
|
102
|
-
: "${PENDING:=0}"
|
|
103
|
-
|
|
104
|
-
# issue #191: no "### Phase" headings -> not a phase-structured plan. Report
|
|
105
|
-
# nothing rather than a false "0/0 phases complete" status. With TOTAL=0 the
|
|
106
|
-
# gate can never legitimately block (IN_PROGRESS is also 0), so exit is safe.
|
|
107
|
-
if [ "$TOTAL" -eq 0 ]; then
|
|
108
|
-
exit 0
|
|
109
|
-
fi
|
|
110
|
-
|
|
111
|
-
# advisory_report: the v2.43 status echo. Always exit 0 after calling.
|
|
112
|
-
advisory_report() {
|
|
113
|
-
if [ "$COMPLETE" -eq "$TOTAL" ] && [ "$TOTAL" -gt 0 ]; then
|
|
114
|
-
echo "[planning-with-files] ALL PHASES COMPLETE ($COMPLETE/$TOTAL). If the user has additional work, add new phases to task_plan.md before starting."
|
|
115
|
-
else
|
|
116
|
-
echo "[planning-with-files] Task in progress ($COMPLETE/$TOTAL phases complete). Update progress.md before stopping."
|
|
117
|
-
if [ "$IN_PROGRESS" -gt 0 ]; then
|
|
118
|
-
echo "[planning-with-files] $IN_PROGRESS phase(s) still in progress."
|
|
119
|
-
fi
|
|
120
|
-
if [ "$PENDING" -gt 0 ]; then
|
|
121
|
-
echo "[planning-with-files] $PENDING phase(s) pending."
|
|
122
|
-
fi
|
|
123
|
-
fi
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
# ---- Default (advisory) path: byte-equivalent to v2.43 ----
|
|
127
|
-
if [ "$GATE" -ne 1 ]; then
|
|
128
|
-
advisory_report
|
|
129
|
-
exit 0
|
|
130
|
-
fi
|
|
131
|
-
|
|
132
|
-
# ---- Gate path (--gate). Resolves to advisory unless every guard says block. ----
|
|
133
|
-
|
|
134
|
-
# Guard 1: gated mode. The .mode file must contain "gate". Absent or other
|
|
135
|
-
# content means advisory mode (legacy behavior preserved).
|
|
136
|
-
MODE_FILE="${PLAN_DIR}/.mode"
|
|
137
|
-
if [ ! -f "${MODE_FILE}" ] || ! grep -q "gate" "${MODE_FILE}" 2>/dev/null; then
|
|
138
|
-
advisory_report
|
|
139
|
-
exit 0
|
|
140
|
-
fi
|
|
141
|
-
|
|
142
|
-
# Guard 3: stop_hook_active. Read the Stop hook JSON from stdin only when fd 0
|
|
143
|
-
# is not a TTY (see header). A true value means we are already inside a forced
|
|
144
|
-
# continuation; allow the stop to avoid runaway recursion.
|
|
145
|
-
STDIN_JSON=""
|
|
146
|
-
if [ ! -t 0 ]; then
|
|
147
|
-
STDIN_JSON="$(cat 2>/dev/null)"
|
|
148
|
-
fi
|
|
149
|
-
# Anchor on the VALUE: "stop_hook_active" immediately followed (allowing
|
|
150
|
-
# whitespace and the colon) by true. A bare glob like *stop_hook_active*true*
|
|
151
|
-
# false-positives on '{"stop_hook_active": false, "other": true}', which would
|
|
152
|
-
# silently disable the gate. Newlines are collapsed so the match works whether
|
|
153
|
-
# the payload is pretty-printed or single-line.
|
|
154
|
-
STOP_HOOK_ACTIVE="$(
|
|
155
|
-
printf '%s' "${STDIN_JSON}" \
|
|
156
|
-
| tr '\n' ' ' \
|
|
157
|
-
| sed -n 's/.*"stop_hook_active"[[:space:]]*:[[:space:]]*true.*/FOUND/p'
|
|
158
|
-
)"
|
|
159
|
-
if [ "${STOP_HOOK_ACTIVE}" = "FOUND" ]; then
|
|
160
|
-
advisory_report
|
|
161
|
-
exit 0
|
|
162
|
-
fi
|
|
163
|
-
|
|
164
|
-
# Guard 2: an in_progress phase must exist. Merely complete<total is a normal
|
|
165
|
-
# state and must NOT block (issue #178 lesson).
|
|
166
|
-
if [ "$IN_PROGRESS" -le 0 ]; then
|
|
167
|
-
advisory_report
|
|
168
|
-
exit 0
|
|
169
|
-
fi
|
|
170
|
-
|
|
171
|
-
# ledger_line_count: total lines across all <plan-dir>/ledger-*.jsonl files.
|
|
172
|
-
# Echoes a single integer (0 when no ledger files exist).
|
|
173
|
-
ledger_line_count() {
|
|
174
|
-
_total=0
|
|
175
|
-
for _lf in "${PLAN_DIR}"/ledger-*.jsonl; do
|
|
176
|
-
[ -f "${_lf}" ] || continue
|
|
177
|
-
_n="$(grep -c '' "${_lf}" 2>/dev/null || echo 0)"
|
|
178
|
-
_total=$((_total + _n))
|
|
179
|
-
done
|
|
180
|
-
printf "%s" "${_total}"
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
CAP="${PWF_GATE_CAP:-20}"
|
|
184
|
-
case "${CAP}" in
|
|
185
|
-
''|*[!0-9]*) CAP=20 ;;
|
|
186
|
-
esac
|
|
187
|
-
|
|
188
|
-
BLOCKS_FILE="${PLAN_DIR}/.stop_blocks"
|
|
189
|
-
BLOCKS="$(cat "${BLOCKS_FILE}" 2>/dev/null || echo 0)"
|
|
190
|
-
case "${BLOCKS}" in
|
|
191
|
-
''|*[!0-9]*) BLOCKS=0 ;;
|
|
192
|
-
esac
|
|
193
|
-
|
|
194
|
-
LEDGER_FILE="${PLAN_DIR}/.gate_last_ledger"
|
|
195
|
-
LEDGER_PREV="$(cat "${LEDGER_FILE}" 2>/dev/null || echo 0)"
|
|
196
|
-
case "${LEDGER_PREV}" in
|
|
197
|
-
''|*[!0-9]*) LEDGER_PREV=0 ;;
|
|
198
|
-
esac
|
|
199
|
-
LEDGER_NOW="$(ledger_line_count)"
|
|
200
|
-
|
|
201
|
-
# Guard 4: block-count cap. At or over the cap, allow the stop.
|
|
202
|
-
if [ "${BLOCKS}" -ge "${CAP}" ]; then
|
|
203
|
-
advisory_report
|
|
204
|
-
echo "[planning-with-files] gate cap reached ($BLOCKS/$CAP) — allowing stop."
|
|
205
|
-
exit 0
|
|
206
|
-
fi
|
|
207
|
-
|
|
208
|
-
# Guard 5: stall detection. If we have blocked before (BLOCKS > 0) and the
|
|
209
|
-
# ledger line count has not advanced since the last block, nothing progressed:
|
|
210
|
-
# allow the stop instead of looping.
|
|
211
|
-
if [ "${BLOCKS}" -gt 0 ] && [ "${LEDGER_NOW}" -eq "${LEDGER_PREV}" ]; then
|
|
212
|
-
advisory_report
|
|
213
|
-
echo "[planning-with-files] no progress since last gate block — allowing stop."
|
|
214
|
-
exit 0
|
|
215
|
-
fi
|
|
216
|
-
|
|
217
|
-
# All guards passed: block the stop.
|
|
218
|
-
# json_escape: escape a string for safe inclusion in a JSON string literal.
|
|
219
|
-
# Escapes backslash and double-quote, then neutralizes every bare control
|
|
220
|
-
# character JSON forbids (0x01-0x1F) by mapping it to a space. A phase heading
|
|
221
|
-
# may carry a literal tab or other control byte; left raw it produces invalid
|
|
222
|
-
# JSON ("Bad control character in string literal") that the Stop hook rejects.
|
|
223
|
-
json_escape() {
|
|
224
|
-
printf "%s" "$1" \
|
|
225
|
-
| sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' \
|
|
226
|
-
| tr '\001-\037' ' '
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
# first_in_progress_phase: heading text of the first phase whose Status is
|
|
230
|
-
# in_progress. Reads the plan top-to-bottom, remembers the most recent
|
|
231
|
-
# "### " heading, and prints it (with the "### " prefix stripped) at the first
|
|
232
|
-
# in_progress status line. Plain text only — no plan body beyond the heading.
|
|
233
|
-
first_in_progress_phase() {
|
|
234
|
-
awk '
|
|
235
|
-
/^### / { heading = substr($0, 5); next }
|
|
236
|
-
/\*\*Status:\*\* in_progress/ { print heading; exit }
|
|
237
|
-
/\[in_progress\]/ { print heading; exit }
|
|
238
|
-
' "$PLAN_FILE"
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
PHASE_NAME="$(first_in_progress_phase)"
|
|
242
|
-
if [ -z "${PHASE_NAME}" ]; then
|
|
243
|
-
PHASE_NAME="unknown phase"
|
|
244
|
-
fi
|
|
245
|
-
PHASE_ESCAPED="$(json_escape "${PHASE_NAME}")"
|
|
246
|
-
|
|
247
|
-
NEW_BLOCKS=$((BLOCKS + 1))
|
|
248
|
-
printf "%s\n" "${NEW_BLOCKS}" > "${BLOCKS_FILE}" 2>/dev/null || true
|
|
249
|
-
printf "%s\n" "${LEDGER_NOW}" > "${LEDGER_FILE}" 2>/dev/null || true
|
|
250
|
-
|
|
251
|
-
printf '{"decision":"block","reason":"[planning-with-files] Gated plan incomplete: phase '\''%s'\'' is in_progress (%s/%s complete, gate block %s/%s). Finish or update the plan, then stop."}\n' \
|
|
252
|
-
"${PHASE_ESCAPED}" "${COMPLETE}" "${TOTAL}" "${NEW_BLOCKS}" "${CAP}"
|
|
253
|
-
exit 0
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Check if all phases in task_plan.md are complete
|
|
3
|
+
# Default invocation: advisory echo, always exits 0 (Stop hook status report).
|
|
4
|
+
# With --gate: deliberate completion gate, opt-in per plan via <plan-dir>/.mode.
|
|
5
|
+
# Used by Stop hook to report task completion status.
|
|
6
|
+
#
|
|
7
|
+
# Plan-file resolution (v2.40+):
|
|
8
|
+
# 1. $1 (explicit path) — first non-flag positional argument
|
|
9
|
+
# 2. resolve-plan-dir.sh: $PLAN_ID env → .planning/.active_plan → newest mtime
|
|
10
|
+
# 3. Legacy ./task_plan.md
|
|
11
|
+
#
|
|
12
|
+
# This restores slug-mode parity: the Stop hook and any caller invoking with
|
|
13
|
+
# zero args now respects the active plan dir instead of silently defaulting to
|
|
14
|
+
# the legacy root path.
|
|
15
|
+
#
|
|
16
|
+
# Gate mode (v3, --gate flag):
|
|
17
|
+
# The gate is OFF unless ALL of these hold (design "Gate decision table"):
|
|
18
|
+
# 1. <plan-dir>/.mode exists and contains "gate" (explicit opt-in)
|
|
19
|
+
# 2. an in_progress phase exists (not merely complete<total)
|
|
20
|
+
# 3. the Stop hook input JSON on stdin does not set stop_hook_active=true
|
|
21
|
+
# 4. the block counter (<plan-dir>/.stop_blocks) is below cap (PWF_GATE_CAP, default 20)
|
|
22
|
+
# 5. the ledger advanced since the last block (stall → allow stop)
|
|
23
|
+
# When all hold, it emits a single-line block-decision JSON on stdout and
|
|
24
|
+
# exits 0. Otherwise it falls back to advisory output and exits 0.
|
|
25
|
+
# Without --gate, or in non-gated mode, behavior is byte-equivalent to v2.43.
|
|
26
|
+
#
|
|
27
|
+
# Stdin handling: the Claude Code Stop hook pipes a JSON payload on stdin. To
|
|
28
|
+
# avoid hanging when nothing is piped, stdin is read ONLY when fd 0 is not a
|
|
29
|
+
# TTY ([ -t 0 ]). Hook-piped input is EOF-terminated, so the read returns; an
|
|
30
|
+
# interactive terminal (TTY) is skipped entirely. No data on stdin is treated
|
|
31
|
+
# as stop_hook_active=false.
|
|
32
|
+
|
|
33
|
+
# issue #195: per-invocation opt-out (PLANNING_DISABLED=1) for one-shot/CI
|
|
34
|
+
# sessions that share a cwd with a plan but never opted into it.
|
|
35
|
+
[ "${PLANNING_DISABLED:-}" = "1" ] && exit 0
|
|
36
|
+
|
|
37
|
+
GATE=0
|
|
38
|
+
PLAN_FILE=""
|
|
39
|
+
for _arg in "$@"; do
|
|
40
|
+
case "$_arg" in
|
|
41
|
+
--gate) GATE=1 ;;
|
|
42
|
+
*)
|
|
43
|
+
if [ -z "$PLAN_FILE" ]; then
|
|
44
|
+
PLAN_FILE="$_arg"
|
|
45
|
+
fi
|
|
46
|
+
;;
|
|
47
|
+
esac
|
|
48
|
+
done
|
|
49
|
+
|
|
50
|
+
PLAN_DIR=""
|
|
51
|
+
if [ -n "${PLAN_FILE}" ]; then
|
|
52
|
+
PLAN_DIR="$(dirname "${PLAN_FILE}")"
|
|
53
|
+
else
|
|
54
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd 2>/dev/null)" || SCRIPT_DIR="."
|
|
55
|
+
RESOLVER="${SCRIPT_DIR}/resolve-plan-dir.sh"
|
|
56
|
+
RESOLVED_DIR=""
|
|
57
|
+
if [ -f "${RESOLVER}" ]; then
|
|
58
|
+
RESOLVED_DIR="$(sh "${RESOLVER}" 2>/dev/null)"
|
|
59
|
+
fi
|
|
60
|
+
if [ -n "${RESOLVED_DIR}" ] && [ -f "${RESOLVED_DIR}/task_plan.md" ]; then
|
|
61
|
+
PLAN_FILE="${RESOLVED_DIR}/task_plan.md"
|
|
62
|
+
PLAN_DIR="${RESOLVED_DIR}"
|
|
63
|
+
else
|
|
64
|
+
PLAN_FILE="task_plan.md"
|
|
65
|
+
PLAN_DIR="."
|
|
66
|
+
fi
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
if [ ! -f "$PLAN_FILE" ]; then
|
|
70
|
+
echo "[planning-with-files] No task_plan.md found — no active planning session."
|
|
71
|
+
exit 0
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# Count total phases
|
|
75
|
+
TOTAL=$(grep -c "### Phase" "$PLAN_FILE" || true)
|
|
76
|
+
|
|
77
|
+
# Count both formats per field and keep the larger of the two. A plan may mix
|
|
78
|
+
# '**Status:** pending' on one phase with '[in_progress]' on another; counting
|
|
79
|
+
# only the primary format (and falling back to inline ONLY when all three
|
|
80
|
+
# primaries are zero) lost the inline count and let an in_progress plan slip
|
|
81
|
+
# past the gate. Per-field max preserves the legacy single-format result
|
|
82
|
+
# (the other format contributes 0) while catching mixed plans.
|
|
83
|
+
COMPLETE_PRIMARY=$(grep -cF "**Status:** complete" "$PLAN_FILE" || true)
|
|
84
|
+
IN_PROGRESS_PRIMARY=$(grep -cF "**Status:** in_progress" "$PLAN_FILE" || true)
|
|
85
|
+
PENDING_PRIMARY=$(grep -cF "**Status:** pending" "$PLAN_FILE" || true)
|
|
86
|
+
|
|
87
|
+
COMPLETE_INLINE=$(grep -c "\[complete\]" "$PLAN_FILE" || true)
|
|
88
|
+
IN_PROGRESS_INLINE=$(grep -c "\[in_progress\]" "$PLAN_FILE" || true)
|
|
89
|
+
PENDING_INLINE=$(grep -c "\[pending\]" "$PLAN_FILE" || true)
|
|
90
|
+
|
|
91
|
+
: "${COMPLETE_PRIMARY:=0}"; : "${IN_PROGRESS_PRIMARY:=0}"; : "${PENDING_PRIMARY:=0}"
|
|
92
|
+
: "${COMPLETE_INLINE:=0}"; : "${IN_PROGRESS_INLINE:=0}"; : "${PENDING_INLINE:=0}"
|
|
93
|
+
|
|
94
|
+
if [ "$COMPLETE_INLINE" -gt "$COMPLETE_PRIMARY" ]; then COMPLETE="$COMPLETE_INLINE"; else COMPLETE="$COMPLETE_PRIMARY"; fi
|
|
95
|
+
if [ "$IN_PROGRESS_INLINE" -gt "$IN_PROGRESS_PRIMARY" ]; then IN_PROGRESS="$IN_PROGRESS_INLINE"; else IN_PROGRESS="$IN_PROGRESS_PRIMARY"; fi
|
|
96
|
+
if [ "$PENDING_INLINE" -gt "$PENDING_PRIMARY" ]; then PENDING="$PENDING_INLINE"; else PENDING="$PENDING_PRIMARY"; fi
|
|
97
|
+
|
|
98
|
+
# Default to 0 if empty
|
|
99
|
+
: "${TOTAL:=0}"
|
|
100
|
+
: "${COMPLETE:=0}"
|
|
101
|
+
: "${IN_PROGRESS:=0}"
|
|
102
|
+
: "${PENDING:=0}"
|
|
103
|
+
|
|
104
|
+
# issue #191: no "### Phase" headings -> not a phase-structured plan. Report
|
|
105
|
+
# nothing rather than a false "0/0 phases complete" status. With TOTAL=0 the
|
|
106
|
+
# gate can never legitimately block (IN_PROGRESS is also 0), so exit is safe.
|
|
107
|
+
if [ "$TOTAL" -eq 0 ]; then
|
|
108
|
+
exit 0
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# advisory_report: the v2.43 status echo. Always exit 0 after calling.
|
|
112
|
+
advisory_report() {
|
|
113
|
+
if [ "$COMPLETE" -eq "$TOTAL" ] && [ "$TOTAL" -gt 0 ]; then
|
|
114
|
+
echo "[planning-with-files] ALL PHASES COMPLETE ($COMPLETE/$TOTAL). If the user has additional work, add new phases to task_plan.md before starting."
|
|
115
|
+
else
|
|
116
|
+
echo "[planning-with-files] Task in progress ($COMPLETE/$TOTAL phases complete). Update progress.md before stopping."
|
|
117
|
+
if [ "$IN_PROGRESS" -gt 0 ]; then
|
|
118
|
+
echo "[planning-with-files] $IN_PROGRESS phase(s) still in progress."
|
|
119
|
+
fi
|
|
120
|
+
if [ "$PENDING" -gt 0 ]; then
|
|
121
|
+
echo "[planning-with-files] $PENDING phase(s) pending."
|
|
122
|
+
fi
|
|
123
|
+
fi
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# ---- Default (advisory) path: byte-equivalent to v2.43 ----
|
|
127
|
+
if [ "$GATE" -ne 1 ]; then
|
|
128
|
+
advisory_report
|
|
129
|
+
exit 0
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
# ---- Gate path (--gate). Resolves to advisory unless every guard says block. ----
|
|
133
|
+
|
|
134
|
+
# Guard 1: gated mode. The .mode file must contain "gate". Absent or other
|
|
135
|
+
# content means advisory mode (legacy behavior preserved).
|
|
136
|
+
MODE_FILE="${PLAN_DIR}/.mode"
|
|
137
|
+
if [ ! -f "${MODE_FILE}" ] || ! grep -q "gate" "${MODE_FILE}" 2>/dev/null; then
|
|
138
|
+
advisory_report
|
|
139
|
+
exit 0
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
# Guard 3: stop_hook_active. Read the Stop hook JSON from stdin only when fd 0
|
|
143
|
+
# is not a TTY (see header). A true value means we are already inside a forced
|
|
144
|
+
# continuation; allow the stop to avoid runaway recursion.
|
|
145
|
+
STDIN_JSON=""
|
|
146
|
+
if [ ! -t 0 ]; then
|
|
147
|
+
STDIN_JSON="$(cat 2>/dev/null)"
|
|
148
|
+
fi
|
|
149
|
+
# Anchor on the VALUE: "stop_hook_active" immediately followed (allowing
|
|
150
|
+
# whitespace and the colon) by true. A bare glob like *stop_hook_active*true*
|
|
151
|
+
# false-positives on '{"stop_hook_active": false, "other": true}', which would
|
|
152
|
+
# silently disable the gate. Newlines are collapsed so the match works whether
|
|
153
|
+
# the payload is pretty-printed or single-line.
|
|
154
|
+
STOP_HOOK_ACTIVE="$(
|
|
155
|
+
printf '%s' "${STDIN_JSON}" \
|
|
156
|
+
| tr '\n' ' ' \
|
|
157
|
+
| sed -n 's/.*"stop_hook_active"[[:space:]]*:[[:space:]]*true.*/FOUND/p'
|
|
158
|
+
)"
|
|
159
|
+
if [ "${STOP_HOOK_ACTIVE}" = "FOUND" ]; then
|
|
160
|
+
advisory_report
|
|
161
|
+
exit 0
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
# Guard 2: an in_progress phase must exist. Merely complete<total is a normal
|
|
165
|
+
# state and must NOT block (issue #178 lesson).
|
|
166
|
+
if [ "$IN_PROGRESS" -le 0 ]; then
|
|
167
|
+
advisory_report
|
|
168
|
+
exit 0
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
# ledger_line_count: total lines across all <plan-dir>/ledger-*.jsonl files.
|
|
172
|
+
# Echoes a single integer (0 when no ledger files exist).
|
|
173
|
+
ledger_line_count() {
|
|
174
|
+
_total=0
|
|
175
|
+
for _lf in "${PLAN_DIR}"/ledger-*.jsonl; do
|
|
176
|
+
[ -f "${_lf}" ] || continue
|
|
177
|
+
_n="$(grep -c '' "${_lf}" 2>/dev/null || echo 0)"
|
|
178
|
+
_total=$((_total + _n))
|
|
179
|
+
done
|
|
180
|
+
printf "%s" "${_total}"
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
CAP="${PWF_GATE_CAP:-20}"
|
|
184
|
+
case "${CAP}" in
|
|
185
|
+
''|*[!0-9]*) CAP=20 ;;
|
|
186
|
+
esac
|
|
187
|
+
|
|
188
|
+
BLOCKS_FILE="${PLAN_DIR}/.stop_blocks"
|
|
189
|
+
BLOCKS="$(cat "${BLOCKS_FILE}" 2>/dev/null || echo 0)"
|
|
190
|
+
case "${BLOCKS}" in
|
|
191
|
+
''|*[!0-9]*) BLOCKS=0 ;;
|
|
192
|
+
esac
|
|
193
|
+
|
|
194
|
+
LEDGER_FILE="${PLAN_DIR}/.gate_last_ledger"
|
|
195
|
+
LEDGER_PREV="$(cat "${LEDGER_FILE}" 2>/dev/null || echo 0)"
|
|
196
|
+
case "${LEDGER_PREV}" in
|
|
197
|
+
''|*[!0-9]*) LEDGER_PREV=0 ;;
|
|
198
|
+
esac
|
|
199
|
+
LEDGER_NOW="$(ledger_line_count)"
|
|
200
|
+
|
|
201
|
+
# Guard 4: block-count cap. At or over the cap, allow the stop.
|
|
202
|
+
if [ "${BLOCKS}" -ge "${CAP}" ]; then
|
|
203
|
+
advisory_report
|
|
204
|
+
echo "[planning-with-files] gate cap reached ($BLOCKS/$CAP) — allowing stop."
|
|
205
|
+
exit 0
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
# Guard 5: stall detection. If we have blocked before (BLOCKS > 0) and the
|
|
209
|
+
# ledger line count has not advanced since the last block, nothing progressed:
|
|
210
|
+
# allow the stop instead of looping.
|
|
211
|
+
if [ "${BLOCKS}" -gt 0 ] && [ "${LEDGER_NOW}" -eq "${LEDGER_PREV}" ]; then
|
|
212
|
+
advisory_report
|
|
213
|
+
echo "[planning-with-files] no progress since last gate block — allowing stop."
|
|
214
|
+
exit 0
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
# All guards passed: block the stop.
|
|
218
|
+
# json_escape: escape a string for safe inclusion in a JSON string literal.
|
|
219
|
+
# Escapes backslash and double-quote, then neutralizes every bare control
|
|
220
|
+
# character JSON forbids (0x01-0x1F) by mapping it to a space. A phase heading
|
|
221
|
+
# may carry a literal tab or other control byte; left raw it produces invalid
|
|
222
|
+
# JSON ("Bad control character in string literal") that the Stop hook rejects.
|
|
223
|
+
json_escape() {
|
|
224
|
+
printf "%s" "$1" \
|
|
225
|
+
| sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' \
|
|
226
|
+
| tr '\001-\037' ' '
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
# first_in_progress_phase: heading text of the first phase whose Status is
|
|
230
|
+
# in_progress. Reads the plan top-to-bottom, remembers the most recent
|
|
231
|
+
# "### " heading, and prints it (with the "### " prefix stripped) at the first
|
|
232
|
+
# in_progress status line. Plain text only — no plan body beyond the heading.
|
|
233
|
+
first_in_progress_phase() {
|
|
234
|
+
awk '
|
|
235
|
+
/^### / { heading = substr($0, 5); next }
|
|
236
|
+
/\*\*Status:\*\* in_progress/ { print heading; exit }
|
|
237
|
+
/\[in_progress\]/ { print heading; exit }
|
|
238
|
+
' "$PLAN_FILE"
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
PHASE_NAME="$(first_in_progress_phase)"
|
|
242
|
+
if [ -z "${PHASE_NAME}" ]; then
|
|
243
|
+
PHASE_NAME="unknown phase"
|
|
244
|
+
fi
|
|
245
|
+
PHASE_ESCAPED="$(json_escape "${PHASE_NAME}")"
|
|
246
|
+
|
|
247
|
+
NEW_BLOCKS=$((BLOCKS + 1))
|
|
248
|
+
printf "%s\n" "${NEW_BLOCKS}" > "${BLOCKS_FILE}" 2>/dev/null || true
|
|
249
|
+
printf "%s\n" "${LEDGER_NOW}" > "${LEDGER_FILE}" 2>/dev/null || true
|
|
250
|
+
|
|
251
|
+
printf '{"decision":"block","reason":"[planning-with-files] Gated plan incomplete: phase '\''%s'\'' is in_progress (%s/%s complete, gate block %s/%s). Finish or update the plan, then stop."}\n' \
|
|
252
|
+
"${PHASE_ESCAPED}" "${COMPLETE}" "${TOTAL}" "${NEW_BLOCKS}" "${CAP}"
|
|
253
|
+
exit 0
|
package/scripts/gate-stop.sh
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# planning-with-files: Stop-hook dispatcher for the v3 completion gate.
|
|
3
|
-
#
|
|
4
|
-
# Thin wrapper: discover check-complete.sh (sibling first, then the known
|
|
5
|
-
# install paths) and run it with --gate, passing the Stop hook's stdin JSON
|
|
6
|
-
# through so check-complete can read stop_hook_active and apply the gate
|
|
7
|
-
# decision table. check-complete in --gate mode is the host-aware termination
|
|
8
|
-
# oracle (W1A); without --gate it keeps the legacy advisory echo behavior.
|
|
9
|
-
#
|
|
10
|
-
# Always exits with check-complete's exit code. In legacy mode (no .mode file)
|
|
11
|
-
# check-complete --gate never blocks, so the Stop event proceeds exactly as v2.
|
|
12
|
-
|
|
13
|
-
set -u
|
|
14
|
-
|
|
15
|
-
# issue #195: per-invocation opt-out (PLANNING_DISABLED=1) for one-shot/CI
|
|
16
|
-
# sessions that share a cwd with a plan but never opted into it.
|
|
17
|
-
[ "${PLANNING_DISABLED:-}" = "1" ] && exit 0
|
|
18
|
-
|
|
19
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd 2>/dev/null)" || SCRIPT_DIR="."
|
|
20
|
-
|
|
21
|
-
TARGET="${SCRIPT_DIR}/check-complete.sh"
|
|
22
|
-
if [ ! -f "$TARGET" ] && [ -n "${HOME:-}" ]; then
|
|
23
|
-
# ${HOME:-} keeps set -u from aborting the substitution in CI/Docker images
|
|
24
|
-
# where HOME is unset; without the guard the shell exits before the gate runs.
|
|
25
|
-
TARGET=$(ls "${HOME}/.claude/skills/planning-with-files/scripts/check-complete.sh" \
|
|
26
|
-
"${HOME}/.claude/plugins/marketplaces/planning-with-files/scripts/check-complete.sh" \
|
|
27
|
-
2>/dev/null | head -1)
|
|
28
|
-
fi
|
|
29
|
-
|
|
30
|
-
[ -n "${TARGET:-}" ] && [ -f "$TARGET" ] || exit 0
|
|
31
|
-
|
|
32
|
-
sh "$TARGET" --gate
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# planning-with-files: Stop-hook dispatcher for the v3 completion gate.
|
|
3
|
+
#
|
|
4
|
+
# Thin wrapper: discover check-complete.sh (sibling first, then the known
|
|
5
|
+
# install paths) and run it with --gate, passing the Stop hook's stdin JSON
|
|
6
|
+
# through so check-complete can read stop_hook_active and apply the gate
|
|
7
|
+
# decision table. check-complete in --gate mode is the host-aware termination
|
|
8
|
+
# oracle (W1A); without --gate it keeps the legacy advisory echo behavior.
|
|
9
|
+
#
|
|
10
|
+
# Always exits with check-complete's exit code. In legacy mode (no .mode file)
|
|
11
|
+
# check-complete --gate never blocks, so the Stop event proceeds exactly as v2.
|
|
12
|
+
|
|
13
|
+
set -u
|
|
14
|
+
|
|
15
|
+
# issue #195: per-invocation opt-out (PLANNING_DISABLED=1) for one-shot/CI
|
|
16
|
+
# sessions that share a cwd with a plan but never opted into it.
|
|
17
|
+
[ "${PLANNING_DISABLED:-}" = "1" ] && exit 0
|
|
18
|
+
|
|
19
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd 2>/dev/null)" || SCRIPT_DIR="."
|
|
20
|
+
|
|
21
|
+
TARGET="${SCRIPT_DIR}/check-complete.sh"
|
|
22
|
+
if [ ! -f "$TARGET" ] && [ -n "${HOME:-}" ]; then
|
|
23
|
+
# ${HOME:-} keeps set -u from aborting the substitution in CI/Docker images
|
|
24
|
+
# where HOME is unset; without the guard the shell exits before the gate runs.
|
|
25
|
+
TARGET=$(ls "${HOME}/.claude/skills/planning-with-files/scripts/check-complete.sh" \
|
|
26
|
+
"${HOME}/.claude/plugins/marketplaces/planning-with-files/scripts/check-complete.sh" \
|
|
27
|
+
2>/dev/null | head -1)
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
[ -n "${TARGET:-}" ] && [ -f "$TARGET" ] || exit 0
|
|
31
|
+
|
|
32
|
+
sh "$TARGET" --gate
|