devlog-tracker 1.0.0 → 1.1.1
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/README.md +7 -5
- package/README.zh-TW.md +7 -5
- package/codex/hooks/on-interrupt.sh +1 -0
- package/codex/hooks/on-pre-tool.sh +9 -7
- package/codex/hooks/on-session-end.sh +1 -0
- package/codex/hooks/on-session-start.sh +1 -0
- package/codex/hooks/on-stop.sh +1 -0
- package/codex/hooks/on-subagent-start.sh +1 -0
- package/codex/hooks/on-user-prompt-submit.sh +1 -0
- package/codex/hooks/test-adapters.sh +29 -19
- package/commands/clean.md +2 -2
- package/commands/continue.md +1 -1
- package/commands/keep-all.md +4 -4
- package/commands/keep.md +4 -4
- package/commands/migrate.md +2 -2
- package/commands/span.md +4 -4
- package/commands/status.md +1 -1
- package/core/scripts/await-open.sh +4 -4
- package/core/scripts/clean-devlog.sh +11 -5
- package/core/scripts/close-open-round.sh +0 -4
- package/core/scripts/devlog-md.sh +89 -8
- package/core/scripts/devlog-path.sh +128 -10
- package/core/scripts/enforce-devlog.sh +35 -19
- package/core/scripts/keep-all.js +4 -3
- package/core/scripts/keep-all.sh +2 -3
- package/core/scripts/keep-all.test.js +31 -0
- package/core/scripts/keep-move.sh +13 -9
- package/core/scripts/migrate-handoff.sh +2 -2
- package/core/scripts/on-tool-failure.sh +6 -1
- package/core/scripts/pause-devlog.sh +5 -3
- package/core/scripts/round-start.sh +31 -20
- package/core/scripts/segment-watch-set.sh +12 -8
- package/core/scripts/segment-watch.sh +3 -6
- package/core/scripts/session-start-devlog.sh +11 -5
- package/core/scripts/span-close.sh +7 -3
- package/core/scripts/span-open.sh +4 -4
- package/core/scripts/status-devlog.sh +6 -6
- package/core/scripts/tests/test-await-open.sh +9 -0
- package/core/scripts/tests/test-devlog-md.sh +76 -15
- package/core/scripts/tests/test-devlog-path.sh +126 -0
- package/core/scripts/tests/test-multi-platform.sh +286 -0
- package/core/scripts/tests/test-segment-watch-set.sh +10 -0
- package/core/scripts/tests/test-session-start-devlog.sh +15 -0
- package/cursor/hooks/on-pre-tool.sh +1 -0
- package/cursor/hooks/on-session-end.sh +1 -0
- package/cursor/hooks/on-session-start.sh +1 -0
- package/cursor/hooks/on-stop.sh +1 -0
- package/cursor/hooks/on-submit-prompt.sh +1 -0
- package/cursor/hooks/on-tool-failure.sh +1 -0
- package/cursor/hooks/test-adapters.sh +19 -16
- package/package.json +1 -1
- package/skills/devlog-tracker/SKILL.md +23 -18
- package/skills/devlog-tracker/references/contract.md +2 -2
- package/skills/devlog-tracker/references/reply-fold.md +8 -8
- package/skills/devlog-tracker/references/round-segments.md +2 -2
- package/skills/devlog-tracker/references/span-mode.md +10 -3
|
@@ -22,10 +22,18 @@ _devlog_fence_nofence() {
|
|
|
22
22
|
if [ $((count % 2)) -eq 0 ]; then printf '0\n'; else printf '1\n'; fi
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
# Prints "<line> <round>" per round heading outside fences. With platform $2,
|
|
26
|
+
# only rounds it owns: a heading ending in " · <platform>" (lowercase word)
|
|
27
|
+
# belongs to it; any other heading (every pre-upgrade round) belongs to claude.
|
|
25
28
|
devlog_list_round_starts() {
|
|
26
|
-
awk '
|
|
29
|
+
LC_ALL=C awk -v p="${2:-}" '
|
|
27
30
|
/^[ \t]*```/ { fence = !fence; next }
|
|
28
31
|
!fence && /^## Round [0-9]+/ {
|
|
32
|
+
if (p != "") {
|
|
33
|
+
owner = $0
|
|
34
|
+
if (!(sub(/.* · /, "", owner) && owner ~ /^[a-z]+$/)) owner = "claude"
|
|
35
|
+
if (owner != p) next
|
|
36
|
+
}
|
|
29
37
|
round = $0
|
|
30
38
|
sub(/^## Round /, "", round)
|
|
31
39
|
sub(/[^0-9].*$/, "", round)
|
|
@@ -34,6 +42,24 @@ devlog_list_round_starts() {
|
|
|
34
42
|
' "$1"
|
|
35
43
|
}
|
|
36
44
|
|
|
45
|
+
devlog_list_round_starts_of() {
|
|
46
|
+
devlog_list_round_starts "$1" "$2"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Start line of the last "## Round $2" heading; with platform $3, only its own.
|
|
50
|
+
devlog_round_start_by_number() {
|
|
51
|
+
devlog_list_round_starts "$1" "${3:-}" | awk -v n="$2" '$2 == n { s = $1 } END { if (s) print s }'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Reply Fold target for platform $3: its own "## Round $2" first, else any
|
|
55
|
+
# owner's (a pre-claim heading it wrote without a suffix).
|
|
56
|
+
devlog_fold_round_start() {
|
|
57
|
+
local s
|
|
58
|
+
s="$(devlog_round_start_by_number "$1" "$2" "$3")"
|
|
59
|
+
[ -n "$s" ] || s="$(devlog_round_start_by_number "$1" "$2")"
|
|
60
|
+
[ -z "$s" ] || printf '%s\n' "$s"
|
|
61
|
+
}
|
|
62
|
+
|
|
37
63
|
devlog_block_end() {
|
|
38
64
|
awk -v start="$2" '
|
|
39
65
|
NR < start { next }
|
|
@@ -178,6 +204,59 @@ devlog_origin_of() {
|
|
|
178
204
|
sed -n '1s/^<!-- devlog-origin: \(.*\) -->$/\1/p' "$1"
|
|
179
205
|
}
|
|
180
206
|
|
|
207
|
+
# Span Mode rounds are numbered by the LLM, not reserved by round-start.sh.
|
|
208
|
+
# So before a merge (caller holds the lock), every "## Round N" heading in
|
|
209
|
+
# $2 whose N is already a round in $1, reserved by another platform's open
|
|
210
|
+
# round on $1, or repeated within $2 gets the next free number; a
|
|
211
|
+
# non-Claude heading without an owner suffix gains " · <platform>". The
|
|
212
|
+
# caller's own reserved round passes through unchanged. Leaves $2 alone on
|
|
213
|
+
# any error.
|
|
214
|
+
_devlog_renumber_current() {
|
|
215
|
+
local devlog="$1" current="$2" p="${DEVLOG_PLATFORM:-claude}" rows="" own="" reserved=""
|
|
216
|
+
if [ -n "${DEVLOG_DIR:-}" ] && type devlog_open_rounds >/dev/null 2>&1; then
|
|
217
|
+
rows="$(devlog_open_rounds | awk -F '\t' -v f="${devlog##*/}" '($3 == f || $3 == "") && $2 ~ /^[0-9]+$/')"
|
|
218
|
+
own="$(printf '%s\n' "$rows" | awk -F '\t' -v p="$p" '$1 == p { print $2 }')"
|
|
219
|
+
reserved="$(printf '%s\n' "$rows" | awk -F '\t' -v p="$p" '$1 != p && $2 != "" { printf "%s ", $2 }')"
|
|
220
|
+
fi
|
|
221
|
+
LC_ALL=C awk -v p="$p" -v own="$own" -v reserved="$reserved" -v devlog="$devlog" '
|
|
222
|
+
function num(s) { sub(/^## Round /, "", s); sub(/[^0-9].*$/, "", s); return s + 0 }
|
|
223
|
+
BEGIN {
|
|
224
|
+
k = split(reserved, r, " ")
|
|
225
|
+
for (i = 1; i <= k; i++) { taken[r[i] + 0] = 1; if (r[i] + 0 > max) max = r[i] + 0 }
|
|
226
|
+
if (own != "") { taken[own + 0] = 1; if (own + 0 > max) max = own + 0 }
|
|
227
|
+
while ((getline line < devlog) > 0) {
|
|
228
|
+
if (line ~ /^[ \t]*```/) { f = !f; continue }
|
|
229
|
+
if (!f && line ~ /^## Round [0-9]+/) { x = num(line); taken[x] = 1; if (x > max) max = x }
|
|
230
|
+
}
|
|
231
|
+
close(devlog)
|
|
232
|
+
}
|
|
233
|
+
{ lines[NR] = $0 }
|
|
234
|
+
/^[ \t]*```/ { fence = !fence; next }
|
|
235
|
+
!fence && /^## Round [0-9]+/ { head[NR] = 1; x = num($0); if (x > max) max = x }
|
|
236
|
+
END {
|
|
237
|
+
next_n = max + 1
|
|
238
|
+
for (i = 1; i <= NR; i++) {
|
|
239
|
+
line = lines[i]
|
|
240
|
+
if (head[i] && own != "" && !own_seen && num(line) == own + 0) { own_seen = 1 }
|
|
241
|
+
else if (head[i]) {
|
|
242
|
+
x = num(line)
|
|
243
|
+
if (x in taken) {
|
|
244
|
+
rest = line; sub(/^## Round [0-9]+/, "", rest)
|
|
245
|
+
x = next_n++
|
|
246
|
+
line = "## Round " x rest
|
|
247
|
+
}
|
|
248
|
+
taken[x] = 1
|
|
249
|
+
owner = line
|
|
250
|
+
if (p != "claude" && !(sub(/.* · /, "", owner) && owner ~ /^[a-z]+$/)) line = line " · " p
|
|
251
|
+
}
|
|
252
|
+
print line
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
' "$current" 2>/dev/null >"$current.renum" && mv "$current.renum" "$current" 2>/dev/null
|
|
256
|
+
rm -f "$current.renum" 2>/dev/null
|
|
257
|
+
return 0
|
|
258
|
+
}
|
|
259
|
+
|
|
181
260
|
devlog_merge_round_current() {
|
|
182
261
|
# Appends $2's content onto $1 (one blank line separator, matching the
|
|
183
262
|
# existing "\n## Round N" append convention) and removes $2. No-op if $2
|
|
@@ -186,6 +265,7 @@ devlog_merge_round_current() {
|
|
|
186
265
|
# goes first.
|
|
187
266
|
local devlog="$1" current="$2"
|
|
188
267
|
[ -s "$current" ] || return 0
|
|
268
|
+
_devlog_renumber_current "$devlog" "$current"
|
|
189
269
|
{
|
|
190
270
|
[ -s "$devlog" ] || [ -z "${DEVLOG_ORIGIN:-}" ] || devlog_origin_line "$DEVLOG_ORIGIN"
|
|
191
271
|
printf '\n'
|
|
@@ -195,12 +275,13 @@ devlog_merge_round_current() {
|
|
|
195
275
|
return 0
|
|
196
276
|
}
|
|
197
277
|
|
|
198
|
-
|
|
199
|
-
# Moves the
|
|
200
|
-
#
|
|
201
|
-
# has no round
|
|
278
|
+
devlog_reopen_round() {
|
|
279
|
+
# Moves the "## Round $3" block (wherever it sits in $1) into $2, removing
|
|
280
|
+
# it from $1. With platform $4, picks the block devlog_fold_round_start
|
|
281
|
+
# does. Returns 1 and touches neither file if $1 has no such round.
|
|
202
282
|
local devlog="$1" current="$2" start end
|
|
203
|
-
start="$(
|
|
283
|
+
if [ -n "${4:-}" ]; then start="$(devlog_fold_round_start "$devlog" "$3" "$4")"
|
|
284
|
+
else start="$(devlog_round_start_by_number "$devlog" "$3")"; fi
|
|
204
285
|
[ -n "$start" ] || return 1
|
|
205
286
|
end="$(devlog_block_end "$devlog" "$start")"
|
|
206
287
|
awk -v start="$start" -v end="$end" 'NR >= start && NR <= end' "$devlog" > "$current" 2>/dev/null || return 1
|
|
@@ -210,12 +291,12 @@ devlog_reopen_last_round() {
|
|
|
210
291
|
}
|
|
211
292
|
|
|
212
293
|
workspace_claim_state() {
|
|
213
|
-
local dir="$1" file="$2" start end status claimed live
|
|
294
|
+
local dir="$1" file="$2" platform="${3:-claude}" start end status claimed live
|
|
214
295
|
if ! type workspace_snapshot >/dev/null 2>&1; then
|
|
215
296
|
# shellcheck source=workspace-snapshot.sh
|
|
216
297
|
. "$_DEVLOG_MD_DIR/workspace-snapshot.sh"
|
|
217
298
|
fi
|
|
218
|
-
start="$(
|
|
299
|
+
start="$(devlog_list_round_starts_of "$file" "$platform" | awk 'END { print $1 }')"
|
|
219
300
|
[ -n "$start" ] || { printf 'NO_CLAIM\n'; return 0; }
|
|
220
301
|
end="$(devlog_block_end "$file" "$start")"
|
|
221
302
|
status="$(devlog_round_status "$file" "$start" "$end")"
|
|
@@ -8,6 +8,112 @@ _DEVLOG_PATH_DIR="$(cd "${BASH_SOURCE[0]%/*}" && pwd)"
|
|
|
8
8
|
. "$_DEVLOG_PATH_DIR/devlog-lock.sh"
|
|
9
9
|
# shellcheck source=devlog-md.sh
|
|
10
10
|
. "$_DEVLOG_PATH_DIR/devlog-md.sh"
|
|
11
|
+
# shellcheck source=json-field.sh
|
|
12
|
+
. "$_DEVLOG_PATH_DIR/json-field.sh"
|
|
13
|
+
|
|
14
|
+
# Every platform that can own per-round state. Claude keeps the pre-0.x
|
|
15
|
+
# un-suffixed names; the others add "@<platform>" (docs/design/
|
|
16
|
+
# multi-platform-concurrency.md). "@" never survives _devlog_sanitize_name,
|
|
17
|
+
# so a branch named "codex" cannot collide with platform Codex.
|
|
18
|
+
DEVLOG_PLATFORMS="claude codex cursor"
|
|
19
|
+
|
|
20
|
+
devlog_platform() {
|
|
21
|
+
case "${DEVLOG_PLATFORM:-}" in
|
|
22
|
+
codex|cursor) printf '%s\n' "$DEVLOG_PLATFORM" ;;
|
|
23
|
+
*) printf 'claude\n' ;;
|
|
24
|
+
esac
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# $1 stem (".round-current", "handoff.feat-x"), $2 extension (".md" or ""),
|
|
28
|
+
# $3 platform. Prints a basename.
|
|
29
|
+
devlog_platform_file() {
|
|
30
|
+
if [ "$3" = claude ]; then printf '%s%s\n' "$1" "$2"
|
|
31
|
+
else printf '%s@%s%s\n' "$1" "$3" "$2"; fi
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_devlog_set_state_paths() {
|
|
35
|
+
local p="$1" d="$DEVLOG_DIR"
|
|
36
|
+
# shellcheck disable=SC2034 # consumed by callers
|
|
37
|
+
ROUND_CURRENT="$d/$(devlog_platform_file .round-current .md "$p")"
|
|
38
|
+
# shellcheck disable=SC2034 # consumed by callers
|
|
39
|
+
ROUND_OPEN="$d/$(devlog_platform_file .round-open '' "$p")"
|
|
40
|
+
# shellcheck disable=SC2034 # consumed by callers
|
|
41
|
+
TURN_MARKER="$d/$(devlog_platform_file .turn-start '' "$p")"
|
|
42
|
+
# shellcheck disable=SC2034 # consumed by callers
|
|
43
|
+
SEGMENT_FILE="$d/$(devlog_platform_file .segment-state '' "$p")"
|
|
44
|
+
# shellcheck disable=SC2034 # consumed by callers
|
|
45
|
+
AWAITING_FILE="$d/$(devlog_platform_file .awaiting-reply '' "$p")"
|
|
46
|
+
# shellcheck disable=SC2034 # consumed by callers
|
|
47
|
+
INTERRUPTED_FLAG="$d/$(devlog_platform_file .interrupted '' "$p")"
|
|
48
|
+
# shellcheck disable=SC2034 # consumed by callers
|
|
49
|
+
SPAN_FILE="$d/$(devlog_platform_file .span-open '' "$p")"
|
|
50
|
+
# shellcheck disable=SC2034 # consumed by callers
|
|
51
|
+
MISMATCH_FILE="$d/$(devlog_platform_file .workspace-mismatch '' "$p")"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# First resolve after upgrade: before this change every platform wrote the
|
|
55
|
+
# un-suffixed names. The platform that resolves first most likely wrote
|
|
56
|
+
# them, so a non-Claude first resolver renames them to its own names.
|
|
57
|
+
# .segment-state is configuration (start-devlog.sh creates it), not round
|
|
58
|
+
# state, so it stays. Runs once; .platform-claimed stops it.
|
|
59
|
+
_devlog_claim_legacy_state() {
|
|
60
|
+
local p="$1" d="$DEVLOG_DIR" name f stem
|
|
61
|
+
[ -f "$d/.enabled" ] && [ ! -e "$d/.platform-claimed" ] || return 0
|
|
62
|
+
devlog_lock_acquire
|
|
63
|
+
if [ ! -e "$d/.platform-claimed" ]; then
|
|
64
|
+
if [ "$p" != claude ]; then
|
|
65
|
+
for name in .round-open .turn-start .awaiting-reply .interrupted .span-open .workspace-mismatch; do
|
|
66
|
+
[ -e "$d/$name" ] && [ ! -e "$d/$name@$p" ] && mv "$d/$name" "$d/$name@$p" 2>/dev/null
|
|
67
|
+
done
|
|
68
|
+
[ -e "$d/.round-current.md" ] && [ ! -e "$d/.round-current@$p.md" ] \
|
|
69
|
+
&& mv "$d/.round-current.md" "$d/.round-current@$p.md" 2>/dev/null
|
|
70
|
+
for f in "$d"/handoff.md "$d"/handoff.*.md; do
|
|
71
|
+
[ -f "$f" ] || continue
|
|
72
|
+
case "${f##*/}" in *@*) continue ;; esac
|
|
73
|
+
stem="${f%.md}"
|
|
74
|
+
[ -e "$stem@$p.md" ] || mv "$f" "$stem@$p.md" 2>/dev/null
|
|
75
|
+
done
|
|
76
|
+
fi
|
|
77
|
+
: > "$d/.platform-claimed" 2>/dev/null || true
|
|
78
|
+
fi
|
|
79
|
+
devlog_lock_release
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# One line per existing .round-open marker: platform, round, file field.
|
|
83
|
+
devlog_open_rounds() {
|
|
84
|
+
local q f
|
|
85
|
+
for q in $DEVLOG_PLATFORMS; do
|
|
86
|
+
f="$DEVLOG_DIR/$(devlog_platform_file .round-open '' "$q")"
|
|
87
|
+
[ -f "$f" ] || continue
|
|
88
|
+
printf '%s\t%s\t%s\n' "$q" "$(json_int_get "$f" round)" "$(json_str_get "$f" file)"
|
|
89
|
+
done
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Prints "<platform>\t<round>" of the only open round; nothing when none.
|
|
93
|
+
# Returns 2 (message on stderr) when two or more platforms have one open.
|
|
94
|
+
devlog_single_open_round() {
|
|
95
|
+
local rows count
|
|
96
|
+
rows="$(devlog_open_rounds)"
|
|
97
|
+
[ -n "$rows" ] || return 0
|
|
98
|
+
count="$(printf '%s\n' "$rows" | grep -c .)"
|
|
99
|
+
if [ "$count" -gt 1 ]; then
|
|
100
|
+
echo "其他平台還有進行中的輪次($(printf '%s\n' "$rows" | cut -f1 | grep -vxF "$(devlog_platform)" | paste -sd '、' -)),等它們收尾再執行。若該平台已不再使用,可先 pause 再 start 清掉它的標記。" >&2
|
|
101
|
+
return 2
|
|
102
|
+
fi
|
|
103
|
+
printf '%s\n' "$rows" | cut -f1,2
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# One line per other platform's non-empty handoff for this branch.
|
|
107
|
+
devlog_other_handoffs() {
|
|
108
|
+
local q f
|
|
109
|
+
for q in $DEVLOG_PLATFORMS; do
|
|
110
|
+
[ "$q" = "$DEVLOG_PLATFORM" ] && continue
|
|
111
|
+
f="$HANDOFF_STEM.md"
|
|
112
|
+
[ "$q" = claude ] || f="$HANDOFF_STEM@$q.md"
|
|
113
|
+
[ -s "$f" ] && printf '%s\t%s\n' "$q" "$f"
|
|
114
|
+
done
|
|
115
|
+
return 0
|
|
116
|
+
}
|
|
11
117
|
|
|
12
118
|
# Turns an arbitrary branch/worktree name into a safe devlog.<name>.md
|
|
13
119
|
# filename segment: anything outside [A-Za-z0-9._-] becomes '-', repeats
|
|
@@ -24,8 +130,9 @@ _devlog_sanitize_name() {
|
|
|
24
130
|
# work in progress). When $6 (an origin like "branch=feat/x") is given,
|
|
25
131
|
# the branch file starts with its origin marker. The project header,
|
|
26
132
|
# Checkpoints, and Kept/Lessons
|
|
27
|
-
# indexes always stay in $2. When rounds move,
|
|
28
|
-
# $5 too, since it snapshots that same
|
|
133
|
+
# indexes always stay in $2. When rounds move, every platform's handoff
|
|
134
|
+
# under stem $4 moves to stem $5 too, since it snapshots that same
|
|
135
|
+
# unfinished work. Caller holds the
|
|
29
136
|
# devlog lock.
|
|
30
137
|
_devlog_migrate_unfinished_tail() {
|
|
31
138
|
local dir="$1" src="$2" dst="$3" src_handoff="$4" dst_handoff="$5" origin="${6:-}"
|
|
@@ -82,9 +189,13 @@ _devlog_migrate_unfinished_tail() {
|
|
|
82
189
|
|
|
83
190
|
# Branch file first: if the second mv fails the tail is duplicated, never lost.
|
|
84
191
|
if mv "$dst.tmp" "$dst" 2>/dev/null && mv "$src.tmp" "$src" 2>/dev/null; then
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
192
|
+
local q suffix
|
|
193
|
+
for q in $DEVLOG_PLATFORMS; do
|
|
194
|
+
suffix="$(devlog_platform_file '' .md "$q")"
|
|
195
|
+
if [ -f "$src_handoff$suffix" ] && [ ! -f "$dst_handoff$suffix" ]; then
|
|
196
|
+
mv "$src_handoff$suffix" "$dst_handoff$suffix" 2>/dev/null || true
|
|
197
|
+
fi
|
|
198
|
+
done
|
|
88
199
|
fi
|
|
89
200
|
rm -f "$dst.tmp" "$src.tmp" 2>/dev/null || true
|
|
90
201
|
}
|
|
@@ -102,13 +213,19 @@ _devlog_migrate_unfinished_tail() {
|
|
|
102
213
|
# Also sets DEVLOG_ORIGIN ("branch=<raw name>" / "detached=<dir>", empty
|
|
103
214
|
# for devlog.md): the first line written into a new branch file records it
|
|
104
215
|
# as an origin marker (docs/design/keep-all.md "Origin marker").
|
|
216
|
+
# Call before taking the devlog lock: it may acquire/release the lock
|
|
217
|
+
# itself, and a nested acquire would reset the caller's lock state.
|
|
105
218
|
devlog_resolve_paths() {
|
|
106
219
|
local dir="${1:-.}"
|
|
107
220
|
DEVLOG_DIR="$dir/.devlog"
|
|
108
221
|
DEVLOG_FILE="$DEVLOG_DIR/devlog.md"
|
|
109
|
-
|
|
110
|
-
|
|
222
|
+
DEVLOG_PLATFORM="$(devlog_platform)"
|
|
223
|
+
HANDOFF_STEM="$DEVLOG_DIR/handoff"
|
|
111
224
|
DEVLOG_ORIGIN=""
|
|
225
|
+
_devlog_set_state_paths "$DEVLOG_PLATFORM"
|
|
226
|
+
_devlog_claim_legacy_state "$DEVLOG_PLATFORM"
|
|
227
|
+
# shellcheck disable=SC2034 # consumed by callers (e.g. enforce-devlog.sh), not used in this file
|
|
228
|
+
HANDOFF_FILE="$DEVLOG_DIR/$(devlog_platform_file handoff .md "$DEVLOG_PLATFORM")"
|
|
112
229
|
|
|
113
230
|
local branch raw name origin
|
|
114
231
|
branch="$(git -C "$dir" rev-parse --abbrev-ref HEAD 2>/dev/null || echo '')"
|
|
@@ -146,15 +263,16 @@ devlog_resolve_paths() {
|
|
|
146
263
|
esac
|
|
147
264
|
|
|
148
265
|
local resolved="$DEVLOG_DIR/devlog.$name.md"
|
|
149
|
-
local
|
|
266
|
+
local resolved_stem="$DEVLOG_DIR/handoff.$name"
|
|
150
267
|
if [ -f "$DEVLOG_DIR/.enabled" ] && [ ! -f "$resolved" ] && [ -s "$DEVLOG_FILE" ]; then
|
|
151
268
|
devlog_lock_acquire
|
|
152
269
|
[ -f "$resolved" ] || _devlog_migrate_unfinished_tail \
|
|
153
|
-
"$dir" "$DEVLOG_FILE" "$resolved" "$
|
|
270
|
+
"$dir" "$DEVLOG_FILE" "$resolved" "$HANDOFF_STEM" "$resolved_stem" "$origin"
|
|
154
271
|
devlog_lock_release
|
|
155
272
|
fi
|
|
156
273
|
DEVLOG_FILE="$resolved"
|
|
274
|
+
HANDOFF_STEM="$resolved_stem"
|
|
157
275
|
# shellcheck disable=SC2034 # consumed by callers (e.g. enforce-devlog.sh), not used in this file
|
|
158
|
-
HANDOFF_FILE="$
|
|
276
|
+
HANDOFF_FILE="$DEVLOG_DIR/$(devlog_platform_file "handoff.$name" .md "$DEVLOG_PLATFORM")"
|
|
159
277
|
DEVLOG_ORIGIN="$origin"
|
|
160
278
|
}
|
|
@@ -36,6 +36,28 @@ SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
|
|
|
36
36
|
# shellcheck source=handoff-fields.sh
|
|
37
37
|
. "$SCRIPT_DIR/handoff-fields.sh"
|
|
38
38
|
|
|
39
|
+
# 擋下時附上的最小格式:agent 在 /clear 之後通常沒載入 skill,只寫「依
|
|
40
|
+
# SKILL.md」會讓它每輪去 grep/sed 整份 SKILL.md,輸出攤在使用者面前。
|
|
41
|
+
# 這裡給夠填完一輪的骨架;細節錯誤由後面各道檢查的訊息各自說明。
|
|
42
|
+
round_format_hint() {
|
|
43
|
+
cat <<'EOF'
|
|
44
|
+
格式如下,不用另外讀 SKILL.md(Status 只留一個值):
|
|
45
|
+
### Summary
|
|
46
|
+
2–4 句結論
|
|
47
|
+
### Reply
|
|
48
|
+
對使用者說了什麼
|
|
49
|
+
### Handoff
|
|
50
|
+
<handoff>
|
|
51
|
+
<state>
|
|
52
|
+
做到哪、卡在哪
|
|
53
|
+
</state>
|
|
54
|
+
</handoff>
|
|
55
|
+
### Status
|
|
56
|
+
DONE
|
|
57
|
+
IN_PROGRESS/BLOCKED 另需:<handoff> 內依序 decisions→files→workspace→state→done-when→next,必寫 <workspace>(git 快照,如 `main @ a1b2c3d,工作樹乾淨`)、<done-when>、<next>;再加 ### Session Handoff,內含 <session-handoff> 包 <decisions>/<open-questions>/<failed-attempts>(沒有寫 - (無))。標籤各佔一行。
|
|
58
|
+
EOF
|
|
59
|
+
}
|
|
60
|
+
|
|
39
61
|
# --- loop guard -------------------------------------------------------
|
|
40
62
|
# 有 jq 就用 jq 精準解析;沒有 jq 就退化成字串比對(沒有更嚴謹的 parse,但
|
|
41
63
|
# 足以涵蓋 Claude Code 實際送出的 stop_hook_active 欄位形狀),兩種環境都要生效。
|
|
@@ -48,21 +70,18 @@ PROJECT_DIR="${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}"
|
|
|
48
70
|
# DEVLOG_DIR 永遠是 $PROJECT_DIR/.devlog,與分支無關,所以兩者等價。
|
|
49
71
|
ENABLED_FLAG="$PROJECT_DIR/.devlog/.enabled"
|
|
50
72
|
if [ ! -f "$ENABLED_FLAG" ]; then
|
|
51
|
-
#
|
|
52
|
-
# /devlog-tracker:start 會繼承一個陳舊的中斷旗標。.interrupted 跟
|
|
73
|
+
# 沒啟用就不做任何強制,但仍要清掉每個平台殘留的 .interrupted,否則之後
|
|
74
|
+
# 重新 /devlog-tracker:start 會繼承一個陳舊的中斷旗標。.interrupted 跟
|
|
53
75
|
# DEVLOG_DIR 一樣與分支無關,所以這裡不必解析分支。
|
|
54
|
-
|
|
55
|
-
rm -f "$PROJECT_DIR/.devlog/.interrupted" 2>/dev/null || true
|
|
56
|
-
fi
|
|
76
|
+
rm -f "$PROJECT_DIR"/.devlog/.interrupted "$PROJECT_DIR"/.devlog/.interrupted@* 2>/dev/null || true
|
|
57
77
|
exit 0
|
|
58
78
|
fi
|
|
59
79
|
# shellcheck source=devlog-path.sh
|
|
60
80
|
. "$SCRIPT_DIR/devlog-path.sh"
|
|
61
81
|
devlog_resolve_paths "$PROJECT_DIR"
|
|
62
|
-
|
|
63
|
-
if [ -f "$DEVLOG_DIR/.interrupted" ]; then
|
|
82
|
+
if [ -f "$INTERRUPTED_FLAG" ]; then
|
|
64
83
|
bash "$SCRIPT_DIR/close-open-round.sh" "user_interrupt" || true
|
|
65
|
-
rm -f "$
|
|
84
|
+
rm -f "$INTERRUPTED_FLAG" 2>/dev/null || true
|
|
66
85
|
# close-open-round.sh merges and removes .round-current.md whenever it
|
|
67
86
|
# decided the round was finished (stamped INTERRUPTED, or recovered —
|
|
68
87
|
# Claude had already written Summary/Handoff before the interrupt signal
|
|
@@ -89,8 +108,6 @@ if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
|
|
|
89
108
|
fi
|
|
90
109
|
|
|
91
110
|
# --- 開關檢查 -----------------------------------------------------------
|
|
92
|
-
TURN_MARKER="$DEVLOG_DIR/.turn-start"
|
|
93
|
-
|
|
94
111
|
devlog_lock_acquire
|
|
95
112
|
trap 'devlog_lock_release' EXIT
|
|
96
113
|
if [ -n "${LOCK_CONTENDED_BY:-}" ]; then
|
|
@@ -104,7 +121,6 @@ fi
|
|
|
104
121
|
# 每個 tick 遞增)沒有累積超過 max_silent_ticks。超過門檻就退回下面正常的
|
|
105
122
|
# 雜湊比對,逼這輪真的寫點東西;寫成功後把計數器歸零。span 檔案壞掉、缺欄位
|
|
106
123
|
# 或不是數字,一律當作沒有 span,直接往下走正常流程——fail-open。
|
|
107
|
-
SPAN_FILE="$DEVLOG_DIR/.span-open"
|
|
108
124
|
SPAN_VALID=0
|
|
109
125
|
if [ -f "$SPAN_FILE" ]; then
|
|
110
126
|
SPAN_TICKS="$(json_int_get "$SPAN_FILE" ticks_since_checkin)"
|
|
@@ -138,9 +154,9 @@ fi
|
|
|
138
154
|
|
|
139
155
|
if [ "$CURRENT_HASH" = "$TURN_START_HASH" ]; then
|
|
140
156
|
if [ "$SPAN_VALID" -eq 1 ]; then
|
|
141
|
-
echo "
|
|
157
|
+
{ echo "這一輪尚未寫入。請在 .devlog/${ROUND_CURRENT##*/} 建立一個新的 ## Round(編號接在 devlog.md 目前最後一輪之後),包含 User Input / Summary / Reply / Handoff / Status;收尾成功後 hook 會自動併回 devlog.md,不要自己直接寫進 devlog.md。"; round_format_hint; } >&2
|
|
142
158
|
else
|
|
143
|
-
echo "這一輪的 Round 只有 hook 寫的 User Input skeleton
|
|
159
|
+
{ echo "這一輪的 Round 只有 hook 寫的 User Input skeleton,還沒有收尾。請在 .devlog/${ROUND_CURRENT##*/} 裡編輯最後一個 Round,補上 User Input / Summary / Reply / Handoff / Status。不要再新增一個 ## Round。"; round_format_hint; } >&2
|
|
144
160
|
fi
|
|
145
161
|
exit 2
|
|
146
162
|
fi
|
|
@@ -177,7 +193,7 @@ if [ -n "$LAST_ROUND" ]; then
|
|
|
177
193
|
printf '%s\n' "$LAST_ROUND" | grep -q '^### Reply' && HAS_REPLY=1
|
|
178
194
|
printf '%s\n' "$LAST_ROUND" | grep -q '^### Handoff' && HAS_HANDOFF=1
|
|
179
195
|
if [ "$HAS_SUMMARY" -eq 0 ] || [ "$HAS_REPLY" -eq 0 ] || [ "$HAS_HANDOFF" -eq 0 ]; then
|
|
180
|
-
echo "最後一個 Round 缺少 \`### Summary\`、\`### Reply\` 或 \`### Handoff
|
|
196
|
+
{ echo "最後一個 Round 缺少 \`### Summary\`、\`### Reply\` 或 \`### Handoff\`。請補上這三個標題(Summary 給人掃、Reply 記對使用者說過的話、Handoff 給下一輪接續),寫在同一個 Round 裡,不要再新增一個 ## Round。"; round_format_hint; } >&2
|
|
181
197
|
exit 2
|
|
182
198
|
fi
|
|
183
199
|
|
|
@@ -230,7 +246,7 @@ if [ -n "$LAST_ROUND" ]; then
|
|
|
230
246
|
nonempty_body '^### Reply' && REPLY_BODY_OK=1
|
|
231
247
|
nonempty_body '^### Handoff' && HAN_BODY_OK=1
|
|
232
248
|
if [ "$SUM_BODY_OK" -eq 0 ] || [ "$REPLY_BODY_OK" -eq 0 ] || [ "$HAN_BODY_OK" -eq 0 ]; then
|
|
233
|
-
echo "最後一個 Round 的 ### Summary、### Reply 或 ### Handoff
|
|
249
|
+
{ echo "最後一個 Round 的 ### Summary、### Reply 或 ### Handoff 是空的。請寫上內容(不要只留標題),寫在同一個 Round 裡,不要再新增一個 ## Round。"; round_format_hint; } >&2
|
|
234
250
|
exit 2
|
|
235
251
|
fi
|
|
236
252
|
|
|
@@ -553,7 +569,7 @@ ${ACTUAL_DIRTY:-(沒有,工作樹乾淨)}"
|
|
|
553
569
|
{
|
|
554
570
|
echo "Session Handoff 格式不對:${SESSION_ERR}"
|
|
555
571
|
echo ""
|
|
556
|
-
echo "正確格式(三個標籤都要有,沒有內容就寫 - (無))。寫完後 hook 會覆寫 .devlog
|
|
572
|
+
echo "正確格式(三個標籤都要有,沒有內容就寫 - (無))。寫完後 hook 會覆寫 .devlog/${HANDOFF_FILE##*/} 給下一 session:"
|
|
557
573
|
handoff_xml_template session-handoff
|
|
558
574
|
} >&2
|
|
559
575
|
exit 2
|
|
@@ -561,7 +577,7 @@ ${ACTUAL_DIRTY:-(沒有,工作樹乾淨)}"
|
|
|
561
577
|
fi
|
|
562
578
|
fi
|
|
563
579
|
|
|
564
|
-
rm -f "$
|
|
580
|
+
rm -f "$MISMATCH_FILE" 2>/dev/null || true
|
|
565
581
|
|
|
566
582
|
# 這一輪通過所有驗證,正式收尾:把 .round-current.md 併回 devlog.md(併完
|
|
567
583
|
# 就地刪除 .round-current.md)。放在 checkpoint 計數檢查之前,這樣如果這輪
|
|
@@ -581,7 +597,7 @@ rm -f "$DEVLOG_DIR/.workspace-mismatch" 2>/dev/null || true
|
|
|
581
597
|
# 知道要去救它。
|
|
582
598
|
if [ -n "$LAST_ROUND" ]; then
|
|
583
599
|
if devlog_merge_round_current "$DEVLOG_FILE" "$ROUND_CURRENT"; then
|
|
584
|
-
rm -f "$
|
|
600
|
+
rm -f "$ROUND_OPEN" 2>/dev/null || true
|
|
585
601
|
case "${STATUS_VAL:-}" in
|
|
586
602
|
IN_PROGRESS|BLOCKED)
|
|
587
603
|
handoff_write "$HANDOFF_FILE" "$LAST_ROUND" 2>/dev/null || \
|
|
@@ -594,7 +610,7 @@ if [ -n "$LAST_ROUND" ]; then
|
|
|
594
610
|
esac
|
|
595
611
|
fi
|
|
596
612
|
else
|
|
597
|
-
rm -f "$
|
|
613
|
+
rm -f "$ROUND_OPEN" 2>/dev/null || true
|
|
598
614
|
fi
|
|
599
615
|
|
|
600
616
|
# 這輪真的有寫東西:如果剛剛因為 span 過期才走到這裡,把計數器歸零,
|
package/core/scripts/keep-all.js
CHANGED
|
@@ -53,7 +53,8 @@ function roundStatus(lines) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
function tsKey(ts) {
|
|
56
|
-
|
|
56
|
+
// Platform ownership is part of the displayed heading, not the timestamp.
|
|
57
|
+
const iso = ts.replace(/ · [a-z]+$/, '').replace(/([+-]\d\d)(\d\d)$/, '$1:$2');
|
|
57
58
|
const t = Date.parse(iso);
|
|
58
59
|
return Number.isNaN(t) ? Infinity : t;
|
|
59
60
|
}
|
|
@@ -408,8 +409,8 @@ function apply(c, planText, expected, count) {
|
|
|
408
409
|
throw new Error(`寫入途中失敗:${e.message}\n原始檔備份在 ${backup}`);
|
|
409
410
|
}
|
|
410
411
|
|
|
411
|
-
const
|
|
412
|
-
|
|
412
|
+
for (const name of fs.readdirSync(c.devlogDir).filter(f => /^\.span-open(@[a-z]+)?$/.test(f))) {
|
|
413
|
+
const span = path.join(c.devlogDir, name);
|
|
413
414
|
const m = fs.readFileSync(span, 'utf8').match(/"round"\s*:\s*(\d+)/);
|
|
414
415
|
if (m && movedFromCurrent.has(Number(m[1]))) fs.rmSync(span, { force: true });
|
|
415
416
|
}
|
package/core/scripts/keep-all.sh
CHANGED
|
@@ -20,11 +20,10 @@ PROJECT_DIR="${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}"
|
|
|
20
20
|
devlog_resolve_paths "$PROJECT_DIR"
|
|
21
21
|
[ -d "$DEVLOG_DIR" ] || { echo "NOTHING"; exit 0; }
|
|
22
22
|
|
|
23
|
-
OPEN=""
|
|
24
|
-
[ ! -f "$DEVLOG_DIR/.round-open" ] || OPEN="$(json_int_get "$DEVLOG_DIR/.round-open" round)"
|
|
25
|
-
|
|
26
23
|
devlog_lock_acquire
|
|
27
24
|
trap 'devlog_lock_release' EXIT
|
|
25
|
+
OPEN_ROW="$(devlog_single_open_round)" || exit 1
|
|
26
|
+
OPEN="$(printf '%s' "$OPEN_ROW" | cut -f2)"
|
|
28
27
|
node "$SCRIPT_DIR/keep-all.js" "$@" \
|
|
29
28
|
--dir "$DEVLOG_DIR" --project "$PROJECT_DIR" --current "${DEVLOG_FILE##*/}" \
|
|
30
29
|
--open "$OPEN" --origin "${DEVLOG_ORIGIN:-}"
|
|
@@ -135,6 +135,23 @@ test('apply moves a cross-file topic into one file in time order and consumes ke
|
|
|
135
135
|
assert.ok(fs.existsSync(path.join(backup, 'devlog.md')));
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
+
test('scan and apply keep platform-tagged rounds in chronological order', () => {
|
|
139
|
+
const d = setup({
|
|
140
|
+
'devlog.md': round(3, '2026-09-01T11:00:00+0800', 'DONE') + '\n' +
|
|
141
|
+
round(2, '2026-09-01T10:00:00+0800 · codex', 'DONE') + '\n' +
|
|
142
|
+
round(1, '2026-09-01T01:00:00Z · cursor', 'DONE'),
|
|
143
|
+
});
|
|
144
|
+
const c = ctx(d);
|
|
145
|
+
const s = scan(c);
|
|
146
|
+
assert.deepEqual(s.rounds.map(r => r.round), [1, 2, 3]);
|
|
147
|
+
|
|
148
|
+
apply(c, 'topic\t\t1,2,3\n', s.fingerprint, s.rounds.length);
|
|
149
|
+
const kept = read(d, 'devlog.topic.md');
|
|
150
|
+
assert.deepEqual([...kept.matchAll(/^## Round (\d+)/gm)].map(m => Number(m[1])), [1, 2, 3]);
|
|
151
|
+
assert.match(kept, /^## Round 1 — 2026-09-01T01:00:00Z · cursor$/m);
|
|
152
|
+
assert.match(kept, /^## Round 2 — 2026-09-01T10:00:00\+0800 · codex$/m);
|
|
153
|
+
});
|
|
154
|
+
|
|
138
155
|
test('apply deletes a branch file it emptied, but not an active branch or devlog.md', () => {
|
|
139
156
|
const d = setup({
|
|
140
157
|
'devlog.feat-main.md': '<!-- devlog-origin: branch=feat/main -->\n\n' + round(1, '2026-09-05T10:00:00+0800', 'IN_PROGRESS'),
|
|
@@ -228,3 +245,17 @@ test('apply clears .span-open only when its round moved out of current', () => {
|
|
|
228
245
|
apply(c, `b\t\t${k2.join(',')}\n`, s2.fingerprint, s2.rounds.length);
|
|
229
246
|
assert.ok(!exists(d, '.span-open'));
|
|
230
247
|
});
|
|
248
|
+
|
|
249
|
+
test('apply clears a platform .span-open@codex when its round moved out of current', () => {
|
|
250
|
+
const d = world();
|
|
251
|
+
fs.writeFileSync(path.join(d, '.span-open@codex'), '{"round": 1, "ticks": 0}\n');
|
|
252
|
+
const c = ctx(d);
|
|
253
|
+
const s = scan(c);
|
|
254
|
+
const k = [...idsOf(s, 'devlog.topic.md', [5]), ...idsOf(s, 'devlog.other.md', [9]), ...idsOf(s, 'devlog.md', [2])].join(',');
|
|
255
|
+
apply(c, `a\t\t${k}\n`, s.fingerprint, s.rounds.length);
|
|
256
|
+
assert.ok(exists(d, '.span-open@codex'));
|
|
257
|
+
const s2 = scan(c);
|
|
258
|
+
const k2 = [...s2.rounds.filter(r => r.file === 'devlog.a.md').map(r => r.id), ...idsOf(s2, 'devlog.md', [1])];
|
|
259
|
+
apply(c, `b\t\t${k2.join(',')}\n`, s2.fingerprint, s2.rounds.length);
|
|
260
|
+
assert.ok(!exists(d, '.span-open@codex'));
|
|
261
|
+
});
|
|
@@ -41,8 +41,9 @@ TARGET="$DEVLOG_DIR/devlog.$NAME.md"
|
|
|
41
41
|
[ -f "$MAIN" ] || { echo "devlog.md 不存在" >&2; exit 1; }
|
|
42
42
|
[ ! -e "$TARGET" ] || { echo "目標檔案已存在:$TARGET" >&2; exit 1; }
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
OPEN_ROW="$(devlog_single_open_round)" || exit 1
|
|
45
|
+
OPEN="$(printf '%s' "$OPEN_ROW" | cut -f2)"
|
|
46
|
+
OPEN_MARKER="$DEVLOG_DIR/$(devlog_platform_file .round-open '' "${OPEN_ROW%% *}")"
|
|
46
47
|
TMP="$(mktemp -d "${TMPDIR:-/tmp}/devlog-keep.XXXXXX")" || exit 1
|
|
47
48
|
trap 'rm -rf "$TMP"; devlog_lock_release' EXIT
|
|
48
49
|
STARTS="$TMP/starts"
|
|
@@ -148,14 +149,17 @@ EXISTING_KEPT_LINES="$(devlog_kept_index_lines "$MAIN")"
|
|
|
148
149
|
} > "$KEPT_STRIPPED.new" && mv "$KEPT_STRIPPED.new" "$MAIN" || exit 1
|
|
149
150
|
|
|
150
151
|
if [ "$FULL" -eq 1 ]; then
|
|
151
|
-
rm -f "$DEVLOG_DIR/.span-open"
|
|
152
|
-
[
|
|
152
|
+
rm -f "$DEVLOG_DIR"/.span-open "$DEVLOG_DIR"/.span-open@*
|
|
153
|
+
[ -z "$OPEN_ROW" ] || json_int_set "$OPEN_MARKER" round 1
|
|
153
154
|
[ ! -f "$DEVLOG_DIR/.checkpoint-state" ] || json_int_set "$DEVLOG_DIR/.checkpoint-state" rounds_since_checkpoint 0
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
155
|
+
else
|
|
156
|
+
for SPAN in "$DEVLOG_DIR"/.span-open "$DEVLOG_DIR"/.span-open@*; do
|
|
157
|
+
[ -f "$SPAN" ] || continue
|
|
158
|
+
SPAN_ROUND="$(json_int_get "$SPAN" round)"
|
|
159
|
+
if [ -n "$SPAN_ROUND" ] && [ "$SPAN_ROUND" -ge "$FROM" ] && [ "$SPAN_ROUND" -le "$TO" ]; then
|
|
160
|
+
rm -f "$SPAN"
|
|
161
|
+
fi
|
|
162
|
+
done
|
|
159
163
|
fi
|
|
160
164
|
|
|
161
165
|
REMAINING="$(devlog_list_round_starts "$MAIN" | wc -l | tr -d ' ')"
|
|
@@ -73,7 +73,7 @@ first_visit() {
|
|
|
73
73
|
DONE_FILES="${DONE_FILES}${1}"$'\n'
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
for f in "$DEVLOG_DIR/.round-current.md" "$DEVLOG_DIR/devlog.md" "$CUR_DEVLOG"; do
|
|
76
|
+
for f in "$DEVLOG_DIR"/.round-current.md "$DEVLOG_DIR"/.round-current@*.md "$DEVLOG_DIR/devlog.md" "$CUR_DEVLOG"; do
|
|
77
77
|
[ -n "$f" ] && [ -s "$f" ] && first_visit "$f" && migrate_round_file "$f"
|
|
78
78
|
done
|
|
79
79
|
for f in "$DEVLOG_DIR"/devlog.*.md; do
|
|
@@ -81,7 +81,7 @@ for f in "$DEVLOG_DIR"/devlog.*.md; do
|
|
|
81
81
|
head -n 1 "$f" | grep -q '^<!-- devlog-origin: ' || continue
|
|
82
82
|
first_visit "$f" && migrate_round_file "$f"
|
|
83
83
|
done
|
|
84
|
-
for f in "$DEVLOG_DIR"/handoff.md "$CUR_HANDOFF" "$DEVLOG_DIR"/handoff.*.md; do
|
|
84
|
+
for f in "$DEVLOG_DIR"/handoff.md "$CUR_HANDOFF" "$DEVLOG_DIR"/handoff@*.md "$DEVLOG_DIR"/handoff.*.md; do
|
|
85
85
|
[ -n "$f" ] && [ -s "$f" ] && first_visit "$f" && migrate_snapshot_file "$f"
|
|
86
86
|
done
|
|
87
87
|
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# PostToolUseFailure: remember user Esc so Stop can stamp without blocking.
|
|
3
3
|
set -uo pipefail
|
|
4
|
+
_src="${BASH_SOURCE[0]}"
|
|
5
|
+
SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
|
|
4
6
|
|
|
5
7
|
PROJECT_DIR="${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}"
|
|
6
8
|
DEVLOG_DIR="$PROJECT_DIR/.devlog"
|
|
7
9
|
ENABLED_FLAG="$DEVLOG_DIR/.enabled"
|
|
8
10
|
[ -f "$ENABLED_FLAG" ] || exit 0
|
|
11
|
+
# shellcheck source=devlog-path.sh
|
|
12
|
+
. "$SCRIPT_DIR/devlog-path.sh"
|
|
13
|
+
devlog_resolve_paths "$PROJECT_DIR"
|
|
9
14
|
|
|
10
15
|
INPUT="$(cat 2>/dev/null || true)"
|
|
11
16
|
IS_INT="false"
|
|
@@ -20,6 +25,6 @@ fi
|
|
|
20
25
|
|
|
21
26
|
if [ "$IS_INT" = "true" ]; then
|
|
22
27
|
mkdir -p "$DEVLOG_DIR" 2>/dev/null || true
|
|
23
|
-
: > "$
|
|
28
|
+
: > "$INTERRUPTED_FLAG" 2>/dev/null || true
|
|
24
29
|
fi
|
|
25
30
|
exit 0
|
|
@@ -6,8 +6,10 @@ if [ ! -f "$DEVLOG_DIR/.enabled" ]; then
|
|
|
6
6
|
echo "NOT_ENABLED"
|
|
7
7
|
exit 0
|
|
8
8
|
fi
|
|
9
|
-
rm -f "$DEVLOG_DIR/.enabled"
|
|
10
|
-
"$DEVLOG_DIR/.
|
|
11
|
-
"$DEVLOG_DIR/.
|
|
9
|
+
rm -f "$DEVLOG_DIR/.enabled" \
|
|
10
|
+
"$DEVLOG_DIR"/.span-open "$DEVLOG_DIR"/.span-open@* \
|
|
11
|
+
"$DEVLOG_DIR"/.round-open "$DEVLOG_DIR"/.round-open@* \
|
|
12
|
+
"$DEVLOG_DIR"/.interrupted "$DEVLOG_DIR"/.interrupted@* \
|
|
13
|
+
"$DEVLOG_DIR"/.awaiting-reply "$DEVLOG_DIR"/.awaiting-reply@*
|
|
12
14
|
echo "PAUSED"
|
|
13
15
|
exit 0
|