devlog-tracker 0.33.5 → 1.0.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 (51) hide show
  1. package/README.md +41 -10
  2. package/README.zh-TW.md +41 -10
  3. package/cli/agents-md.js +1 -0
  4. package/cli/agents-md.test.js +10 -0
  5. package/cli/platforms/claude.js +1 -0
  6. package/codex/hooks/on-interrupt.sh +13 -0
  7. package/codex/hooks/on-pre-tool.sh +69 -1
  8. package/codex/hooks/on-session-end.sh +5 -5
  9. package/codex/hooks/on-session-start.sh +5 -6
  10. package/codex/hooks/on-stop.sh +3 -2
  11. package/codex/hooks/on-subagent-start.sh +19 -0
  12. package/codex/hooks/project-dir.sh +16 -1
  13. package/codex/hooks/test-adapters.sh +127 -9
  14. package/codex/hooks.json +21 -0
  15. package/commands/continue.md +3 -3
  16. package/commands/keep-all.md +1 -1
  17. package/commands/keep.md +2 -2
  18. package/commands/lessons-on.md +1 -1
  19. package/commands/migrate.md +18 -0
  20. package/commands/pr.md +3 -3
  21. package/commands/resume.md +1 -1
  22. package/core/scripts/close-open-round.sh +8 -2
  23. package/core/scripts/devlog-md.sh +6 -11
  24. package/core/scripts/enforce-devlog.sh +79 -85
  25. package/core/scripts/handoff-convert.sh +187 -0
  26. package/core/scripts/handoff-fields.sh +219 -0
  27. package/core/scripts/handoff-file.sh +19 -88
  28. package/core/scripts/lessons-subagent-start.sh +1 -1
  29. package/core/scripts/migrate-handoff.sh +91 -0
  30. package/core/scripts/round-start.sh +13 -2
  31. package/core/scripts/segment-watch.sh +1 -1
  32. package/core/scripts/tests/lib/xml-fixture.sh +10 -0
  33. package/core/scripts/tests/test-close-open-round.sh +2 -1
  34. package/core/scripts/tests/test-devlog-md.sh +21 -0
  35. package/core/scripts/tests/test-enforce-devlog-files.sh +4 -1
  36. package/core/scripts/tests/test-enforce-devlog-handoff-order.sh +149 -81
  37. package/core/scripts/tests/test-enforce-devlog-session-handoff.sh +92 -11
  38. package/core/scripts/tests/test-enforce-devlog-workspace.sh +42 -26
  39. package/core/scripts/tests/test-enforce-devlog.sh +70 -0
  40. package/core/scripts/tests/test-handoff-fields.sh +171 -0
  41. package/core/scripts/tests/test-handoff-file.sh +67 -45
  42. package/core/scripts/tests/test-migrate-handoff.sh +278 -0
  43. package/core/scripts/tests/test-round-start.sh +54 -1
  44. package/core/scripts/tests/test-session-start-devlog.sh +45 -0
  45. package/core/scripts/timeline-render.js +26 -2
  46. package/core/scripts/timeline-render.test.js +23 -1
  47. package/package.json +3 -3
  48. package/skills/devlog-tracker/SKILL.md +80 -55
  49. package/skills/devlog-tracker/references/contract.md +4 -3
  50. package/skills/devlog-tracker/references/lessons-mode.md +7 -7
  51. package/skills/devlog-tracker/references/round-segments.md +11 -5
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env bash
2
+ # Sourced helper: field access for ### Handoff/### Session Handoff in both
3
+ # the legacy `#### 決策` form and the line-based XML form. Tag name == key.
4
+ # Not real XML — see docs/design/handoff-xml.md before changing anything.
5
+
6
+ # shellcheck disable=SC2034 # consumed by sourcing scripts
7
+ HANDOFF_KEYS="decisions files workspace state done-when next"
8
+ # shellcheck disable=SC2034
9
+ SESSION_HANDOFF_KEYS="decisions open-questions failed-attempts"
10
+
11
+ handoff_key_heading() {
12
+ case "$1" in
13
+ decisions) printf '決策' ;;
14
+ files) printf '檔案' ;;
15
+ workspace) printf '工作區' ;;
16
+ state) printf '現況' ;;
17
+ done-when) printf '完成條件' ;;
18
+ next) printf '下一步' ;;
19
+ open-questions) printf '待解問題' ;;
20
+ failed-attempts) printf '失敗嘗試' ;;
21
+ *) return 1 ;;
22
+ esac
23
+ }
24
+
25
+ # 1 when $1 has an odd number of ``` markers (unclosed fence): callers then
26
+ # stop tracking fences, same fail-open as devlog-md.sh/enforce-devlog.sh.
27
+ _hf_nofence() {
28
+ local n
29
+ n="$(printf '%s\n' "$1" | grep -c '^[ \t]*```' 2>/dev/null || true)"
30
+ case "$n" in ''|*[!0-9]*) n=0 ;; esac
31
+ if [ $((n % 2)) -eq 1 ]; then echo 1; else echo 0; fi
32
+ }
33
+
34
+ handoff_section_of() {
35
+ local blob="$1" name="$2" nofence
36
+ nofence="$(_hf_nofence "$blob")"
37
+ printf '%s\n' "$blob" | awk -v h="### $name" -v nofence="$nofence" '
38
+ /^[ \t]*```/ { if (!nofence) fence = !fence; if (grab) print; next }
39
+ !fence && !grab { t = $0; sub(/[ \t]+$/, "", t); if (t == h) { grab = 1; next } }
40
+ grab && !fence && /^###? / { exit }
41
+ grab { print }
42
+ '
43
+ }
44
+
45
+ handoff_format() {
46
+ local first nofence
47
+ first="$(printf '%s\n' "$1" | awk 'NF { sub(/^[ \t]+/, ""); sub(/[ \t]+$/, ""); print; exit }')"
48
+ case "$first" in
49
+ '<handoff>'|'<session-handoff>') echo xml; return 0 ;;
50
+ esac
51
+ nofence="$(_hf_nofence "$1")"
52
+ if printf '%s\n' "$1" | awk -v nofence="$nofence" '
53
+ /^[ \t]*```/ { if (!nofence) fence = !fence; next }
54
+ !fence && /^#### / { found = 1; exit }
55
+ END { exit(found ? 0 : 1) }
56
+ '; then
57
+ echo md
58
+ else
59
+ echo none
60
+ fi
61
+ }
62
+
63
+ handoff_field() {
64
+ local body="$1" key="$2" heading nofence
65
+ case "$(handoff_format "$body")" in
66
+ xml)
67
+ printf '%s\n' "$body" | awk -v tag="$key" '
68
+ { t = $0; sub(/^[ \t]+/, "", t); sub(/[ \t]+$/, "", t) }
69
+ !grab && t == "<" tag ">" { grab = 1; next }
70
+ grab && t == "</" tag ">" { exit }
71
+ grab { print }
72
+ '
73
+ ;;
74
+ md)
75
+ heading="$(handoff_key_heading "$key")" || return 0
76
+ nofence="$(_hf_nofence "$body")"
77
+ printf '%s\n' "$body" | awk -v h="$heading" -v nofence="$nofence" '
78
+ /^[ \t]*```/ { if (!nofence) fence = !fence; if (grab) print; next }
79
+ !fence && /^#### / {
80
+ if (grab) exit
81
+ name = $0; sub(/^#### [ \t]*/, "", name); sub(/[ \t]+$/, "", name)
82
+ if (name == h) grab = 1
83
+ next
84
+ }
85
+ grab && !fence && /^###? / { exit }
86
+ grab { print }
87
+ '
88
+ ;;
89
+ esac
90
+ }
91
+
92
+ handoff_xml_check() {
93
+ local body="$1" kind="$2" keys
94
+ case "$kind" in
95
+ handoff) keys="$HANDOFF_KEYS" ;;
96
+ session-handoff) keys="$SESSION_HANDOFF_KEYS" ;;
97
+ *) return 1 ;;
98
+ esac
99
+ printf '%s\n' "$body" | awk -v kind="$kind" -v keys="$keys" '
100
+ function fail(msg) { print msg; failed = 1; exit 1 }
101
+ function order_str( i, s) { s = k[1]; for (i = 2; i <= n; i++) s = s " → " k[i]; return s }
102
+ BEGIN { n = split(keys, k, " "); for (i = 1; i <= n; i++) ord[k[i]] = i; st = "before"; cur = ""; last = 0 }
103
+ {
104
+ t = $0; sub(/^[ \t]+/, "", t); sub(/[ \t]+$/, "", t)
105
+ if (st == "before") {
106
+ if (t == "") next
107
+ if (t == "<" kind ">") { st = "in"; next }
108
+ fail("第一個非空行必須是 <" kind ">(標籤自己一行),目前是:" t)
109
+ }
110
+ if (st == "after") {
111
+ if (t == "") next
112
+ fail("</" kind "> 外面不能有其他內容:" t)
113
+ }
114
+ if (cur != "") {
115
+ if (t == "</" cur ">") {
116
+ if (!body) fail("<" cur "> 是空的;沒有內容就整個標籤省略")
117
+ cur = ""; next
118
+ }
119
+ if (match(t, /^<\/?[a-z][a-z_-]*>/)) {
120
+ tag = substr(t, 1, RLENGTH); name = tag; gsub(/[<\/>]/, "", name)
121
+ if ((name in ord) || name == kind) {
122
+ if (t == tag) fail("<" cur "> 還沒用 </" cur "> 關閉就出現了 " tag)
123
+ fail(tag " 要自己一行:內容寫在下一行,結尾標籤另起一行。看不懂這一行:" t)
124
+ }
125
+ }
126
+ if (t != "") body = 1
127
+ next
128
+ }
129
+ if (t == "") next
130
+ if (t == "</" kind ">") { st = "after"; next }
131
+ if (t ~ /^<[a-z][a-z_-]*>$/) {
132
+ name = substr(t, 2, length(t) - 2)
133
+ if (!(name in ord)) fail("不認得的標籤 <" name ">;<" kind "> 裡可用的標籤:" keys)
134
+ if (seen[name]) fail("<" name "> 出現超過一次,請合併成一個")
135
+ if (ord[name] < last) fail("標籤順序錯了(應該是 " order_str() "):<" name "> 寫在 <" prev "> 後面")
136
+ seen[name] = 1; last = ord[name]; prev = name; cur = name; body = 0
137
+ next
138
+ }
139
+ if (t ~ /^<\/[a-z][a-z_-]*>$/) fail(t " 前面沒有對應的開頭標籤")
140
+ if (match(t, /^<\/?[a-z][a-z_-]*>/)) {
141
+ tag = substr(t, 1, RLENGTH); name = tag; gsub(/[<\/>]/, "", name)
142
+ if (name in ord) fail(tag " 要自己一行:內容寫在下一行,結尾標籤另起一行。看不懂這一行:" t)
143
+ fail("不認得的標籤 " tag ";<" kind "> 裡可用的標籤:" keys)
144
+ }
145
+ fail("<" kind "> 裡的內容要放在欄位標籤裡面,看不懂這一行:" t)
146
+ }
147
+ END {
148
+ if (failed) exit 1
149
+ if (st == "before") { print "缺少 <" kind "> 區塊"; exit 1 }
150
+ if (cur != "") { print "<" cur "> 還沒用 </" cur "> 關閉"; exit 1 }
151
+ if (st == "in") { print "缺少結尾的 </" kind ">"; exit 1 }
152
+ if (kind == "session-handoff") {
153
+ for (i = 1; i <= n; i++) if (!seen[k[i]]) { print "<session-handoff> 必須依序有 " keys "(沒有就寫 - (無)),缺 <" k[i] ">"; exit 1 }
154
+ }
155
+ }
156
+ '
157
+ }
158
+
159
+ handoff_xml_template() {
160
+ case "$1" in
161
+ handoff)
162
+ cat <<'EOF'
163
+ ### Handoff
164
+ <handoff>
165
+ <decisions>
166
+ 影響後續方向的選擇與理由(沒做選擇就整個標籤省略)
167
+ </decisions>
168
+ <files>
169
+ 尚未 commit:
170
+ 修改:path/to/file
171
+ </files>
172
+ <workspace>
173
+ main @ a1b2c3d,工作樹乾淨
174
+ </workspace>
175
+ <state>
176
+ 任務做到哪、卡在哪
177
+ </state>
178
+ <done-when>
179
+ 可觀察的做完判準(IN_PROGRESS/BLOCKED 必寫)
180
+ </done-when>
181
+ <next>
182
+ 下一輪第一件具體要做的事(IN_PROGRESS/BLOCKED 必寫)
183
+ </next>
184
+ </handoff>
185
+ EOF
186
+ ;;
187
+ session-handoff)
188
+ cat <<'EOF'
189
+ ### Session Handoff
190
+ <session-handoff>
191
+ <decisions>
192
+ - 仍影響後續方向的選擇;沒有就寫 - (無)
193
+ </decisions>
194
+ <open-questions>
195
+ - 下一 session 最該先看的卡點;沒有就寫 - (無)
196
+ </open-questions>
197
+ <failed-attempts>
198
+ - 試過但放棄的做法;沒有就寫 - (無)
199
+ </failed-attempts>
200
+ </session-handoff>
201
+ EOF
202
+ ;;
203
+ esac
204
+ }
205
+
206
+ handoff_legacy_message() {
207
+ local migrate="$1" project="$2"
208
+ printf '%s\n' "Handoff 還是舊的「#### 小節」格式,這一輪要改用 XML 標籤。先跑下面的指令,把 devlog 裡的舊格式 Handoff 一次轉好,再結束這一輪一次:"
209
+ printf '\n'
210
+ printf 'DEVLOG_PROJECT_DIR="%s" bash "%s"\n' "$project" "$migrate"
211
+ printf '\n'
212
+ printf '%s\n' "輸出的 SKIP 若列出這一輪,就照下面的模板手動改寫(標籤自己一行、沒有的欄位整個省略):"
213
+ printf '\n'
214
+ handoff_xml_template handoff
215
+ printf '\n'
216
+ handoff_xml_template session-handoff
217
+ printf '\n'
218
+ printf '%s\n' "給使用者:如果 agent 每一輪都還是寫舊格式,代表專案裡的 devlog-tracker skill/指令文件是舊版。請重跑 \`npx devlog-tracker init\`(Claude Code plugin 使用者改成更新 plugin),再執行 \`/devlog-tracker:start\`。"
219
+ }
@@ -1,98 +1,29 @@
1
1
  #!/usr/bin/env bash
2
- # Sourced helper: validate / extract / write / clear Session Handoff file.
3
- # See docs/design/session-handoff-file.md.
2
+ # Sourced helper: write / clear the Session Handoff file. Content is the
3
+ # round's <session-handoff> block verbatim (docs/design/handoff-xml.md,
4
+ # docs/design/session-handoff-file.md).
4
5
 
5
- # Odd ``` count → treat as no fence (same fail-open as enforce-devlog.sh).
6
- _handoff_nofence_flag() {
7
- local blob="$1" n
8
- n="$(printf '%s\n' "$blob" | grep -c '^[ \t]*```' 2>/dev/null || echo 0)"
9
- case "$n" in ''|*[!0-9]*) n=0 ;; esac
10
- if [ $((n % 2)) -eq 1 ]; then
11
- echo 1
12
- else
13
- echo 0
14
- fi
15
- }
16
-
17
- # Returns 0 if $1 (a Round blob) has ### Session Handoff with #### 決策 /
18
- # #### 待解問題 / #### 失敗嘗試 in that order, each with a non-empty body.
19
- handoff_session_section_ok() {
20
- local blob="$1" nofence
21
- nofence="$(_handoff_nofence_flag "$blob")"
22
- printf '%s\n' "$blob" | awk -v nofence="$nofence" '
23
- BEGIN { ok = 0 }
24
- /^[ \t]*```/ { if (!nofence) fence = !fence; next }
25
- fence { next }
26
- /^### Session Handoff[ \t]*$/ { in_sh = 1; next }
27
- in_sh && /^### / { exit }
28
- in_sh && /^## / { exit }
29
- in_sh && /^#### / {
30
- name = $0
31
- sub(/^#### [ \t]*/, "", name)
32
- sub(/[ \t]+$/, "", name)
33
- if (name == "決策" || name == "待解問題" || name == "失敗嘗試") {
34
- if (expecting_body) { exit }
35
- if (name == "決策") {
36
- if (seen_decision || seen_open || seen_failed) exit
37
- seen_decision = 1
38
- expecting_body = 1
39
- } else if (name == "待解問題") {
40
- if (!seen_decision || seen_open || seen_failed) exit
41
- seen_open = 1
42
- expecting_body = 1
43
- } else {
44
- if (!seen_decision || !seen_open || seen_failed) exit
45
- seen_failed = 1
46
- expecting_body = 1
47
- }
48
- }
49
- next
50
- }
51
- in_sh && expecting_body {
52
- if ($0 ~ /[^[:space:]]/) {
53
- expecting_body = 0
54
- if (seen_decision && seen_open && seen_failed) ok = 1
55
- }
56
- next
57
- }
58
- END { exit(ok && seen_decision && seen_open && seen_failed && !expecting_body ? 0 : 1) }
59
- '
60
- }
6
+ _HANDOFF_FILE_DIR="$(cd "${BASH_SOURCE[0]%/*}" && pwd)"
7
+ # shellcheck source=handoff-fields.sh
8
+ . "$_HANDOFF_FILE_DIR/handoff-fields.sh"
61
9
 
62
- # Print independent-file markdown from Round blob $1. Exit 1 if invalid.
63
- handoff_extract_file_body() {
64
- local blob="$1" nofence
65
- handoff_session_section_ok "$blob" || return 1
66
- nofence="$(_handoff_nofence_flag "$blob")"
67
- printf '%s\n' "$blob" | awk -v nofence="$nofence" '
68
- BEGIN { print "## Session Handoff"; print "" }
69
- /^[ \t]*```/ { if (!nofence) fence = !fence; next }
70
- fence { next }
71
- /^### Session Handoff[ \t]*$/ { in_sh = 1; next }
72
- in_sh && /^### / { exit }
73
- in_sh && /^## / { exit }
74
- in_sh && /^#### / {
75
- name = $0
76
- sub(/^#### [ \t]*/, "", name)
77
- sub(/[ \t]+$/, "", name)
78
- if (name == "決策" || name == "待解問題" || name == "失敗嘗試") {
79
- if (printing) print ""
80
- print "### " name
81
- printing = 1
82
- grab = 1
83
- next
84
- }
85
- grab = 0
86
- next
87
- }
88
- in_sh && grab { print }
89
- '
10
+ # Prints the trimmed <session-handoff> block of Round blob $1; exit 1 when
11
+ # absent or invalid.
12
+ handoff_session_block() {
13
+ local body
14
+ body="$(handoff_section_of "$1" 'Session Handoff' | awk '
15
+ { l[NR] = $0 } NF && !s { s = NR } NF { e = NR }
16
+ END { for (i = s; s && i <= e; i++) print l[i] }
17
+ ')"
18
+ [ -n "$body" ] || return 1
19
+ [ "$(handoff_format "$body")" = xml ] || return 1
20
+ handoff_xml_check "$body" session-handoff >/dev/null || return 1
21
+ printf '%s\n' "$body"
90
22
  }
91
23
 
92
- # Atomic write of extracted body to $1 from Round blob $2.
93
24
  handoff_write() {
94
25
  local path="$1" blob="$2" dir tmp body
95
- body="$(handoff_extract_file_body "$blob")" || return 1
26
+ body="$(handoff_session_block "$blob")" || return 1
96
27
  dir="$(dirname "$path")"
97
28
  mkdir -p "$dir" 2>/dev/null || true
98
29
  tmp="$(mktemp "$dir/.handoff.XXXXXX")" || return 1
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SubagentStart hook(Claude Code):Lessons Mode 開著時,把「可以自己呼叫
2
+ # SubagentStart hook(Claude Code/Codex):Lessons Mode 開著時,把「可以自己呼叫
3
3
  # lessons-append.sh」的說明注入 sub agent/Workflow agent 的 context
4
4
  # (docs/design/lessons-mode.md「sub agent/workflow 情境」)。
5
5
  # sub agent 的 Bash 裡沒有 CLAUDE_PROJECT_DIR,isolation: "worktree" 時 cwd
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env bash
2
+ # Converts legacy `#### ` Handoff/Session Handoff in the live devlog files to
3
+ # XML (docs/design/handoff-xml.md「Migrate」). Run by the agent when Stop
4
+ # blocks on a legacy Handoff, or by /devlog-tracker:migrate.
5
+ set -uo pipefail
6
+ _src="${BASH_SOURCE[0]}"
7
+ SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
8
+ # shellcheck source=handoff-convert.sh
9
+ . "$SCRIPT_DIR/handoff-convert.sh"
10
+ # shellcheck source=devlog-lock.sh
11
+ . "$SCRIPT_DIR/devlog-lock.sh"
12
+
13
+ PROJECT_DIR="${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}"
14
+ DEVLOG_DIR="$PROJECT_DIR/.devlog"
15
+ [ -d "$DEVLOG_DIR" ] || { echo NO_DEVLOG; exit 1; }
16
+
17
+ # The current branch's files, even when they predate the devlog-origin
18
+ # marker. Resolved before taking the lock: devlog_resolve_paths may take it
19
+ # itself. Outside git this just yields devlog.md/handoff.md.
20
+ # shellcheck source=devlog-path.sh
21
+ . "$SCRIPT_DIR/devlog-path.sh"
22
+ devlog_resolve_paths "$PROJECT_DIR" 2>/dev/null || true
23
+ CUR_DEVLOG="${DEVLOG_FILE:-}"
24
+ CUR_HANDOFF="${HANDOFF_FILE:-}"
25
+ DEVLOG_DIR="$PROJECT_DIR/.devlog"
26
+
27
+ devlog_lock_acquire
28
+ trap 'devlog_lock_release' EXIT
29
+ if [ -n "${LOCK_CONTENDED_BY:-}" ]; then echo "LOCKED ${LOCK_CONTENDED_BY}"; exit 1; fi
30
+
31
+ MIGRATED=0
32
+ SKIPPED=0
33
+ LINES=""
34
+ add_line() { LINES="${LINES}${1}"$'\n'; }
35
+
36
+ migrate_round_file() {
37
+ local f="$1" name rep n
38
+ name="${f##*/}"
39
+ rep="$(mktemp "$DEVLOG_DIR/.migrate.XXXXXX")" || return 0
40
+ handoff_convert_file "$f" "$f.migrate-tmp" "$rep" || { rm -f "$f.migrate-tmp" "$rep"; return 0; }
41
+ n="$(grep -c '^MIGRATED ' "$rep" || true)"
42
+ while read -r tag round reason; do
43
+ [ "$tag" = SKIP ] || continue
44
+ SKIPPED=$((SKIPPED + 1))
45
+ add_line "SKIP $name Round $round: $reason"
46
+ done < "$rep"
47
+ rm -f "$rep"
48
+ if [ "${n:-0}" -gt 0 ]; then
49
+ cp "$f" "$f.pre-migrate" && mv "$f.migrate-tmp" "$f" || { rm -f "$f.migrate-tmp"; return 0; }
50
+ MIGRATED=$((MIGRATED + n))
51
+ add_line "BACKUP=$f.pre-migrate"
52
+ else
53
+ rm -f "$f.migrate-tmp"
54
+ fi
55
+ }
56
+
57
+ migrate_snapshot_file() {
58
+ local f="$1" rc
59
+ handoff_convert_snapshot "$f" "$f.migrate-tmp"; rc=$?
60
+ if [ "$rc" -eq 0 ]; then
61
+ cp "$f" "$f.pre-migrate" && mv "$f.migrate-tmp" "$f" || { rm -f "$f.migrate-tmp"; return 0; }
62
+ add_line "BACKUP=$f.pre-migrate"
63
+ else
64
+ rm -f "$f.migrate-tmp"
65
+ [ "$rc" -eq 1 ] && add_line "SKIP ${f##*/}: 看不懂的 handoff 快照格式,保留原樣"
66
+ fi
67
+ }
68
+
69
+ # Each file at most once (the current branch file may also carry a marker).
70
+ DONE_FILES=$'\n'
71
+ first_visit() {
72
+ case "$DONE_FILES" in *$'\n'"$1"$'\n'*) return 1 ;; esac
73
+ DONE_FILES="${DONE_FILES}${1}"$'\n'
74
+ }
75
+
76
+ for f in "$DEVLOG_DIR/.round-current.md" "$DEVLOG_DIR/devlog.md" "$CUR_DEVLOG"; do
77
+ [ -n "$f" ] && [ -s "$f" ] && first_visit "$f" && migrate_round_file "$f"
78
+ done
79
+ for f in "$DEVLOG_DIR"/devlog.*.md; do
80
+ [ -s "$f" ] || continue
81
+ head -n 1 "$f" | grep -q '^<!-- devlog-origin: ' || continue
82
+ first_visit "$f" && migrate_round_file "$f"
83
+ done
84
+ for f in "$DEVLOG_DIR"/handoff.md "$CUR_HANDOFF" "$DEVLOG_DIR"/handoff.*.md; do
85
+ [ -n "$f" ] && [ -s "$f" ] && first_visit "$f" && migrate_snapshot_file "$f"
86
+ done
87
+
88
+ echo "MIGRATED=$MIGRATED"
89
+ echo "SKIPPED=$SKIPPED"
90
+ printf '%s' "$LINES"
91
+ exit 0
@@ -94,8 +94,19 @@ case "$PROMPT" in
94
94
  CMD_NAME="${CMD_NAME#/}"
95
95
  ;;
96
96
  esac
97
+ # Codex exposes project commands as skills. An explicit $devlog-name mention
98
+ # is the first token of the submitted prompt, rather than a command-name tag.
99
+ if [ -z "$CMD_NAME" ]; then
100
+ case "$PROMPT" in
101
+ \$devlog-*)
102
+ CMD_NAME="${PROMPT%%[[:space:]]*}"
103
+ CMD_NAME="${CMD_NAME#\$devlog-}"
104
+ CMD_NAME="devlog-tracker:$CMD_NAME"
105
+ ;;
106
+ esac
107
+ fi
97
108
  case "$CMD_NAME" in
