devlog-tracker 0.33.5 → 0.34.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.
- package/README.md +41 -10
- package/README.zh-TW.md +41 -10
- package/cli/agents-md.js +1 -0
- package/cli/agents-md.test.js +10 -0
- package/cli/platforms/claude.js +1 -0
- package/codex/hooks/on-interrupt.sh +13 -0
- package/codex/hooks/on-pre-tool.sh +69 -1
- package/codex/hooks/on-session-end.sh +5 -5
- package/codex/hooks/on-session-start.sh +5 -6
- package/codex/hooks/on-stop.sh +3 -2
- package/codex/hooks/on-subagent-start.sh +19 -0
- package/codex/hooks/project-dir.sh +16 -1
- package/codex/hooks/test-adapters.sh +127 -9
- package/codex/hooks.json +21 -0
- package/commands/continue.md +3 -3
- package/commands/keep-all.md +1 -1
- package/commands/keep.md +2 -2
- package/commands/lessons-on.md +1 -1
- package/commands/migrate.md +18 -0
- package/commands/pr.md +3 -3
- package/commands/resume.md +1 -1
- package/core/scripts/close-open-round.sh +8 -2
- package/core/scripts/devlog-md.sh +6 -11
- package/core/scripts/enforce-devlog.sh +79 -85
- package/core/scripts/handoff-convert.sh +187 -0
- package/core/scripts/handoff-fields.sh +219 -0
- package/core/scripts/handoff-file.sh +19 -88
- package/core/scripts/lessons-subagent-start.sh +1 -1
- package/core/scripts/migrate-handoff.sh +91 -0
- package/core/scripts/round-start.sh +13 -2
- package/core/scripts/segment-watch.sh +1 -1
- package/core/scripts/tests/lib/xml-fixture.sh +10 -0
- package/core/scripts/tests/test-close-open-round.sh +2 -1
- package/core/scripts/tests/test-devlog-md.sh +21 -0
- package/core/scripts/tests/test-enforce-devlog-files.sh +4 -1
- package/core/scripts/tests/test-enforce-devlog-handoff-order.sh +149 -81
- package/core/scripts/tests/test-enforce-devlog-session-handoff.sh +92 -11
- package/core/scripts/tests/test-enforce-devlog-workspace.sh +42 -26
- package/core/scripts/tests/test-enforce-devlog.sh +70 -0
- package/core/scripts/tests/test-handoff-fields.sh +171 -0
- package/core/scripts/tests/test-handoff-file.sh +67 -45
- package/core/scripts/tests/test-migrate-handoff.sh +278 -0
- package/core/scripts/tests/test-round-start.sh +54 -1
- package/core/scripts/tests/test-session-start-devlog.sh +45 -0
- package/core/scripts/timeline-render.js +26 -2
- package/core/scripts/timeline-render.test.js +23 -1
- package/package.json +3 -3
- package/skills/devlog-tracker/SKILL.md +80 -55
- package/skills/devlog-tracker/references/contract.md +4 -3
- package/skills/devlog-tracker/references/lessons-mode.md +7 -7
- 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:
|
|
3
|
-
#
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
#
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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="$(
|
|
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
|
|
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 "
|
|
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
|
|
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
|
|
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() {
|