devlog-tracker 0.22.0

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.
Files changed (120) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +234 -0
  3. package/bin/devlog-tracker.js +51 -0
  4. package/cli/init.js +55 -0
  5. package/cli/init.test.js +37 -0
  6. package/cli/merge-hooks.js +60 -0
  7. package/cli/merge-hooks.test.js +187 -0
  8. package/cli/platforms/codex.js +13 -0
  9. package/cli/platforms/cursor.js +13 -0
  10. package/cli/status.js +14 -0
  11. package/cli/status.test.js +33 -0
  12. package/cli/vendor.js +37 -0
  13. package/cli/vendor.test.js +84 -0
  14. package/codex/hooks/on-pre-tool.sh +19 -0
  15. package/codex/hooks/on-session-end.sh +31 -0
  16. package/codex/hooks/on-session-start.sh +37 -0
  17. package/codex/hooks/on-stop.sh +18 -0
  18. package/codex/hooks/on-user-prompt-submit.sh +15 -0
  19. package/codex/hooks/project-dir.sh +13 -0
  20. package/codex/hooks/test-adapters.sh +98 -0
  21. package/codex/hooks.json +54 -0
  22. package/commands/checkpoint.md +17 -0
  23. package/commands/clean.md +50 -0
  24. package/commands/compact.md +24 -0
  25. package/commands/continue.md +30 -0
  26. package/commands/keep.md +100 -0
  27. package/commands/lessons-drift.md +19 -0
  28. package/commands/lessons-off.md +16 -0
  29. package/commands/lessons-on.md +20 -0
  30. package/commands/lessons.md +20 -0
  31. package/commands/overview.md +32 -0
  32. package/commands/pause.md +20 -0
  33. package/commands/resume.md +18 -0
  34. package/commands/segment-watch.md +21 -0
  35. package/commands/span.md +13 -0
  36. package/commands/start.md +19 -0
  37. package/commands/status.md +14 -0
  38. package/cursor/hooks/on-pre-tool.sh +24 -0
  39. package/cursor/hooks/on-session-end.sh +11 -0
  40. package/cursor/hooks/on-session-start.sh +15 -0
  41. package/cursor/hooks/on-stop.sh +44 -0
  42. package/cursor/hooks/on-submit-prompt.sh +28 -0
  43. package/cursor/hooks/on-tool-failure.sh +16 -0
  44. package/cursor/hooks/project-dir.sh +10 -0
  45. package/cursor/hooks/test-adapters.sh +201 -0
  46. package/cursor/hooks.json +26 -0
  47. package/hooks/scripts/await-open.sh +23 -0
  48. package/hooks/scripts/checkpoint-set.sh +44 -0
  49. package/hooks/scripts/clean-devlog.sh +69 -0
  50. package/hooks/scripts/close-open-round.sh +186 -0
  51. package/hooks/scripts/compact-devlog.sh +72 -0
  52. package/hooks/scripts/detect-pending-question.sh +31 -0
  53. package/hooks/scripts/devlog-lock.sh +38 -0
  54. package/hooks/scripts/devlog-md.sh +222 -0
  55. package/hooks/scripts/devlog-path.sh +70 -0
  56. package/hooks/scripts/enforce-devlog.sh +629 -0
  57. package/hooks/scripts/files-snapshot.sh +70 -0
  58. package/hooks/scripts/json-field.sh +76 -0
  59. package/hooks/scripts/keep-move.sh +155 -0
  60. package/hooks/scripts/kept-list.sh +40 -0
  61. package/hooks/scripts/lessons-append.sh +93 -0
  62. package/hooks/scripts/lessons-drift-set.sh +42 -0
  63. package/hooks/scripts/lessons-off.sh +15 -0
  64. package/hooks/scripts/lessons-on.sh +21 -0
  65. package/hooks/scripts/lessons-read.sh +60 -0
  66. package/hooks/scripts/on-session-end.sh +14 -0
  67. package/hooks/scripts/on-stop-failure.sh +18 -0
  68. package/hooks/scripts/on-tool-failure.sh +25 -0
  69. package/hooks/scripts/pause-devlog.sh +13 -0
  70. package/hooks/scripts/redact-prompt.sh +19 -0
  71. package/hooks/scripts/resume-devlog.sh +41 -0
  72. package/hooks/scripts/round-start.sh +333 -0
  73. package/hooks/scripts/run-tests.sh +26 -0
  74. package/hooks/scripts/segment-watch-set.sh +47 -0
  75. package/hooks/scripts/segment-watch.sh +197 -0
  76. package/hooks/scripts/session-start-devlog.sh +157 -0
  77. package/hooks/scripts/span-close.sh +9 -0
  78. package/hooks/scripts/span-open.sh +21 -0
  79. package/hooks/scripts/start-devlog.sh +24 -0
  80. package/hooks/scripts/status-devlog.sh +43 -0
  81. package/hooks/scripts/tests/test-await-open.sh +112 -0
  82. package/hooks/scripts/tests/test-branch-scoped-integration.sh +108 -0
  83. package/hooks/scripts/tests/test-checkpoint-set.sh +36 -0
  84. package/hooks/scripts/tests/test-clean-devlog.sh +160 -0
  85. package/hooks/scripts/tests/test-cli-init-e2e.sh +37 -0
  86. package/hooks/scripts/tests/test-close-open-round.sh +325 -0
  87. package/hooks/scripts/tests/test-compact-devlog.sh +62 -0
  88. package/hooks/scripts/tests/test-devlog-lock.sh +37 -0
  89. package/hooks/scripts/tests/test-devlog-md.sh +530 -0
  90. package/hooks/scripts/tests/test-devlog-path.sh +118 -0
  91. package/hooks/scripts/tests/test-enforce-devlog-files.sh +233 -0
  92. package/hooks/scripts/tests/test-enforce-devlog-handoff-order.sh +223 -0
  93. package/hooks/scripts/tests/test-enforce-devlog-workspace.sh +333 -0
  94. package/hooks/scripts/tests/test-enforce-devlog.sh +1290 -0
  95. package/hooks/scripts/tests/test-files-snapshot.sh +142 -0
  96. package/hooks/scripts/tests/test-json-field.sh +57 -0
  97. package/hooks/scripts/tests/test-keep-move.sh +127 -0
  98. package/hooks/scripts/tests/test-kept-list.sh +80 -0
  99. package/hooks/scripts/tests/test-lessons-append.sh +138 -0
  100. package/hooks/scripts/tests/test-lessons-drift-set.sh +36 -0
  101. package/hooks/scripts/tests/test-lessons-on-off.sh +75 -0
  102. package/hooks/scripts/tests/test-lessons-read.sh +97 -0
  103. package/hooks/scripts/tests/test-on-interrupt.sh +186 -0
  104. package/hooks/scripts/tests/test-redact-prompt.sh +27 -0
  105. package/hooks/scripts/tests/test-resume-devlog.sh +35 -0
  106. package/hooks/scripts/tests/test-round-start.sh +825 -0
  107. package/hooks/scripts/tests/test-segment-watch-set.sh +75 -0
  108. package/hooks/scripts/tests/test-segment-watch.sh +468 -0
  109. package/hooks/scripts/tests/test-session-start-devlog.sh +508 -0
  110. package/hooks/scripts/tests/test-start-pause-devlog.sh +84 -0
  111. package/hooks/scripts/tests/test-status-span.sh +50 -0
  112. package/hooks/scripts/tests/test-workspace-snapshot.sh +130 -0
  113. package/hooks/scripts/workspace-snapshot.sh +69 -0
  114. package/package.json +37 -0
  115. package/skills/devlog-tracker/SKILL.md +411 -0
  116. package/skills/devlog-tracker/references/checkpoint-mode.md +42 -0
  117. package/skills/devlog-tracker/references/lessons-mode.md +32 -0
  118. package/skills/devlog-tracker/references/reply-fold.md +129 -0
  119. package/skills/devlog-tracker/references/round-segments.md +55 -0
  120. package/skills/devlog-tracker/references/span-mode.md +65 -0
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+
4
+ _src="${BASH_SOURCE[0]}"
5
+ SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
6
+ . "$SCRIPT_DIR/devlog-md.sh"
7
+ . "$SCRIPT_DIR/devlog-lock.sh"
8
+ . "$SCRIPT_DIR/devlog-path.sh"
9
+
10
+ devlog_resolve_paths "${CLAUDE_PROJECT_DIR:-.}"
11
+ devlog_lock_acquire
12
+ trap 'devlog_lock_release' EXIT
13
+ MAIN="$DEVLOG_FILE"
14
+ ARCHIVE="$DEVLOG_DIR/devlog.archive.md"
15
+ [ -f "$MAIN" ] || { echo "devlog.md 不存在" >&2; exit 1; }
16
+
17
+ TMP="$(mktemp -d "${TMPDIR:-/tmp}/devlog-compact.XXXXXX")" || exit 1
18
+ trap 'rm -rf "$TMP"; devlog_lock_release' EXIT
19
+ STARTS="$TMP/starts"
20
+ MOVE="$TMP/move"
21
+ CANDIDATES="$TMP/candidates"
22
+ MOVED_BLOCKS="$TMP/moved"
23
+ NEW_MAIN="$TMP/main"
24
+ devlog_list_round_starts "$MAIN" > "$STARTS"
25
+ : > "$MOVE"
26
+ : > "$CANDIDATES"
27
+
28
+ TOTAL="$(wc -l < "$STARTS" | tr -d ' ')"
29
+ while read -r start _; do
30
+ [ -n "${start:-}" ] || continue
31
+ end="$(devlog_block_end "$MAIN" "$start")"
32
+ status="$(devlog_round_status "$MAIN" "$start" "$end")"
33
+ case "$status" in
34
+ IN_PROGRESS|BLOCKED|INTERRUPTED) ;;
35
+ *) printf '%s\n' "$start" >> "$CANDIDATES" ;;
36
+ esac
37
+ done < "$STARTS"
38
+
39
+ COMPLETED="$(wc -l < "$CANDIDATES" | tr -d ' ')"
40
+ TO_MOVE=$((COMPLETED > 5 ? COMPLETED - 5 : 0))
41
+ awk -v count="$TO_MOVE" 'NR <= count { print }' "$CANDIDATES" > "$MOVE"
42
+ MOVED="$(wc -l < "$MOVE" | tr -d ' ')"
43
+ if [ "$MOVED" -eq 0 ]; then
44
+ ARCHIVE_COUNT=0
45
+ [ ! -f "$ARCHIVE" ] || ARCHIVE_COUNT="$(devlog_list_round_starts "$ARCHIVE" | wc -l | tr -d ' ')"
46
+ printf 'MOVED=0 REMAINING=%s ARCHIVE=%s\n' "$TOTAL" "$ARCHIVE_COUNT"
47
+ exit 0
48
+ fi
49
+
50
+ awk -v move_file="$MOVE" -v moved_file="$MOVED_BLOCKS" '
51
+ BEGIN {
52
+ while ((getline line < move_file) > 0) move[line] = 1
53
+ }
54
+ /^[ \t]*```/ { fence = !fence }
55
+ !fence && /^## / {
56
+ moving = (NR in move)
57
+ }
58
+ moving { print >> moved_file; next }
59
+ { print }
60
+ ' "$MAIN" > "$NEW_MAIN" || exit 1
61
+
62
+ while read -r start; do
63
+ heading="$(awk -v n="$start" 'NR == n { print; exit }' "$MAIN")"
64
+ grep -Fqx "$heading" "$MOVED_BLOCKS" || { echo "archive 驗證失敗" >&2; exit 1; }
65
+ done < "$MOVE"
66
+
67
+ cat "$MOVED_BLOCKS" >> "$ARCHIVE" || exit 1
68
+ mv "$NEW_MAIN" "$MAIN" || exit 1
69
+
70
+ REMAINING="$(devlog_list_round_starts "$MAIN" | wc -l | tr -d ' ')"
71
+ ARCHIVE_COUNT="$(devlog_list_round_starts "$ARCHIVE" | wc -l | tr -d ' ')"
72
+ printf 'MOVED=%s REMAINING=%s ARCHIVE=%s\n' "$MOVED" "$REMAINING" "$ARCHIVE_COUNT"
@@ -0,0 +1,31 @@
1
+ # shellcheck shell=bash
2
+ # sourced by hook scripts. Do not execute.
3
+ #
4
+ # Best-effort only, used to make the dangling-heal stub message more useful —
5
+ # not part of the enable/detection logic itself. 專案原則是不解析 transcript
6
+ # 來判斷「這輪有沒有記錄」(見 SKILL.md),這裡不違反:偵測不到/沒有 jq/
7
+ # 檔案讀不到,一律回傳空字串,退回原本的通用 stub 文字,不影響任何強制流程。
8
+ #
9
+ # detect_pending_question <transcript_path>
10
+ # 印出 "1" 代表:transcript 裡最後一筆 assistant/user 訊息,是一個還沒拿到
11
+ # 回答的 AskUserQuestion tool_use(also 是整份 transcript 的最後一筆有意義
12
+ # 紀錄)——最典型的訊號是「process 在等使用者選答案時被中斷」。其餘一律印出
13
+ # 空字串。
14
+ detect_pending_question() {
15
+ local transcript="$1"
16
+ [ -n "$transcript" ] || { echo ''; return 0; }
17
+ [ -f "$transcript" ] || { echo ''; return 0; }
18
+ command -v jq >/dev/null 2>&1 || { echo ''; return 0; }
19
+ tail -n 200 "$transcript" 2>/dev/null | jq -rs '
20
+ map(select(.type == "assistant" or .type == "user"))
21
+ | if length == 0 then ""
22
+ else
23
+ (.[-1]) as $last
24
+ | if $last.type == "assistant"
25
+ and ($last.message.content // [] | any(.type == "tool_use" and .name == "AskUserQuestion"))
26
+ then "1"
27
+ else ""
28
+ end
29
+ end
30
+ ' 2>/dev/null || echo ''
31
+ }
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ # Sourced helper for serializing writes under .devlog.
3
+ devlog_lock_acquire() {
4
+ local dir="${DEVLOG_DIR:-.}/.lock"
5
+ local start now
6
+ LOCK_HELD=0
7
+ command -v mkdir >/dev/null 2>&1 || return 0
8
+ command -v rmdir >/dev/null 2>&1 || return 0
9
+ command -v date >/dev/null 2>&1 || return 0
10
+ command -v sleep >/dev/null 2>&1 || return 0
11
+ start="$(date +%s 2>/dev/null || echo 0)"
12
+ while true; do
13
+ if mkdir "$dir" 2>/dev/null; then
14
+ LOCK_HELD=1
15
+ return 0
16
+ fi
17
+ now="$(date +%s 2>/dev/null || echo 0)"
18
+ if [ $((now - start)) -ge 2 ]; then
19
+ return 0
20
+ fi
21
+ sleep 0.1 2>/dev/null || true
22
+ done
23
+ }
24
+
25
+ devlog_lock_release() {
26
+ if [ "${LOCK_HELD:-0}" -eq 1 ]; then
27
+ rmdir "${DEVLOG_DIR:-.}/.lock" 2>/dev/null || true
28
+ LOCK_HELD=0
29
+ fi
30
+ }
31
+
32
+ devlog_with_lock() {
33
+ devlog_lock_acquire
34
+ "$@"
35
+ local result=$?
36
+ devlog_lock_release
37
+ return "$result"
38
+ }
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env bash
2
+
3
+ _DEVLOG_MD_DIR="$(cd "${BASH_SOURCE[0]%/*}" && pwd)"
4
+
5
+ # NOFENCE 防呆(跟 enforce-devlog.sh 同一套邏輯):如果 [start,end] 這個範圍
6
+ # 內 ``` 記號數量是奇數,代表某處圍欄沒有正常收尾(例如 User Input 貼了一段
7
+ # 忘記關閉 fence 的程式碼)——這是真的寫錯了,不是刻意的範例。此時呼叫端的
8
+ # fence 變數若照常 toggle,會在剩下的內容裡卡在 1,把明明存在的 #### 工作區
9
+ # / ### Status / ### 段落 誤判成「被吃掉、看起來是空的」,讓
10
+ # workspace_claim_state 靜默回報 NO_CLAIM(本來該是 MISMATCH),整個漂移
11
+ # 偵測形同虛設。回傳 1 時呼叫端要讓 fence 變數維持 0(退化回單純掃描),
12
+ # 圍欄成雙成對(含 0 個)時回傳 0,fence-aware 行為不變。
13
+ _devlog_fence_nofence() {
14
+ local file="$1" start="$2" end="$3" count
15
+ count="$(awk -v start="$start" -v end="$end" '
16
+ NR < start || NR > end { next }
17
+ /^[ \t]*```/ { c++ }
18
+ END { print c + 0 }
19
+ ' "$file")"
20
+ if [ $((count % 2)) -eq 0 ]; then printf '0\n'; else printf '1\n'; fi
21
+ }
22
+
23
+ devlog_list_round_starts() {
24
+ awk '
25
+ /^[ \t]*```/ { fence = !fence; next }
26
+ !fence && /^## Round [0-9]+/ {
27
+ round = $0
28
+ sub(/^## Round /, "", round)
29
+ sub(/[^0-9].*$/, "", round)
30
+ print NR, round
31
+ }
32
+ ' "$1"
33
+ }
34
+
35
+ devlog_block_end() {
36
+ awk -v start="$2" '
37
+ NR < start { next }
38
+ /^[ \t]*```/ { fence = !fence }
39
+ NR > start && !fence && /^## / { print NR - 1; found = 1; exit }
40
+ END { if (!found) print NR }
41
+ ' "$1"
42
+ }
43
+
44
+ devlog_round_status() {
45
+ local nofence
46
+ nofence="$(_devlog_fence_nofence "$1" "$2" "$3")"
47
+ awk -v start="$2" -v end="$3" -v nofence="$nofence" '
48
+ NR < start || NR > end { next }
49
+ /^[ \t]*```/ { if (!nofence) fence = !fence; next }
50
+ !fence && /^### Status[[:space:]]*$/ { status = 1; value = ""; next }
51
+ !fence && /^### / && status { status = 0 }
52
+ status && $0 !~ /^[[:space:]]*$/ { value = $0 }
53
+ END { print value }
54
+ ' "$1"
55
+ }
56
+
57
+ devlog_count_segments() {
58
+ awk -v start="$2" -v end="$3" '
59
+ NR < start || NR > end { next }
60
+ /^[ \t]*```/ { fence = !fence; next }
61
+ !fence && /^### 段落 / { count++ }
62
+ END { print count + 0 }
63
+ ' "$1"
64
+ }
65
+
66
+ devlog_insert_before_summary() {
67
+ local file="$1" start="$2" end="$3" text_file="$4"
68
+ awk -v start="$start" -v end="$end" -v textfile="$text_file" '
69
+ BEGIN {
70
+ inserted = 0
71
+ while ((getline line < textfile) > 0) {
72
+ ins[ni++] = line
73
+ }
74
+ close(textfile)
75
+ }
76
+ {
77
+ if (!inserted && NR >= start && NR <= end && !fence && $0 ~ /^### Summary[[:space:]]*$/) {
78
+ for (i = 0; i < ni; i++) print ins[i]
79
+ inserted = 1
80
+ }
81
+ if ($0 ~ /^[ \t]*```/) fence = !fence
82
+ print
83
+ }
84
+ ' "$file"
85
+ }
86
+
87
+ devlog_kept_index_lines() {
88
+ awk '
89
+ /^[ \t]*```/ { fence = !fence; next }
90
+ !fence && /^## Kept 索引/ { grab = 1; found = 1; buf = ""; next }
91
+ !fence && grab && /^## / { grab = 0 }
92
+ grab { buf = buf $0 ORS }
93
+ END { if (found) printf "%s", buf }
94
+ ' "$1"
95
+ }
96
+
97
+ devlog_strip_kept_index() {
98
+ # Pipe through a second pass that buffers blank lines and only emits them
99
+ # once a non-blank line follows. Kept 索引 is always the trailing section
100
+ # (keep-move.sh appends it last), so any blank line(s) left as its
101
+ # separator are always at true EOF here and get dropped instead of
102
+ # surviving into the caller's rebuild — this is what stops the separator
103
+ # from growing by one line on every keep-move.sh call.
104
+ awk '
105
+ /^[ \t]*```/ { fence = !fence }
106
+ !fence && /^## Kept 索引/ { grab = 1; next }
107
+ !fence && grab && /^## / { grab = 0 }
108
+ grab { next }
109
+ { print }
110
+ ' "$1" | awk '
111
+ /^[ \t]*$/ { pending = pending $0 ORS; next }
112
+ { printf "%s", pending; pending = ""; print }
113
+ ' > "$2"
114
+ }
115
+
116
+ devlog_lessons_index_lines() {
117
+ awk '
118
+ /^[ \t]*```/ { fence = !fence; next }
119
+ !fence && /^## Lessons 索引/ { grab = 1; found = 1; buf = ""; next }
120
+ !fence && grab && /^## / { grab = 0 }
121
+ grab { buf = buf $0 ORS }
122
+ END { if (found) printf "%s", buf }
123
+ ' "$1"
124
+ }
125
+
126
+ devlog_strip_lessons_index() {
127
+ # Same trailing-block assumption and blank-line-collapse behavior as
128
+ # devlog_strip_kept_index() above (docs/design/lessons-mode.md).
129
+ awk '
130
+ /^[ \t]*```/ { fence = !fence }
131
+ !fence && /^## Lessons 索引/ { grab = 1; next }
132
+ !fence && grab && /^## / { grab = 0 }
133
+ grab { next }
134
+ { print }
135
+ ' "$1" | awk '
136
+ /^[ \t]*$/ { pending = pending $0 ORS; next }
137
+ { printf "%s", pending; pending = ""; print }
138
+ ' > "$2"
139
+ }
140
+
141
+ devlog_round_workspace_body() {
142
+ local nofence
143
+ nofence="$(_devlog_fence_nofence "$1" "$2" "$3")"
144
+ awk -v start="$2" -v end="$3" -v nofence="$nofence" '
145
+ NR < start || NR > end { next }
146
+ /^[ \t]*```/ { if (!nofence) fence = !fence; next }
147
+ !fence && /^#### 工作區[[:space:]]*$/ { grab = 1; next }
148
+ grab && !fence && /^#### / { grab = 0 }
149
+ grab && !fence && /^### / { grab = 0 }
150
+ grab && !fence && /^## / { grab = 0 }
151
+ grab { print }
152
+ ' "$1" | sed -e '/^[[:space:]]*$/d'
153
+ }
154
+
155
+ devlog_round_segments_body() {
156
+ local nofence
157
+ nofence="$(_devlog_fence_nofence "$1" "$2" "$3")"
158
+ awk -v start="$2" -v end="$3" -v nofence="$nofence" '
159
+ NR < start || NR > end { next }
160
+ /^[ \t]*```/ {
161
+ if (segment) print
162
+ if (!nofence) fence = !fence
163
+ next
164
+ }
165
+ !fence && /^### 段落 / { segment = 1; next }
166
+ segment && !fence && /^### / { segment = 0 }
167
+ segment && !fence && /^## / { segment = 0 }
168
+ segment { print }
169
+ ' "$1"
170
+ }
171
+
172
+ devlog_merge_round_current() {
173
+ # Appends $2's content onto $1 (one blank line separator, matching the
174
+ # existing "\n## Round N" append convention) and removes $2. No-op if $2
175
+ # is absent or empty — nothing to merge.
176
+ local devlog="$1" current="$2"
177
+ [ -s "$current" ] || return 0
178
+ {
179
+ printf '\n'
180
+ cat "$current"
181
+ } >> "$devlog" 2>/dev/null || return 1
182
+ rm -f "$current" 2>/dev/null || true
183
+ return 0
184
+ }
185
+
186
+ devlog_reopen_last_round() {
187
+ # Moves the last "## Round N ..." block out of $1 into $2 (creating $2),
188
+ # removing those lines from $1. Returns 1 and touches neither file if $1
189
+ # has no round to move.
190
+ local devlog="$1" current="$2" start end
191
+ start="$(devlog_list_round_starts "$devlog" | awk 'END { print $1 }')"
192
+ [ -n "$start" ] || return 1
193
+ end="$(devlog_block_end "$devlog" "$start")"
194
+ awk -v start="$start" -v end="$end" 'NR >= start && NR <= end' "$devlog" > "$current" 2>/dev/null || return 1
195
+ awk -v start="$start" -v end="$end" 'NR < start || NR > end' "$devlog" > "$devlog.tmp" 2>/dev/null \
196
+ && mv "$devlog.tmp" "$devlog" 2>/dev/null || { rm -f "$devlog.tmp" "$current" 2>/dev/null; return 1; }
197
+ return 0
198
+ }
199
+
200
+ workspace_claim_state() {
201
+ local dir="$1" file="$2" start end status claimed live
202
+ if ! type workspace_snapshot >/dev/null 2>&1; then
203
+ # shellcheck source=workspace-snapshot.sh
204
+ . "$_DEVLOG_MD_DIR/workspace-snapshot.sh"
205
+ fi
206
+ start="$(devlog_list_round_starts "$file" | awk 'END { print $1 }')"
207
+ [ -n "$start" ] || { printf 'NO_CLAIM\n'; return 0; }
208
+ end="$(devlog_block_end "$file" "$start")"
209
+ status="$(devlog_round_status "$file" "$start" "$end")"
210
+ case "$status" in
211
+ '[reason:'*) status=INTERRUPTED ;;
212
+ esac
213
+ case "$status" in
214
+ IN_PROGRESS|BLOCKED|INTERRUPTED) ;;
215
+ *) printf 'NO_CLAIM\n'; return 0 ;;
216
+ esac
217
+ claimed="$(devlog_round_workspace_body "$file" "$start" "$end")"
218
+ [ -n "$claimed" ] || { printf 'NO_CLAIM\n'; return 0; }
219
+ live="$(workspace_snapshot "$dir")"
220
+ [ -n "$live" ] || { printf 'NO_CLAIM\n'; return 0; }
221
+ if [ "$claimed" = "$live" ]; then printf 'MATCH\n'; else printf 'MISMATCH\n'; fi
222
+ }
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bash
2
+ # Sourced helper: resolves the branch-scoped devlog file for the branch
3
+ # currently checked out in a working directory. See
4
+ # docs/design/branch-scoped-devlog.md.
5
+
6
+ _DEVLOG_PATH_DIR="$(cd "${BASH_SOURCE[0]%/*}" && pwd)"
7
+ # shellcheck source=devlog-lock.sh
8
+ . "$_DEVLOG_PATH_DIR/devlog-lock.sh"
9
+
10
+ # Turns an arbitrary branch/worktree name into a safe devlog.<name>.md
11
+ # filename segment: anything outside [A-Za-z0-9._-] becomes '-', repeats
12
+ # collapse, leading/trailing '-' trimmed.
13
+ _devlog_sanitize_name() {
14
+ printf '%s' "$1" | sed -E 's/[^A-Za-z0-9._-]/-/g; s/-+/-/g; s/^-+//; s/-+$//'
15
+ }
16
+
17
+ # Sets DEVLOG_DIR and DEVLOG_FILE for the branch currently checked out in
18
+ # $1 (defaults to "."). main/master (any case) and anything that isn't a
19
+ # git repo or has no resolvable branch keep the shared devlog.md. Any
20
+ # other branch gets devlog.<sanitized-branch>.md; a detached HEAD (or an
21
+ # unborn branch, which git also reports as "HEAD" here) falls back to the
22
+ # working directory's own basename. The first time a branch resolves to a
23
+ # file that doesn't exist yet while devlog.md already has content, the
24
+ # existing devlog.md is renamed (not copied) into that branch's file.
25
+ devlog_resolve_paths() {
26
+ local dir="${1:-.}"
27
+ DEVLOG_DIR="$dir/.devlog"
28
+ DEVLOG_FILE="$DEVLOG_DIR/devlog.md"
29
+
30
+ local branch raw name
31
+ branch="$(git -C "$dir" rev-parse --abbrev-ref HEAD 2>/dev/null || echo '')"
32
+
33
+ case "$branch" in
34
+ ''|[Mm][Aa][Ii][Nn]|[Mm][Aa][Ss][Tt][Ee][Rr])
35
+ return 0
36
+ ;;
37
+ HEAD)
38
+ raw="$(basename "$(cd "$dir" 2>/dev/null && pwd)" 2>/dev/null || echo '')"
39
+ ;;
40
+ *)
41
+ raw="$branch"
42
+ ;;
43
+ esac
44
+ [ -n "$raw" ] || return 0
45
+
46
+ name="$(_devlog_sanitize_name "$raw")"
47
+ if [ -z "$name" ]; then
48
+ # Sanitizing stripped every character (e.g. an all-CJK branch name).
49
+ # Falling back to the shared devlog.md here would silently defeat this
50
+ # whole feature for exactly the case it's most likely to hit in a
51
+ # Chinese-language project. Derive a short, stable, ASCII-safe name
52
+ # from the original string instead.
53
+ name="b-$(printf '%s' "$raw" | cksum | awk '{print $1}')"
54
+ fi
55
+
56
+ # Reserved namespace: devlog.archive.md and devlog.lessons.*.md already
57
+ # mean something else in this plugin. A branch name that sanitizes to
58
+ # one of these shares the plain devlog.md instead of colliding with them.
59
+ case "$name" in
60
+ archive|lessons.*) return 0 ;;
61
+ esac
62
+
63
+ local resolved="$DEVLOG_DIR/devlog.$name.md"
64
+ if [ -f "$DEVLOG_DIR/.enabled" ] && [ ! -f "$resolved" ] && [ -f "$DEVLOG_FILE" ]; then
65
+ devlog_lock_acquire
66
+ mv "$DEVLOG_FILE" "$resolved" 2>/dev/null || true
67
+ devlog_lock_release
68
+ fi
69
+ DEVLOG_FILE="$resolved"
70
+ }