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.
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
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bash
2
- # Self-check for enforce-devlog.sh's Handoff subsection order/duplicate
3
- # check (Phase 2, docs/design/devlog-as-ssot-assessment.md). No framework —
2
+ # Self-check for enforce-devlog.sh's Handoff format gate: XML tag order/
3
+ # duplicate/unknown-tag checks and legacy-format blocking
4
+ # (docs/design/handoff-xml.md). No framework —
4
5
  # plain assert-and-exit, matching this repo's existing style.
5
6
  set -uo pipefail
6
7
 
@@ -14,11 +15,10 @@ mkdir -p "$DEVLOG_DIR"
14
15
  touch "$DEVLOG_DIR/.enabled"
15
16
 
16
17
  # A real (if minimal) git repo, only so the "full canonical order" fixture's
17
- # #### 檔案 + #### 工作區 pair below can carry a #### 工作區 that actually
18
- # matches live git — DONE rounds with a non-empty #### 檔案 are now
19
- # machine-verified too (enforce-devlog.sh's workspace check, extended past
20
- # Phase 1's IN_PROGRESS/BLOCKED). Every other fixture in this file keeps
21
- # Status DONE with no #### 檔案, so it stays decoupled from this check.
18
+ # <files> + <workspace> pair below can carry a <workspace> that actually
19
+ # matches live git — DONE rounds with a non-empty <files> are
20
+ # machine-verified too. Every other fixture in this file keeps Status DONE
21
+ # with no <files>, so it stays decoupled from this check.
22
22
  git -C "$TMP_ROOT" init -q -b main
23
23
  git -C "$TMP_ROOT" config user.email test@example.com
24
24
  git -C "$TMP_ROOT" config user.name test
@@ -55,7 +55,7 @@ assert_round_merged() {
55
55
  }
56
56
 
57
57
  write_round() {
58
- # $1 = Handoff body (already includes #### subsection lines). Status DONE
58
+ # $1 = Handoff body (already includes the <handoff> block). Status DONE
59
59
  # throughout so this suite is decoupled from Phase 1's 工作區/下一步 checks.
60
60
  # Writes into .round-current.md — the file enforce-devlog.sh now validates
61
61
  # (round-start.sh already opened a skeleton there; this overwrites it).
@@ -78,40 +78,56 @@ write_round() {
78
78
 
79
79
  # --- full canonical order -> allowed -----------------------------------------
80
80
  bash "$SCRIPT_DIR/round-start.sh" < /dev/null
81
- write_round "#### 決策
81
+ write_round "<handoff>
82
+ <decisions>
82
83
  d
83
- #### 檔案
84
+ </decisions>
85
+ <files>
84
86
  尚未 commit:
85
- #### 工作區
87
+ </files>
88
+ <workspace>
86
89
  main @ ${HASH},工作樹乾淨
87
- #### 現況
90
+ </workspace>
91
+ <state>
88
92
  c
89
- #### 完成條件
93
+ </state>
94
+ <done-when>
90
95
  observable done.
91
- #### 下一步
92
- n"
96
+ </done-when>
97
+ <next>
98
+ n
99
+ </next>
100
+ </handoff>"
93
101
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
94
102
  assert_exit "full canonical order -> allowed" 0 $?
95
103
  assert_round_merged "full canonical order"
96
104
 
97
105
  # --- subset in order -> allowed -----------------------------------------------
98
106
  bash "$SCRIPT_DIR/round-start.sh" < /dev/null
99
- write_round "#### 決策
107
+ write_round "<handoff>
108
+ <decisions>
100
109
  d
101
- #### 現況
102
- c"
110
+ </decisions>
111
+ <state>
112
+ c
113
+ </state>
114
+ </handoff>"
103
115
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
104
116
  assert_exit "subset in canonical order -> allowed" 0 $?
105
117
  assert_round_merged "subset in canonical order"
106
118
 
107
119
  # --- reordered -> blocked -------------------------------------------------------
108
120
  bash "$SCRIPT_DIR/round-start.sh" < /dev/null
109
- write_round "#### 現況
121
+ write_round "<handoff>
122
+ <state>
110
123
  c
111
- #### 決策
112
- d"
124
+ </state>
125
+ <decisions>
126
+ d
127
+ </decisions>
128
+ </handoff>"
113
129
  MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
114
- assert_exit "現況 before 決策 -> blocked" 2 $?
130
+ assert_exit "state before decisions -> blocked" 2 $?
115
131
  case "$MSG" in
116
132
  *"順序錯了"*) echo "PASS: order-violation message" ;;
117
133
  *) echo "FAIL: expected 順序錯了 message, got: $MSG"; FAIL=1 ;;
