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.0
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  *[English](README.md) | 繁體中文*
4
4
 
5
- **版本** 0.31.0
5
+ **版本** 0.31.1
6
6
 
7
7
  在專案中維護一份 `.devlog/devlog.md`,把每一輪對話的請求、決策與結果寫成永久紀錄。對話一 `/clear` 或換 session 就沒了;這份檔案取代那個缺口,讓工作可以中斷再接。沒下過 `/devlog-tracker:start` 時,裝著也不會動任何檔案。
8
8
 
@@ -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"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlog-tracker",
3
- "version": "0.31.0",
3
+ "version": "0.31.1",
4
4
  "description": "npx installer for devlog-tracker's Claude Code, Cursor and Codex adapters.",
5
5
  "bin": {
6
6
  "devlog-tracker": "bin/devlog-tracker.js"