98
- devlog-tracker:checkpoint|devlog-tracker:clean|devlog-tracker:compact|devlog-tracker:keep|devlog-tracker:keep-all|devlog-tracker:lessons|devlog-tracker:lessons-drift|devlog-tracker:lessons-off|devlog-tracker:lessons-on|devlog-tracker:overview|devlog-tracker:pause|devlog-tracker:report|devlog-tracker:search|devlog-tracker:segment-watch|devlog-tracker:span|devlog-tracker:start|devlog-tracker:status|devlog-tracker:timeline)
109
+ devlog-tracker:checkpoint|devlog-tracker:clean|devlog-tracker:compact|devlog-tracker:keep|devlog-tracker:keep-all|devlog-tracker:lessons|devlog-tracker:lessons-drift|devlog-tracker:lessons-off|devlog-tracker:lessons-on|devlog-tracker:migrate|devlog-tracker:overview|devlog-tracker:pause|devlog-tracker:report|devlog-tracker:search|devlog-tracker:segment-watch|devlog-tracker:span|devlog-tracker:start|devlog-tracker:status|devlog-tracker:timeline)
99
110
  exit 0
100
111
  ;;
101
112
  esac
@@ -165,7 +176,7 @@ if [ "$SPAN_SKIP" -eq 0 ] && [ "$TASK_NOTIF" -eq 0 ] && [ -f "$DEVLOG_FILE" ]; t
165
176
  LIVE_WS="$(workspace_snapshot "$PROJECT_DIR")"
