devlog-tracker 0.31.0 → 0.31.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
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
*English | [繁體中文](README.zh-TW.md)*
|
|
4
4
|
|
|
5
|
-
**Version** 0.31.
|
|
5
|
+
**Version** 0.31.1
|
|
6
6
|
|
|
7
7
|
Maintains a `.devlog/devlog.md` in your project, turning each conversation round's requests, decisions, and outcomes into a permanent record. A conversation disappears the moment you `/clear` or switch sessions; this file fills that gap so work can pause and resume. Nothing is touched until you explicitly run `/devlog-tracker:start` — installing the plugin alone doesn't create or modify any files.
|
|
8
8
|
|
package/README.zh-TW.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Sourced helper for serializing writes under .devlog.
|
|
3
|
+
#
|
|
4
|
+
# The holder exports DEVLOG_LOCK_OWNER=<its pid>, so a script it runs as a
|
|
5
|
+
# child (round-start.sh -> close-open-round.sh) finds the lock already its
|
|
6
|
+
# own and proceeds instead of waiting out the contention timeout on its
|
|
7
|
+
# parent. The child never holds it, so its release leaves the lock alone.
|
|
3
8
|
devlog_lock_acquire() {
|
|
4
9
|
local dir="${DEVLOG_DIR:-.}/.lock"
|
|
5
10
|
local start now pid
|
|
@@ -14,12 +19,16 @@ devlog_lock_acquire() {
|
|
|
14
19
|
if mkdir "$dir" 2>/dev/null; then
|
|
15
20
|
LOCK_HELD=1
|
|
16
21
|
printf '%s\n' "$$" > "$dir/pid" 2>/dev/null || true
|
|
22
|
+
export DEVLOG_LOCK_OWNER="$$"
|
|
17
23
|
return 0
|
|
18
24
|
fi
|
|
19
25
|
pid="$(cat "$dir/pid" 2>/dev/null || true)"
|
|
20
26
|
case "$pid" in
|
|
21
27
|
''|*[!0-9]*) pid='' ;;
|
|
22
28
|
esac
|
|
29
|
+
if [ -n "$pid" ] && [ "$pid" = "${DEVLOG_LOCK_OWNER:-}" ]; then
|
|
30
|
+
return 0
|
|
31
|
+
fi
|
|
23
32
|
# Stale lock: the pid that created it is no longer running (a crashed
|
|
24
33
|
# session), so reclaim it immediately instead of waiting out the full
|
|
25
34
|
# contention timeout below.
|
|
@@ -42,6 +51,7 @@ devlog_lock_release() {
|
|
|
42
51
|
rm -f "${DEVLOG_DIR:-.}/.lock/pid" 2>/dev/null || true
|
|
43
52
|
rmdir "${DEVLOG_DIR:-.}/.lock" 2>/dev/null || true
|
|
44
53
|
LOCK_HELD=0
|
|
54
|
+
unset DEVLOG_LOCK_OWNER
|
|
45
55
|
fi
|
|
46
56
|
}
|
|
47
57
|
|
|
@@ -316,6 +316,60 @@ fi
|
|
|
316
316
|
rm -rf "$REC_DIR"
|
|
317
317
|
export CLAUDE_PROJECT_DIR="$TMP_ROOT"
|
|
318
318
|
|
|
319
|
+
# --- L1: run as a child of the lock holder (round-start.sh's dangling heal)
|
|
320
|
+
# -> no wait on the parent's lock, still stamps, and its EXIT-trap
|
|
321
|
+
# release leaves the parent's lock in place ----------------------
|
|
322
|
+
# shellcheck source=../devlog-lock.sh
|
|
323
|
+
. "$SCRIPT_DIR/devlog-lock.sh"
|
|
324
|
+
write_skeleton
|
|
325
|
+
devlog_lock_acquire
|
|
326
|
+
START="$(date +%s)"
|
|
327
|
+
bash "$SCRIPT_DIR/close-open-round.sh" "dangling:next_prompt"
|
|
328
|
+
ELAPSED=$(( $(date +%s) - START ))
|
|
329
|
+
if [ "$ELAPSED" -le 1 ]; then
|
|
330
|
+
echo "PASS: child of lock holder doesn't wait (${ELAPSED}s)"
|
|
331
|
+
else
|
|
332
|
+
echo "FAIL: child of lock holder waited ${ELAPSED}s"
|
|
333
|
+
FAIL=1
|
|
334
|
+
fi
|
|
335
|
+
assert_contains "child of lock holder still stamps" $'### Status\nINTERRUPTED' "$(cat "$DEVLOG_DIR/devlog.md")"
|
|
336
|
+
if [ -d "$DEVLOG_DIR/.lock" ] && [ "$(cat "$DEVLOG_DIR/.lock/pid" 2>/dev/null)" = "$$" ]; then
|
|
337
|
+
echo "PASS: child's exit leaves the holder's lock"
|
|
338
|
+
else
|
|
339
|
+
echo "FAIL: holder's lock removed by child"
|
|
340
|
+
FAIL=1
|
|
341
|
+
fi
|
|
342
|
+
devlog_lock_release
|
|
343
|
+
assert_file_absent "holder release removes lock" "$DEVLOG_DIR/.lock/pid"
|
|
344
|
+
|
|
345
|
+
# --- L2: lock held by an unrelated live process (no inherited owner) ->
|
|
346
|
+
# still waits out the contention timeout, stamps fail-open, and
|
|
347
|
+
# leaves the foreign lock alone --------------------------------------
|
|
348
|
+
write_skeleton
|
|
349
|
+
sleep 5 &
|
|
350
|
+
LIVE_PID=$!
|
|
351
|
+
mkdir "$DEVLOG_DIR/.lock"
|
|
352
|
+
echo "$LIVE_PID" > "$DEVLOG_DIR/.lock/pid"
|
|
353
|
+
START="$(date +%s)"
|
|
354
|
+
bash "$SCRIPT_DIR/close-open-round.sh" "dangling:next_prompt"
|
|
355
|
+
ELAPSED=$(( $(date +%s) - START ))
|
|
356
|
+
if [ "$ELAPSED" -ge 2 ]; then
|
|
357
|
+
echo "PASS: unrelated holder still contends (${ELAPSED}s)"
|
|
358
|
+
else
|
|
359
|
+
echo "FAIL: unrelated holder skipped contention (${ELAPSED}s)"
|
|
360
|
+
FAIL=1
|
|
361
|
+
fi
|
|
362
|
+
assert_contains "contended run still stamps (fail-open)" $'### Status\nINTERRUPTED' "$(cat "$DEVLOG_DIR/devlog.md")"
|
|
363
|
+
if [ "$(cat "$DEVLOG_DIR/.lock/pid" 2>/dev/null)" = "$LIVE_PID" ]; then
|
|
364
|
+
echo "PASS: foreign lock left in place"
|
|
365
|
+
else
|
|
366
|
+
echo "FAIL: foreign lock touched"
|
|
367
|
+
FAIL=1
|
|
368
|
+
fi
|
|
369
|
+
kill "$LIVE_PID" 2>/dev/null || true
|
|
370
|
+
wait "$LIVE_PID" 2>/dev/null || true
|
|
371
|
+
rm -rf "$DEVLOG_DIR/.lock"
|
|
372
|
+
|
|
319
373
|
if [ "$FAIL" -eq 0 ]; then
|
|
320
374
|
echo "All checks passed."
|
|
321
375
|
exit 0
|
|
@@ -72,5 +72,49 @@ kill "$LIVE_PID" 2>/dev/null || true
|
|
|
72
72
|
wait "$LIVE_PID" 2>/dev/null || true
|
|
73
73
|
rm -rf "$DEVLOG_DIR/.lock"
|
|
74
74
|
|
|
75
|
+
# Re-entry from a child process: a script run by the lock holder (e.g.
|
|
76
|
+
# round-start.sh -> close-open-round.sh) sees the lock as already its own —
|
|
77
|
+
# returns at once, and its release leaves the parent's lock in place.
|
|
78
|
+
devlog_lock_acquire
|
|
79
|
+
START="$(date +%s)"
|
|
80
|
+
CHILD="$(bash -c '. "$1"; DEVLOG_DIR="$2"; devlog_lock_acquire; echo "held=${LOCK_HELD:-0} by=${LOCK_CONTENDED_BY:-}"; devlog_lock_release' _ "$SCRIPT_DIR/devlog-lock.sh" "$DEVLOG_DIR")"
|
|
81
|
+
ELAPSED=$(( $(date +%s) - START ))
|
|
82
|
+
[ "$ELAPSED" -le 1 ] && [ "$CHILD" = "held=0 by=" ] \
|
|
83
|
+
&& echo "PASS: child of holder re-enters without waiting" \
|
|
84
|
+
|| { echo "FAIL: child re-entry elapsed=$ELAPSED child='$CHILD'"; FAIL=1; }
|
|
85
|
+
[ -d "$DEVLOG_DIR/.lock" ] && [ "$(cat "$DEVLOG_DIR/.lock/pid")" = "$$" ] \
|
|
86
|
+
&& echo "PASS: child release leaves parent's lock" \
|
|
87
|
+
|| { echo "FAIL: parent's lock gone after child release"; FAIL=1; }
|
|
88
|
+
# Grandchild (holder -> script -> script) inherits the owner too.
|
|
89
|
+
START="$(date +%s)"
|
|
90
|
+
GRAND="$(bash -c 'bash -c ". \"\$1\"; DEVLOG_DIR=\"\$2\"; devlog_lock_acquire; echo held=\${LOCK_HELD:-0}; devlog_lock_release" _ "$1" "$2"' _ "$SCRIPT_DIR/devlog-lock.sh" "$DEVLOG_DIR")"
|
|
91
|
+
ELAPSED=$(( $(date +%s) - START ))
|
|
92
|
+
[ "$GRAND" = "held=0" ] && [ "$ELAPSED" -le 1 ] && [ -d "$DEVLOG_DIR/.lock" ] \
|
|
93
|
+
&& echo "PASS: grandchild of holder re-enters" || { echo "FAIL: grandchild got '$GRAND' elapsed=$ELAPSED"; FAIL=1; }
|
|
94
|
+
devlog_lock_release
|
|
95
|
+
[ -z "${DEVLOG_LOCK_OWNER:-}" ] && [ ! -d "$DEVLOG_DIR/.lock" ] \
|
|
96
|
+
&& echo "PASS: release clears owner" || { echo "FAIL: owner=${DEVLOG_LOCK_OWNER:-} survives release"; FAIL=1; }
|
|
97
|
+
|
|
98
|
+
# After the holder releases, a child it spawns later no longer carries the
|
|
99
|
+
# owner, so it would contend on a lock someone else took meanwhile.
|
|
100
|
+
OUT="$(bash -c 'echo "owner=${DEVLOG_LOCK_OWNER:-}"')"
|
|
101
|
+
[ "$OUT" = "owner=" ] && echo "PASS: owner not exported after release" \
|
|
102
|
+
|| { echo "FAIL: child still sees $OUT"; FAIL=1; }
|
|
103
|
+
|
|
104
|
+
# A process that did not inherit the owner (another hook launched by the
|
|
105
|
+
# agent while the lock is held) still waits out the timeout.
|
|
106
|
+
sleep 5 &
|
|
107
|
+
LIVE_PID=$!
|
|
108
|
+
mkdir "$DEVLOG_DIR/.lock"
|
|
109
|
+
echo "$LIVE_PID" > "$DEVLOG_DIR/.lock/pid"
|
|
110
|
+
START="$(date +%s)"
|
|
111
|
+
OUT="$(DEVLOG_LOCK_OWNER=12345 bash -c '. "$1"; DEVLOG_DIR="$2"; devlog_lock_acquire; echo "${LOCK_HELD:-0}"' _ "$SCRIPT_DIR/devlog-lock.sh" "$DEVLOG_DIR")"
|
|
112
|
+
ELAPSED=$(( $(date +%s) - START ))
|
|
113
|
+
[ "$OUT" = "0" ] && [ "$ELAPSED" -ge 2 ] \
|
|
114
|
+
&& echo "PASS: unrelated owner still contends" || { echo "FAIL: unrelated owner out=$OUT elapsed=$ELAPSED"; FAIL=1; }
|
|
115
|
+
kill "$LIVE_PID" 2>/dev/null || true
|
|
116
|
+
wait "$LIVE_PID" 2>/dev/null || true
|
|
117
|
+
rm -rf "$DEVLOG_DIR/.lock"
|
|
118
|
+
|
|
75
119
|
if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
|
|
76
120
|
else echo "Some checks FAILED."; exit 1; fi
|
|
@@ -125,7 +125,19 @@ hello world
|
|
|
125
125
|
```
|
|
126
126
|
EOF
|
|
127
127
|
printf '%s\n' '{"round": 1, "opened_at": "2026-09-09T12:00:00+08:00"}' > "$DEVLOG_DIR/.round-open"
|
|
128
|
+
# round-start.sh holds .devlog/.lock while it runs close-open-round.sh as a
|
|
129
|
+
# child; the child must not wait out the 2s contention timeout on its own
|
|
130
|
+
# parent's lock.
|
|
131
|
+
HEAL_START="$(date +%s)"
|
|
128
132
|
printf '%s' '{"prompt":"second"}' | bash "$SCRIPT_DIR/round-start.sh"
|
|
133
|
+
HEAL_ELAPSED=$(( $(date +%s) - HEAL_START ))
|
|
134
|
+
if [ "$HEAL_ELAPSED" -le 1 ]; then
|
|
135
|
+
echo "PASS: dangling heal doesn't block on parent's lock (${HEAL_ELAPSED}s)"
|
|
136
|
+
else
|
|
137
|
+
echo "FAIL: dangling heal took ${HEAL_ELAPSED}s (child waited on parent's lock)"
|
|
138
|
+
FAIL=1
|
|
139
|
+
fi
|
|
140
|
+
assert_file_absent "dangling heal releases .lock" "$DEVLOG_DIR/.lock"
|
|
129
141
|
MAIN_BODY="$(cat "$DEVLOG_DIR/devlog.md")"
|
|
130
142
|
CUR_BODY="$(cat "$DEVLOG_DIR/.round-current.md" 2>/dev/null || echo '')"
|
|
131
143
|
assert_contains "Round 2 heading in .round-current.md" "## Round 2 —" "$CUR_BODY"
|