@@ -119,32 +135,44 @@ esac
119
135
 
120
136
  # --- duplicate -> blocked -------------------------------------------------------
121
137
  bash "$SCRIPT_DIR/round-start.sh" < /dev/null
122
- write_round "#### 現況
138
+ write_round "<handoff>
139
+ <state>
123
140
  c1
124
- #### 現況
125
- c2"
141
+ </state>
142
+ <state>
143
+ c2
144
+ </state>
145
+ </handoff>"
126
146
  MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
127
- assert_exit "duplicate 現況 -> blocked" 2 $?
147
+ assert_exit "duplicate state -> blocked" 2 $?
128
148
  case "$MSG" in
129
- *"出現超過一次"*) echo "PASS: duplicate-subsection message" ;;
149
+ *"出現超過一次"*) echo "PASS: duplicate-tag message" ;;
130
150
  *) echo "FAIL: expected 出現超過一次 message, got: $MSG"; FAIL=1 ;;
131
151
  esac
132
152
 
133
- # --- unrecognized heading interleaved -> ignored, allowed ------------------------
153
+ # --- unknown tag -> blocked (spec rule 6: no silently ignored subsections) ---
134
154
  bash "$SCRIPT_DIR/round-start.sh" < /dev/null
135
- write_round "#### 決策
155
+ write_round "<handoff>
156
+ <decisions>
136
157
  d
137
- #### 其他備註
158
+ </decisions>
159
+ <notes>
138
160
  x
139
- #### 現況
140
- c"
141
- echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
142
- assert_exit "unrecognized #### heading interleaved -> ignored, allowed" 0 $?
143
- assert_round_merged "unrecognized #### heading interleaved"
161
+ </notes>
162
+ </handoff>"
163
+ MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
164
+ assert_exit "unknown tag -> blocked" 2 $?
165
+ case "$MSG" in
166
+ *"不認得的標籤"*) echo "PASS: unknown-tag message" ;;
167
+ *) echo "FAIL: expected 不認得的標籤 message, got: $MSG"; FAIL=1 ;;
168
+ esac
144
169
 
145
- # --- fenced example reordering subsection headings -> ignored, allowed -----
170
+ # --- fenced legacy example inside a field -> plain content, allowed --------
171
+ # XML fields don't track fences; a ``` block quoting old #### headings is
172
+ # just text in <decisions>.
146
173
  bash "$SCRIPT_DIR/round-start.sh" < /dev/null
147
- write_round "#### 決策
174
+ write_round "<handoff>
175
+ <decisions>
148
176
  說明格式時可以貼一段範例:
149
177
  \`\`\`markdown
150
178
  #### 現況
@@ -153,67 +181,107 @@ c
153
181
  d
154
182
  \`\`\`
155
183
  真正決定
156
- #### 現況
157
- c"
184
+ </decisions>
185
+ <state>
186
+ c
187
+ </state>
188
+ </handoff>"
158
189
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
159
- assert_exit "fenced example reordering subsection headings -> ignored, allowed" 0 $?
160
- assert_round_merged "fenced example reordering subsection headings"
190
+ assert_exit "fenced #### example inside <decisions> -> allowed" 0 $?
191
+ assert_round_merged "fenced #### example inside <decisions>"
161
192
 
162
- # --- fenced example duplicating a heading already used -> ignored, allowed -
163
- bash "$SCRIPT_DIR/round-start.sh" < /dev/null
164
- write_round "#### 決策
165
- 真正決策
166
- 說明格式時再貼一次範例:
167
- \`\`\`markdown
168
- #### 決策
169
- d2
170
- \`\`\`
171
- #### 現況
172
- c"
173
- echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
174
- assert_exit "fenced example duplicating a used heading -> ignored, allowed" 0 $?
175
- assert_round_merged "fenced example duplicating a used heading"
176
-
177
- # --- unterminated fence inside #### 決策 -> fail-open, not a false block
178
- # (final-review Fix 1). A ``` fence that never closes (odd fence-marker
179
- # count) must never make a present #### 現況 register as missing/malformed.
180
- # Status DONE so 下一步/工作區 (Phase 1) stay out of the picture — this only
181
- # exercises section_body()/ORDER_ERR's own fence handling.
193
+ # --- unterminated fence inside a field -> fail-open, allowed ----------------
182
194
  bash "$SCRIPT_DIR/round-start.sh" < /dev/null
183
- write_round "#### 決策
195
+ write_round "<handoff>
196
+ <decisions>
184
197
  一段沒收尾的範例:
185
198
  \`\`\`markdown
186
- 沒收尾內容,一路吃到 Handoff 結尾
187
- #### 現況
188
- c"
199
+ 沒收尾內容
200
+ </decisions>
201
+ <state>
202
+ c
203
+ </state>
204
+ </handoff>"
189
205
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
190
- assert_exit "unterminated fence in 決策, real 現況 after it -> fail-open, allowed" 0 $?
191
- assert_round_merged "unterminated fence in 決策, real 現況 after it"
192
-
193
- # --- same unterminated fence, but hiding a real duplicate below it --------
194
- # ORDER_ERR re-scans the extracted Handoff body with its own fence tracking;
195
- # pre-fix the same stuck-fence bug hid every #### heading after the broken
196
- # fence from ORDER_ERR too — including a genuine duplicate "#### 決策" that
197
- # should be rejected. NOFENCE degrades ORDER_ERR back to a plain scan for
198
- # this round, so the duplicate check reaches it again. (This is the mirror
199
- # image of the false-block case above: here the pre-fix bug wrongly *allowed*
200
- # something; the point is the same root cause, fixed the same way.)
206
+ assert_exit "unterminated fence in <decisions> -> allowed" 0 $?
207
+ assert_round_merged "unterminated fence in <decisions>"
208
+
209
+ # --- unterminated fence does not hide a real duplicate tag -------------------
201
210
  bash "$SCRIPT_DIR/round-start.sh" < /dev/null
202
- write_round "#### 決策
211
+ write_round "<handoff>
212
+ <decisions>
203
213
  一段沒收尾的範例:
204
214
  \`\`\`markdown
205
215
  沒收尾內容
206
- #### 現況
216
+ </decisions>
217
+ <state>
207
218
  c
208
- #### 決策
209
- d2(應該被判定為重複)"
219
+ </state>
220
+ <decisions>
221
+ d2(應該被判定為重複)
222
+ </decisions>
223
+ </handoff>"
210
224
  MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
211
225
  assert_exit "unterminated fence no longer hides a real duplicate -> blocked" 2 $?
212
226
  case "$MSG" in
213
- *"出現超過一次"*) echo "PASS: duplicate-subsection message despite unterminated fence" ;;
227
+ *"出現超過一次"*) echo "PASS: duplicate-tag message despite unterminated fence" ;;
214
228
  *) echo "FAIL: expected 出現超過一次 message, got: $MSG"; FAIL=1 ;;
215
229
  esac
216
230
 
231
+ # --- XML gate (docs/design/handoff-xml.md) ---
232
+ write_handoff_round() { # $1 = Handoff body lines (already formatted)
233
+ {
234
+ echo "## Round 1 — 2026-09-26T00:00:00+08:00"
235
+ echo ""
236
+ echo "### Summary"; echo "s"; echo ""
237
+ echo "### Reply"; echo "r"; echo ""
238
+ echo "### Handoff"
239
+ printf '%s\n' "$1"
240
+ echo ""
241
+ echo "### Status"; echo "DONE"
242
+ } > "$DEVLOG_DIR/.round-current.md"
243
+ }
244
+
245
+ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
246
+ write_handoff_round '#### 現況
247
+ legacy'
248
+ MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
249
+ assert_exit "legacy Handoff blocked" 2 $?
250
+ case "$MSG" in
251
+ *"migrate-handoff.sh"*"<handoff>"*"npx devlog-tracker init"*) echo "PASS: legacy message has migrate, template, init hint" ;;
252
+ *) echo "FAIL: legacy message: $MSG"; FAIL=1 ;;
253
+ esac
254
+
255
+ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
256
+ write_handoff_round '<handoff>
257
+ <state>
258
+ ok
259
+ </state>
260
+ </handoff>'
261
+ echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
262
+ assert_exit "XML Handoff DONE passes" 0 $?
263
+
264
+ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
265
+ write_handoff_round '<handoff>
266
+ <next>
267
+ x
268
+ </next>
269
+ <state>
270
+ y
271
+ </state>
272
+ </handoff>'
273
+ MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
274
+ assert_exit "XML order violation blocked" 2 $?
275
+ case "$MSG" in *"順序"*"### Handoff"*"<handoff>"*) echo "PASS: order message + template" ;; *) echo "FAIL: order msg: $MSG"; FAIL=1 ;; esac
276
+
277
+ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
278
+ write_handoff_round '<handoff>
279
+ <state>
280
+ y
281
+ </state>'
282
+ echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
283
+ assert_exit "unclosed handoff blocked" 2 $?
284
+
217
285
  if [ "$FAIL" -eq 0 ]; then
218
286
  echo "All checks passed."
219
287
  exit 0
@@ -4,6 +4,8 @@
4
4
  set -uo pipefail
5
5
 
6
6
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
7
+ # shellcheck source=lib/xml-fixture.sh
8
+ . "$SCRIPT_DIR/tests/lib/xml-fixture.sh"
7
9
  TMP_ROOT="$(mktemp -d)"
8
10
  trap 'rm -rf "$TMP_ROOT"' EXIT
9
11
 
@@ -79,6 +81,7 @@ write_unfinished() {
79
81
  echo "### Status"
80
82
  echo "$status"
81
83
  } > "$DEVLOG_DIR/.round-current.md"
84
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
82
85
  }
83
86
 
84
87
  # 1) IN_PROGRESS without Session Handoff -> blocked
@@ -87,8 +90,8 @@ write_unfinished IN_PROGRESS ""
87
90
  MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
88
91
  assert_exit "IN_PROGRESS without Session Handoff -> blocked" 2 $?
89
92
  case "$MSG" in
90
- *"Session Handoff"*) echo "PASS: message mentions Session Handoff" ;;
91
- *) echo "FAIL: expected Session Handoff in message, got: $MSG"; FAIL=1 ;;
93
+ *"<session-handoff>"*) echo "PASS: message shows <session-handoff> template" ;;
94
+ *) echo "FAIL: expected <session-handoff> in message, got: $MSG"; FAIL=1 ;;
92
95
  esac
93
96
 
94
97
  # 2) IN_PROGRESS with Session Handoff -> write handoff.md
@@ -97,7 +100,7 @@ write_unfinished IN_PROGRESS "$SESSION_OK"
97
100
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
98
101
  assert_exit "IN_PROGRESS with Session Handoff -> allowed" 0 $?
99
102
  if [ -f "$DEVLOG_DIR/handoff.md" ] && grep -q 'pick route A' "$DEVLOG_DIR/handoff.md" \
100
- && grep -q '^## Session Handoff' "$DEVLOG_DIR/handoff.md"; then
103
+ && grep -q '^<session-handoff>$' "$DEVLOG_DIR/handoff.md"; then
101
104
  echo "PASS: handoff.md written from Session Handoff"
102
105
  else
103
106
  echo "FAIL: handoff.md missing or wrong"; FAIL=1
@@ -144,6 +147,7 @@ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
144
147
  echo "### Status"
145
148
  echo "DONE"
146
149
  } > "$DEVLOG_DIR/.round-current.md"
150
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
147
151
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
148
152
  assert_exit "DONE -> allowed" 0 $?
149
153
  if [ ! -f "$DEVLOG_DIR/handoff.md" ]; then
@@ -152,27 +156,104 @@ else
152
156
  echo "FAIL: handoff.md survived DONE"; FAIL=1
153
157
  fi
154
158
 
155
- # 5) wrong order -> blocked; existing handoff unchanged
159
+ # 5) wrong order -> blocked; existing handoff unchanged. Written as XML by
160
+ # hand: the converter skips a Session Handoff in the wrong order, so a legacy
161
+ # fixture would be stopped by the legacy gate instead of the order check.
156
162
  echo "keep-me" > "$DEVLOG_DIR/handoff.md"
157
163
  bash "$SCRIPT_DIR/round-start.sh" < /dev/null
158
164
  write_unfinished IN_PROGRESS "
159
165
  ### Session Handoff
160
-
161
- #### 待解問題
166
+ <session-handoff>
167
+ <open-questions>
162
168
  - x
163
-
164
- #### 決策
169
+ </open-questions>
170
+ <decisions>
165
171
  - y
166
-
167
- #### 失敗嘗試
172
+ </decisions>
173
+ <failed-attempts>
168
174
  - z
175
+ </failed-attempts>
176
+ </session-handoff>
169
177
  "
170
- echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
178
+ MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
171
179
  assert_exit "wrong Session Handoff order -> blocked" 2 $?
180
+ case "$MSG" in
181
+ *"順序"*) echo "PASS: wrong order message mentions 順序" ;;
182
+ *) echo "FAIL: expected 順序 in message, got: $MSG"; FAIL=1 ;;
183
+ esac
172
184
  if [ -f "$DEVLOG_DIR/handoff.md" ] && grep -q 'keep-me' "$DEVLOG_DIR/handoff.md"; then
173
185
  echo "PASS: handoff.md unchanged on validation failure"
174
186
  else
175
187
  echo "FAIL: handoff.md should be untouched on validation failure"; FAIL=1
176
188
  fi
177
189
 
190
+ # 6) legacy Session Handoff under an XML Handoff -> blocked with migrate hint
191
+ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
192
+ write_unfinished IN_PROGRESS "$SESSION_OK"
193
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
194
+ # re-inject a legacy Session Handoff after conversion
195
+ awk '/^### Session Handoff/{skip=1; print "### Session Handoff\n#### 決策\n- x\n#### 待解問題\n- y\n#### 失敗嘗試\n- z\n"; next} skip && /^### /{skip=0} !skip' \
196
+ "$DEVLOG_DIR/.round-current.md" > "$DEVLOG_DIR/.rc" && mv "$DEVLOG_DIR/.rc" "$DEVLOG_DIR/.round-current.md"
197
+ MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
198
+ assert_exit "legacy Session Handoff blocked" 2 $?
199
+ case "$MSG" in *"migrate-handoff.sh"*) echo "PASS: legacy session message" ;; *) echo "FAIL: $MSG"; FAIL=1 ;; esac
200
+
201
+ # 7) DONE round quoting a fenced ### Session Handoff example in its Reply, no
202
+ # real Session Handoff -> allowed (presence check must ignore fences)
203
+ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
204
+ cat > "$DEVLOG_DIR/.round-current.md" <<'EOF2'
205
+ ## Round 1 — 2026-09-22T02:00:00+08:00
206
+
207
+ ### Summary
208
+ done
209
+
210
+ ### Reply
211
+ 範例格式:
212
+ ```markdown
213
+ ### Session Handoff
214
+ <session-handoff>
215
+ <decisions>
216
+ - x
217
+ </decisions>
218
+ </session-handoff>
219
+ ```
220
+
221
+ ### Handoff
222
+ <handoff>
223
+ <state>
224
+ finished
225
+ </state>
226
+ </handoff>
227
+
228
+ ### Status
229
+ DONE
230
+ EOF2
231
+ MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
232
+ assert_exit "DONE with fenced Session Handoff example -> allowed" 0 $?
233
+ [ -n "$MSG" ] && echo "note: stderr was: $MSG"
234
+
235
+ # 8) IN_PROGRESS with only a look-alike heading (### Session Handoff(補充))
236
+ # -> blocked as missing: the writer only accepts the exact heading
237
+ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
238
+ write_unfinished IN_PROGRESS "
239
+ ### Session Handoff(補充)
240
+ <session-handoff>
241
+ <decisions>
242
+ - x
243
+ </decisions>
244
+ <open-questions>
245
+ - y
246
+ </open-questions>
247
+ <failed-attempts>
248
+ - z
249
+ </failed-attempts>
250
+ </session-handoff>
251
+ "
252
+ MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
253
+ assert_exit "look-alike Session Handoff heading -> blocked" 2 $?
254
+ case "$MSG" in
255
+ *"缺少 ### Session Handoff"*) echo "PASS: look-alike heading reported as missing" ;;
256
+ *) echo "FAIL: expected missing Session Handoff message, got: $MSG"; FAIL=1 ;;
257
+ esac
258
+
178
259
  [ "$FAIL" -eq 0 ] && exit 0 || exit 1
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env bash
2
- # Self-check for enforce-devlog.sh's #### 工作區 machine-verify (Phase 1,
2
+ # Self-check for enforce-devlog.sh's <workspace> machine-verify (Phase 1,
3
3
  # docs/design/devlog-as-ssot-assessment.md). Separate file from
4
4
  # test-enforce-devlog.sh to avoid growing that suite further; still
5
5
  # auto-discovered by 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
 
@@ -89,6 +91,7 @@ write_round() {
89
91
  echo "### Status"
90
92
  echo "IN_PROGRESS"
91
93
  } > "$DEVLOG_DIR/.round-current.md"
94
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
92
95
  }
93
96
 
94
97
  # --- exact match -> allowed --------------------------------------------------
@@ -150,6 +153,7 @@ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
150
153
  echo "### Status"
151
154
  echo "BLOCKED"
152
155
  } > "$DEVLOG_DIR/.round-current.md"
156
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
153
157
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
154
158
  assert_exit "BLOCKED with missing workspace -> blocked" 2 $?
155
159
 
@@ -171,12 +175,15 @@ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
171
175
  echo "### Status"
172
176
  echo "DONE"
173
177
  } > "$DEVLOG_DIR/.round-current.md"
178
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
174
179
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
175
180
  assert_exit "DONE with no workspace section -> allowed (check does not apply)" 0 $?
176
181
  assert_round_merged "DONE with no workspace section"
177
182
 
178
- # --- fenced example quoting #### 工作區 before the real section -> ignored,
179
- # extractor still finds the real (unfenced) 工作區 body -------------------
183
+ # --- fenced example quoting a legacy `#### 工作區` inside <decisions> before
184
+ # the real <workspace> -> the quoted text stays plain content of <decisions>
185
+ # after xml_fixture, and the workspace check still reads the real <workspace>
186
+ # body -------------------------------------------------------------------
180
187
  bash "$SCRIPT_DIR/round-start.sh" < /dev/null
181
188
  {
182
189
  echo "## Round 4 — 2026-09-10T00:15:00+08:00"
@@ -219,17 +226,15 @@ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
219
226
  echo "### Status"
220
227
  echo "IN_PROGRESS"
221
228
  } > "$DEVLOG_DIR/.round-current.md"
229
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
222
230
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
223
231
  assert_exit "fenced example quoting #### 工作區 before the real section -> allowed" 0 $?
224
232
  assert_round_merged "fenced example quoting #### 工作區 before the real section"
225
233
 
226
- # --- unterminated fence in an earlier subsection -> fail-open, not a false
227
- # block (final-review Fix 1). A ``` fence that never closes (odd fence-marker
228
- # count) must never make a present, filled-in #### 下一步 register as missing.
229
- # Pre-fix, the odd fence opened by #### 現況's (malformed) example got
230
- # `fence` stuck at 1 for the rest of the round, so handoff_subsection_body()
231
- # could no longer even find the "#### 下一步" heading — even though #### 工作區
232
- # above it is exactly correct and #### 下一步 below it has real content.
234
+ # --- unterminated fence in an earlier field -> not a false block. A ```
235
+ # fence that never closes must never make a present, filled-in <next>
236
+ # register as missing: XML fields don't track fences at all. Written as XML
237
+ # directly — the legacy converter leaves odd-fence rounds untouched.
233
238
  bash "$SCRIPT_DIR/round-start.sh" < /dev/null
234
239
  {
235
240
  echo "## Round 5 — 2026-09-10T00:20:00+08:00"
@@ -241,28 +246,36 @@ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
241
246
  echo "fixture reply."
242
247
  echo ""
243
248
  echo "### Handoff"
244
- echo "#### 工作區"
249
+ echo "<handoff>"
250
+ echo "<workspace>"
245
251
  echo "main @ ${HASH},工作樹乾淨"
246
- echo "#### 現況"
252
+ echo "</workspace>"
253
+ echo "<state>"
247
254
  echo "一段沒收尾的範例:"
248
255
  echo '```markdown'
249
256
  echo "沒收尾內容,一路吃到這個 Round 結尾"
250
- echo "#### 完成條件"
257
+ echo "</state>"
258
+ echo "<done-when>"
251
259
  echo "observable done via test."
252
- echo "#### 下一步"
260
+ echo "</done-when>"
261
+ echo "<next>"
253
262
  echo "edit hooks/scripts/enforce-devlog.sh"
263
+ echo "</next>"
264
+ echo "</handoff>"
254
265
  echo ""
255
266
  echo ""
256
267
  echo "### Session Handoff"
257
- echo ""
258
- echo "#### 決策"
268
+ echo "<session-handoff>"
269
+ echo "<decisions>"
259
270
  echo "- (無)"
260
- echo ""
261
- echo "#### 待解問題"
271
+ echo "</decisions>"
272
+ echo "<open-questions>"
262
273
  echo "- fixture open"
263
- echo ""
264
- echo "#### 失敗嘗試"
274
+ echo "</open-questions>"
275
+ echo "<failed-attempts>"
265
276
  echo "- (無)"
277
+ echo "</failed-attempts>"
278
+ echo "</session-handoff>"
266
279
  echo ""
267
280
  echo "### Status"
268
281
  echo "IN_PROGRESS"
@@ -271,17 +284,17 @@ echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
271
284
  assert_exit "unterminated fence in 現況 -> fail-open, 下一步 still recognized" 0 $?
272
285
  assert_round_merged "unterminated fence in 現況"
273
286
 
274
- # --- DONE with a non-empty #### 檔案 (claims files were touched/committed)
287
+ # --- DONE with a non-empty <files> (claims files were touched/committed)
275
288
  # IS now checked: a DONE round that reports file changes but has no/wrong
276
- # #### 工作區 is exactly the "已 commit 完成" false-claim case
289
+ # <workspace> is exactly the "已 commit 完成" false-claim case
277
290
  # devlog-as-ssot-assessment.md flags as uncaught — machine-verify it the
278
291
  # same way IN_PROGRESS/BLOCKED already are. A trivial DONE round with no
279
- # #### 檔案 stays exempt (previous test above), matching SKILL.md's
292
+ # <files> stays exempt (previous test above), matching SKILL.md's
280
293
  # "瑣碎輪只留現況一句(沒有工作區)" convention untouched.
281
294
  #
282
- # The #### 檔案 body below is just "尚未 commit:" with no claimed paths —
283
- # this suite only cares about triggering the 工作區 check via a non-empty
284
- # #### 檔案, not about 檔案 content itself (that's
295
+ # The <files> body below is just "尚未 commit:" with no claimed paths —
296
+ # this suite only cares about triggering the <workspace> check via a non-empty
297
+ # <files>, not about <files> content itself (that's
285
298
  # test-enforce-devlog-files.sh's job, Phase 4). Any grammar-valid,
286
299
  # always-passing body works here. ------------------------------------------
287
300
  bash "$SCRIPT_DIR/round-start.sh" < /dev/null
@@ -303,6 +316,7 @@ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
303
316
  echo "### Status"
304
317
  echo "DONE"
305
318
  } > "$DEVLOG_DIR/.round-current.md"
319
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
306
320
  MSG_DONE="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
307
321
  assert_exit "DONE with #### 檔案 but no #### 工作區 -> blocked" 2 $?
308
322
  case "$MSG_DONE" in
@@ -331,6 +345,7 @@ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
331
345
  echo "### Status"
332
346
  echo "DONE"
333
347
  } > "$DEVLOG_DIR/.round-current.md"
348
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
334
349
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
335
350
  assert_exit "DONE with #### 檔案 and stale 工作區 hash -> blocked" 2 $?
336
351
 
@@ -355,6 +370,7 @@ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
355
370
  echo "### Status"
356
371
  echo "DONE"
357
372
  } > "$DEVLOG_DIR/.round-current.md"
373
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
358
374
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
359
375
  assert_exit "DONE with #### 檔案 and matching 工作區 -> allowed" 0 $?
360
376
  assert_round_merged "DONE with #### 檔案 and matching 工作區"