166
177
  if [ -n "$LIVE_WS" ]; then
167
178
  printf '%s\n' "$LIVE_WS" > "$MISMATCH_FILE" 2>/dev/null || true
168
- printf '%s\n' "上一輪 Handoff「#### 工作區」跟目前 git 不符。先在這一輪追加 ### 段落,寫宣稱 vs 實際(實際用下面「實際」逐字內容),再依實際工作樹行動,不要照上一輪「現況/下一步」的字面。"
179
+ printf '%s\n' "上一輪 Handoff 的工作區(\`<workspace>\`;舊格式是 \`#### 工作區\`)跟目前 git 不符。先在這一輪追加 ### 段落,寫宣稱 vs 實際(實際用下面「實際」逐字內容),再依實際工作樹行動,不要照上一輪「現況/下一步」的字面。"
169
180
  printf '\n宣稱:\n%s\n\n實際:\n%s\n' "$CLAIMED_WS" "$LIVE_WS"
170
181
 
171
182
  if [ -f "$DEVLOG_DIR/.lessons-enabled" ]; then
@@ -103,7 +103,7 @@ if [ -f "$MISMATCH_FILE" ]; then
103
103
  *"$mark_flat"*) rm -f "$MISMATCH_FILE" 2>/dev/null || true ;;
104
104
  *)
105
105
  if ! is_devlog_tool_allowed "$TOOL_NAME" "$FILE_PATH" "$COMMAND"; then
106
- echo "上一輪「#### 工作區」跟目前 git 不符。請先 Read .devlog/.round-current.md,再用 Edit/StrReplace 在這一輪追加 ### 段落,把下面「實際」逐字貼進段落(宣稱 vs 實際)。寫完再呼叫其他工具。不要照上一輪 Handoff「現況/下一步」的字面行動。(唯讀的 git status/diff/log/show/rev-parse 仍可執行,方便自行核對。)" >&2
106
+ echo "上一輪 Handoff 的工作區(\`<workspace>\`;舊格式是 \`#### 工作區\`)跟目前 git 不符。請先 Read .devlog/.round-current.md,再用 Edit/StrReplace 在這一輪追加 ### 段落,把下面「實際」逐字貼進段落(宣稱 vs 實際)。寫完再呼叫其他工具。不要照上一輪 Handoff「現況/下一步」的字面行動。(唯讀的 git status/diff/log/show/rev-parse 仍可執行,方便自行核對。)" >&2
107
107
  echo "" >&2
108
108
  echo "實際:" >&2
109
109
  printf '%s\n' "$LIVE_MARK" >&2
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env bash
2
+ # Test helper: convert a legacy-format fixture file to XML in place.
3
+ _XML_FIXTURE_DIR="$(cd "${BASH_SOURCE[0]%/*}/../.." && pwd)"
4
+ # shellcheck source=../../handoff-convert.sh
5
+ . "$_XML_FIXTURE_DIR/handoff-convert.sh"
6
+ xml_fixture() {
7
+ local f="$1"
8
+ handoff_convert_file "$f" "$f.xml" "$f.xmlrep" && mv "$f.xml" "$f"
9
+ rm -f "$f.xmlrep"
10
+ }
@@ -88,7 +88,8 @@ BODY="$(cat "$DEVLOG_DIR/devlog.md")"
88
88
  assert_contains "status INTERRUPTED" $'### Status\nINTERRUPTED' "$BODY"
89
89
  assert_contains "reason line" "StopFailure:server_error" "$BODY"
90
90
  assert_contains "summary stub (not recovered)" "devlog.md 沒有留下任何後續處理紀錄就結束了" "$BODY"
91
- assert_contains "handoff stub heading" "#### 現況" "$BODY"
91
+ assert_contains "handoff stub open" "<handoff>" "$BODY"
92
+ assert_contains "handoff stub block" "<state>" "$BODY"
92
93
  assert_contains "handoff stub body (not recovered)" "這一輪沒有留下任何處理紀錄" "$BODY"
93
94
  assert_contains "user input kept" "hello" "$BODY"
94
95
 
@@ -352,6 +352,27 @@ EXPECTED_BODY='feat/foo @ abc1234
352
352
  未提交:a.txt'
353
353
  assert_eq "workspace body two lines" "$EXPECTED_BODY" "$WS_BODY"
354
354
 
355
+ cat > "$TMP_ROOT/xmlws.md" <<'EOF'
356
+ ## Round 1 — t
357
+
358
+ ### Handoff
359
+ <handoff>
360
+ <workspace>
361
+ main @ abc1234
362
+ 未提交:a.txt
363
+ </workspace>
364
+ <next>
365
+ x
366
+ </next>
367
+ </handoff>
368
+
369
+ ### Status
370
+ IN_PROGRESS
371
+ EOF
372
+ XMLWS_END="$(wc -l < "$TMP_ROOT/xmlws.md" | tr -d ' ')"
373
+ assert_eq "workspace body from XML" "main @ abc1234
374
+ 未提交:a.txt" "$(devlog_round_workspace_body "$TMP_ROOT/xmlws.md" 1 "$XMLWS_END")"
375
+
355
376
  cat > "$TMP_ROOT/segments.md" <<'EOF'
356
377
  ## Round 1 — 2026-09-11T00:00:00+08:00
357
378
 
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env bash
2
- # Self-check for enforce-devlog.sh's #### 檔案 machine-verify (Phase 4,
2
+ # Self-check for enforce-devlog.sh's <files> machine-verify (Phase 4,
3
3
  # docs/design/files-verify.md). Separate file from test-enforce-devlog.sh
4
4
  # and test-enforce-devlog-workspace.sh; still auto-discovered by
5
5
  # run-tests.sh's tests/test-*.sh glob.
6
6
  set -uo pipefail
7
7
 
8
8
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
9
+ # shellcheck source=lib/xml-fixture.sh
10
+ . "$SCRIPT_DIR/tests/lib/xml-fixture.sh"
9
11
  TMP_ROOT="$(mktemp -d)"
10
12
  trap 'rm -rf "$TMP_ROOT"' EXIT
11
13
 
@@ -96,6 +98,7 @@ write_round() {
96
98
  echo "### Status"
97
99
  echo "$status"
98
100
  } > "$DEVLOG_DIR/.round-current.md"
101
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
99
102
  }
100
103
 
101
104
  ws_clean() {