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
@@ -6,6 +6,8 @@
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
 
@@ -82,6 +84,7 @@ fixture
82
84
  ### Status
83
85
  DONE
84
86
  EOF
87
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
85
88
  }
86
89
 
87
90
  # Seeds *prior, already-closed* history directly into devlog.md — content
@@ -361,6 +364,7 @@ DONE
361
364
 
362
365
  ## Checkpoint(Round 2 摘要)
363
366
  EOF
367
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
364
368
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
365
369
  assert_exit "genuine checkpoint write above the corrected stored value -> allowed" 0 $?
366
370
  assert_round_merged "genuine checkpoint write above the corrected stored value" "## Checkpoint(Round 2 摘要)"
@@ -429,6 +433,7 @@ missing summary
429
433
  ### Status
430
434
  DONE
431
435
  DEVEOF
436
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
432
437
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
433
438
  assert_exit "last Round missing ### Summary -> blocked" 2 $?
434
439
 
@@ -446,6 +451,7 @@ has reply
446
451
  ### Status
447
452
  DONE
448
453
  DEVEOF
454
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
449
455
  HEADING_MISS_MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
450
456
  assert_exit "last Round missing ### Handoff -> blocked" 2 $?
451
457
  case "$HEADING_MISS_MSG" in
@@ -468,6 +474,7 @@ cat > "$DEVLOG_DIR/.round-current.md" <<'DEVEOF'
468
474
  ### Status
469
475
  DONE
470
476
  DEVEOF
477
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
471
478
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
472
479
  assert_exit "both headings present with empty bodies -> blocked" 2 $?
473
480
  EMPTY_MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1 >/dev/null)"
@@ -571,6 +578,7 @@ complete
571
578
  ### Status
572
579
  DONE
573
580
  DEVEOF
581
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
574
582
  echo "## Checkpoint(Round 6 摘要)" >> "$DEVLOG_DIR/devlog.md"
575
583
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
576
584
  assert_exit "Checkpoint-only append after a complete last Round -> allowed" 0 $?
@@ -600,6 +608,7 @@ complete
600
608
  ### Status
601
609
  DONE
602
610
  DEVEOF
611
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
603
612
  cat > "$DEVLOG_DIR/.span-open" <<'SPANEOF'
604
613
  {
605
614
  "round": 6,
@@ -635,6 +644,7 @@ cat > "$DEVLOG_DIR/.round-current.md" <<'DEVEOF'
635
644
  ## Round 7 — 2026-09-09T10:30:00+08:00
636
645
  incomplete new round
637
646
  DEVEOF
647
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
638
648
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
639
649
  assert_exit "span at max, new Round missing headings -> blocked" 2 $?
640
650
  SPAN_TICKS_HELD="$(grep -o '"ticks_since_checkin"[[:space:]]*:[[:space:]]*[0-9]\+' "$DEVLOG_DIR/.span-open" | grep -o '[0-9]\+$')"
@@ -675,6 +685,7 @@ fixture reply.
675
685
  ### Status
676
686
  DONE
677
687
  DEVEOF
688
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
678
689
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
679
690
  assert_exit "last Round complete, fenced ## Round 15 and ## 安裝 in body -> allowed" 0 $?
680
691
  assert_round_merged "last Round complete, fenced ## Round 15 and ## 安裝 in body" "已回覆格式問題"
@@ -713,6 +724,7 @@ complete last Round
713
724
  ### Status
714
725
  DONE
715
726
  DEVEOF
727
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
716
728
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
717
729
  assert_exit "older Round still uses ### Response, complete last Round -> allowed" 0 $?
718
730
  assert_round_merged "older Round still uses ### Response, complete last Round" "complete last Round"
@@ -759,6 +771,7 @@ done
759
771
  ### Status
760
772
  DONE
761
773
  EOF
774
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
762
775
  printf '%s\n' '{"round": 1, "opened_at": "2026-09-09T12:00:00+08:00"}' > "$DEVLOG_DIR/.round-open"
763
776
  printf '%s\n' 'main @ deadbeef,工作樹乾淨' > "$DEVLOG_DIR/.workspace-mismatch"
764
777
  cksum < "$DEVLOG_DIR/.round-current.md" > "$DEVLOG_DIR/.turn-start"
@@ -808,6 +821,7 @@ esc
808
821
  ### Status
809
822
  IN_PROGRESS
810
823
  EOF
824
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
811
825
  printf '%s\n' '{"round": 1, "opened_at": "2026-09-09T12:00:00+08:00"}' > "$DEVLOG_DIR/.round-open"
812
826
  cksum < "$DEVLOG_DIR/.round-current.md" > "$DEVLOG_DIR/.turn-start"
813
827
  touch "$DEVLOG_DIR/.interrupted"
@@ -862,6 +876,7 @@ round finished normally
862
876
  ### Status
863
877
  DONE
864
878
  EOF
879
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
865
880
  printf '%s\n' '{"round": 1, "opened_at": "2026-09-09T12:00:00+08:00"}' > "$DEVLOG_DIR/.round-open"
866
881
  cksum < "$DEVLOG_DIR/.round-current.md" > "$DEVLOG_DIR/.turn-start"
867
882
  printf '\n' >> "$DEVLOG_DIR/.round-current.md"
@@ -894,6 +909,7 @@ mkdir -p "$RECOVER_DIR/.devlog"
894
909
  touch "$RECOVER_DIR/.devlog/.enabled"
895
910
  : > "$RECOVER_DIR/.devlog/devlog.md"
896
911
  printf '## Round 1 — 2026-09-17T09:00:00+0800\n\n### User Input\n```text\nX\n```\n\n### Summary\n完成\n\n### Handoff\n#### 現況\nok\n\n### Status\nDONE\n' > "$RECOVER_DIR/.devlog/.round-current.md"
912
+ xml_fixture "$RECOVER_DIR/.devlog/.round-current.md"
897
913
  printf '{"round": 1}\n' > "$RECOVER_DIR/.devlog/.round-open"
898
914
  echo "stale" > "$RECOVER_DIR/.devlog/.turn-start"
899
915
  touch "$RECOVER_DIR/.devlog/.interrupted"
@@ -936,6 +952,7 @@ stale interrupt
936
952
  ### Status
937
953
  IN_PROGRESS
938
954
  EOF
955
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
939
956
  cksum < "$DEVLOG_DIR/.round-current.md" > "$DEVLOG_DIR/.turn-start"
940
957
  rm -f "$DEVLOG_DIR/.round-open"
941
958
  touch "$DEVLOG_DIR/.interrupted"
@@ -1016,6 +1033,7 @@ fixture reply.
1016
1033
  ### Status
1017
1034
  DONE
1018
1035
  EOF
1036
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
1019
1037
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
1020
1038
  assert_exit "DONE with bodies and no 下一步 -> allowed" 0 $?
1021
1039
  assert_round_merged "DONE with bodies and no 下一步" "做完了"
@@ -1050,9 +1068,51 @@ fixture reply.
1050
1068
  ### Status
1051
1069
  IN_PROGRESS
1052
1070
  EOF
1071
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
1053
1072
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
1054
1073
  assert_exit "IN_PROGRESS without 下一步 -> blocked" 2 $?
1055
1074
 
1075
+ # --- Heading Scenario 13b: IN_PROGRESS with <done-when> but no <next> ------
1076
+ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
1077
+ cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
1078
+ ## Round 91 — 2026-09-09T10:41:00+08:00
1079
+
1080
+ ### Summary
1081
+ 還在做。
1082
+
1083
+ ### Reply
1084
+ fixture reply.
1085
+
1086
+ ### Handoff
1087
+ <handoff>
1088
+ <state>
1089
+ 做到一半。
1090
+ </state>
1091
+ <done-when>
1092
+ observable done via test.
1093
+ </done-when>
1094
+ </handoff>
1095
+
1096
+ ### Session Handoff
1097
+ <session-handoff>
1098
+ <decisions>
1099
+ - (無)
1100
+ </decisions>
1101
+ <open-questions>
1102
+ - fixture open
1103
+ </open-questions>
1104
+ <failed-attempts>
1105
+ - (無)
1106
+ </failed-attempts>
1107
+ </session-handoff>
1108
+
1109
+ ### Status
1110
+ IN_PROGRESS
1111
+ EOF
1112
+ MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
1113
+ assert_exit "IN_PROGRESS with <done-when> but no <next> -> blocked" 2 $?
1114
+ assert_contains "missing <next> message names <next>" "<next>" "$MSG"
1115
+
1056
1116
  # --- Heading Scenario 14: IN_PROGRESS with 下一步 -> allowed --------------
1057
1117
  bash "$SCRIPT_DIR/round-start.sh" < /dev/null
1058
1118
  cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
@@ -1089,6 +1149,7 @@ fixture reply.
1089
1149
  ### Status
1090
1150
  IN_PROGRESS
1091
1151
  EOF
1152
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
1092
1153
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
1093
1154
  assert_exit "IN_PROGRESS with 下一步 -> allowed" 0 $?
1094
1155
  assert_round_merged "IN_PROGRESS with 下一步" "打開 foo.ts 繼續"
@@ -1111,6 +1172,7 @@ y
1111
1172
  ### Status
1112
1173
  WIP
1113
1174
  EOF
1175
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
1114
1176
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
1115
1177
  assert_exit "illegal Status -> blocked" 2 $?
1116
1178
 
@@ -1149,6 +1211,7 @@ fixture reply.
1149
1211
  ### Status
1150
1212
  IN_PROGRESS
1151
1213
  EOF
1214
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
1152
1215
  BLACKLIST_MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1 >/dev/null)"
1153
1216
  assert_exit "下一步 is pure filler phrase '繼續完成' -> blocked" 2 $?
1154
1217
  case "$BLACKLIST_MSG" in
@@ -1190,6 +1253,7 @@ fixture reply.
1190
1253
  ### Status
1191
1254
  BLOCKED
1192
1255
  EOF
1256
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
1193
1257
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
1194
1258
  assert_exit "下一步 is '持續優化。' with trailing punctuation -> blocked (BLOCKED status too)" 2 $?
1195
1259
 
@@ -1230,6 +1294,7 @@ fixture reply.
1230
1294
  ### Status
1231
1295
  IN_PROGRESS
1232
1296
  EOF
1297
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
1233
1298
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
1234
1299
  assert_exit "下一步 contains 繼續完成 inside a concrete sentence -> allowed" 0 $?
1235
1300
  assert_round_merged "下一步 contains 繼續完成 inside a concrete sentence" "錯誤處理"
@@ -1273,6 +1338,7 @@ fixture reply.
1273
1338
  ### Status
1274
1339
  IN_PROGRESS
1275
1340
  EOF
1341
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
1276
1342
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
1277
1343
  assert_exit "下一步 multi-line body including a filler line -> allowed" 0 $?
1278
1344
  assert_round_merged "下一步 multi-line body including a filler line" "bar.ts"
@@ -1292,6 +1358,7 @@ y
1292
1358
  ### Status
1293
1359
  DONE
1294
1360
  EOF
1361
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
1295
1362
  REPLY_MISS_MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1 >/dev/null)"
1296
1363
  assert_exit "last Round missing ### Reply -> blocked" 2 $?
1297
1364
  case "$REPLY_MISS_MSG" in
@@ -1335,6 +1402,7 @@ cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
1335
1402
  ### Status
1336
1403
  IN_PROGRESS
1337
1404
  EOF
1405
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
1338
1406
  ACTION_MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1 >/dev/null)"
1339
1407
  assert_exit "IN_PROGRESS 下一步 without path/cmd/skill -> blocked" 2 $?
1340
1408
  case "$ACTION_MSG" in
@@ -1378,6 +1446,7 @@ cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
1378
1446
  ### Status
1379
1447
  BLOCKED
1380
1448
  EOF
1449
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
1381
1450
  BLOCKED_MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1 >/dev/null)"
1382
1451
  assert_exit "BLOCKED without 缺件句式 -> blocked" 2 $?
1383
1452
  case "$BLOCKED_MSG" in
@@ -1421,6 +1490,7 @@ cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
1421
1490
  ### Status
1422
1491
  BLOCKED
1423
1492
  EOF
1493
+ xml_fixture "$DEVLOG_DIR/.round-current.md"
1424
1494
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
1425
1495
  assert_exit "BLOCKED with 缺件句式 -> allowed" 0 $?
1426
1496
 
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env bash
2
+ # Self-check for handoff-fields.sh (docs/design/handoff-xml.md).
3
+ set -uo pipefail
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ # shellcheck source=../handoff-fields.sh
6
+ . "$SCRIPT_DIR/handoff-fields.sh"
7
+ FAIL=0
8
+ eq() {
9
+ if [ "$2" = "$3" ]; then echo "PASS: $1"; else printf 'FAIL: %s\n--- expected\n%s\n--- actual\n%s\n' "$1" "$2" "$3"; FAIL=1; fi
10
+ }
11
+ check_fails() {
12
+ local desc="$1" body="$2" kind="$3" want="$4" out
13
+ if out="$(handoff_xml_check "$body" "$kind")"; then
14
+ echo "FAIL: $desc (expected check to fail)"; FAIL=1; return
15
+ fi
16
+ case "$out" in
17
+ *"$want"*) echo "PASS: $desc" ;;
18
+ *) echo "FAIL: $desc (message lacks '$want': $out)"; FAIL=1 ;;
19
+ esac
20
+ }
21
+
22
+ XML='
23
+ <handoff>
24
+ <decisions>
25
+ pick A
26
+ </decisions>
27
+ <workspace>
28
+ main @ abc1234,工作樹乾淨
29
+ </workspace>
30
+ <next>
31
+ edit `core/scripts/x.sh`
32
+ ```sh
33
+ ### not a heading
34
+ ```
35
+ </next>
36
+ </handoff>
37
+ '
38
+ MD='
39
+ #### 決策
40
+ pick A
41
+ #### 工作區
42
+ main @ abc1234,工作樹乾淨
43
+ #### 下一步
44
+ edit `core/scripts/x.sh`
45
+ '
46
+
47
+ eq "format xml" xml "$(handoff_format "$XML")"
48
+ eq "format md" md "$(handoff_format "$MD")"
49
+ eq "format none" none "$(handoff_format 'just prose')"
50
+ eq "xml field decisions" "pick A" "$(handoff_field "$XML" decisions)"
51
+ eq "xml field workspace" "main @ abc1234,工作樹乾淨" "$(handoff_field "$XML" workspace)"
52
+ eq "xml field keeps fenced content" 'edit `core/scripts/x.sh`
53
+ ```sh
54
+ ### not a heading
55
+ ```' "$(handoff_field "$XML" next)"
56
+ eq "xml missing field empty" "" "$(handoff_field "$XML" state)"
57
+ eq "md field workspace" "main @ abc1234,工作樹乾淨" "$(handoff_field "$MD" workspace)"
58
+ eq "md field next" 'edit `core/scripts/x.sh`' "$(handoff_field "$MD" next)"
59
+ eq "md heading exact match only" "" "$(handoff_field '#### 工作區(舊)
60
+ x' workspace)"
61
+ eq "md fenced heading ignored" "" "$(handoff_field '```
62
+ #### 工作區
63
+ fake
64
+ ```' workspace)"
65
+
66
+ if handoff_xml_check "$XML" handoff >/dev/null; then echo "PASS: valid xml passes"; else echo "FAIL: valid xml"; FAIL=1; fi
67
+ check_fails "must open with tag" 'prose
68
+ <handoff>
69
+ </handoff>' handoff "<handoff>"
70
+ check_fails "text after block" '<handoff>
71
+ <state>
72
+ x
73
+ </state>
74
+ </handoff>
75
+ trailing' handoff "外面"
76
+ check_fails "unknown tag" '<handoff>
77
+ <done_when>
78
+ x
79
+ </done_when>
80
+ </handoff>' handoff "done_when"
81
+ check_fails "inline tag" '<handoff>
82
+ <next>做 X</next>
83
+ </handoff>' handoff "自己一行"
84
+ check_fails "unclosed field" '<handoff>
85
+ <state>
86
+ x
87
+ </handoff>' handoff "</state>"
88
+ check_fails "missing block close" '<handoff>
89
+ <state>
90
+ x
91
+ </state>' handoff "</handoff>"
92
+ check_fails "duplicate" '<handoff>
93
+ <state>
94
+ x
95
+ </state>
96
+ <state>
97
+ y
98
+ </state>
99
+ </handoff>' handoff "超過一次"
100
+ check_fails "order" '<handoff>
101
+ <next>
102
+ x
103
+ </next>
104
+ <state>
105
+ y
106
+ </state>
107
+ </handoff>' handoff "順序"
108
+ check_fails "empty field" '<handoff>
109
+ <state>
110
+ </state>
111
+ </handoff>' handoff "是空的"
112
+ check_fails "session requires all three" '<session-handoff>
113
+ <decisions>
114
+ - (無)
115
+ </decisions>
116
+ </session-handoff>' session-handoff "open-questions"
117
+ check_fails "session tag not allowed in handoff" '<handoff>
118
+ <open-questions>
119
+ x
120
+ </open-questions>
121
+ </handoff>' handoff "open-questions"
122
+
123
+ ROUND='## Round 1 — t
124
+
125
+ ### Handoff
126
+ <handoff>
127
+ <state>
128
+ s
129
+ </state>
130
+ </handoff>
131
+
132
+ ### Session Handoff
133
+ <session-handoff>
134
+ <decisions>
135
+ - d
136
+ </decisions>
137
+ </session-handoff>
138
+
139
+ ### Status
140
+ DONE'
141
+ eq "section_of Handoff" "<handoff>
142
+ <state>
143
+ s
144
+ </state>
145
+ </handoff>" "$(handoff_section_of "$ROUND" Handoff | awk 'NF')"
146
+ eq "section_of Session Handoff" "<session-handoff>
147
+ <decisions>
148
+ - d
149
+ </decisions>
150
+ </session-handoff>" "$(handoff_section_of "$ROUND" 'Session Handoff' | awk 'NF')"
151
+
152
+ SKILL="$(cat "$SCRIPT_DIR/../../skills/devlog-tracker/SKILL.md")"
153
+ for kind in handoff session-handoff; do
154
+ case "$SKILL" in
155
+ *"$(handoff_xml_template "$kind")"*) echo "PASS: SKILL.md carries $kind template" ;;
156
+ *) echo "FAIL: SKILL.md template for $kind differs from handoff_xml_template"; FAIL=1 ;;
157
+ esac
158
+ done
159
+
160
+ MSG="$(handoff_legacy_message /abs/core/scripts/migrate-handoff.sh /abs/proj)"
161
+ case "$MSG" in
162
+ *'DEVLOG_PROJECT_DIR="/abs/proj" bash "/abs/core/scripts/migrate-handoff.sh"'*) echo "PASS: legacy message has migrate command" ;;
163
+ *) echo "FAIL: legacy message lacks migrate command: $MSG"; FAIL=1 ;;
164
+ esac
165
+ case "$MSG" in
166
+ *"npx devlog-tracker init"*"/devlog-tracker:start"*) echo "PASS: legacy message has init/start hint" ;;
167
+ *) echo "FAIL: legacy message lacks init/start hint"; FAIL=1 ;;
168
+ esac
169
+
170
+ [ "$FAIL" -eq 0 ] || exit 1
171
+ echo "All handoff-fields checks passed."
@@ -16,80 +16,102 @@ s
16
16
  r
17
17
 
18
18
  ### Handoff
19
- #### 現況
19
+ <handoff>
20
+ <state>
20
21
  c
21
- #### 完成條件
22
+ </state>
23
+ <done-when>
22
24
  done when tests pass
23
- #### 下一步
25
+ </done-when>
26
+ <next>
24
27
  edit core/scripts/handoff-file.sh
28
+ </next>
29
+ </handoff>
25
30
 
26
31
  ### Session Handoff
27
-
28
- #### 決策
32
+ <session-handoff>
33
+ <decisions>
29
34
  - pick route A
30
-
31
- #### 待解問題
35
+ </decisions>
36
+ <open-questions>
32
37
  - (無)
33
-
34
- #### 失敗嘗試
38
+ </open-questions>
39
+ <failed-attempts>
35
40
  - tried parse-in-place
41
+ </failed-attempts>
42
+ </session-handoff>
36
43
 
37
44
  ### Status
38
45
  IN_PROGRESS
39
46
  '
40
47
 
48
+ EXPECTED='<session-handoff>
49
+ <decisions>
50
+ - pick route A
51
+ </decisions>
52
+ <open-questions>
53
+ - (無)
54
+ </open-questions>
55
+ <failed-attempts>
56
+ - tried parse-in-place
57
+ </failed-attempts>
58
+ </session-handoff>'
59
+
41
60
  BAD_ORDER='### Session Handoff
42
- #### 待解問題
61
+ <session-handoff>
62
+ <open-questions>
43
63
  - x
44
- #### 決策
64
+ </open-questions>
65
+ <decisions>
45
66
  - y
46
- #### 失敗嘗試
67
+ </decisions>
68
+ <failed-attempts>
47
69
  - z
70
+ </failed-attempts>
71
+ </session-handoff>
48
72
  '
49
73
 
50
74
  MISSING='### Session Handoff
75
+ <session-handoff>
76
+ <decisions>
77
+ - x
78
+ </decisions>
79
+ </session-handoff>
80
+ '
81
+
82
+ LEGACY='### Session Handoff
51
83
  #### 決策
52
84
  - x
85
+ #### 待解問題
86
+ - y
53
87
  #### 失敗嘗試
54
88
  - z
55
89
  '
56
90
 
57
- if handoff_session_section_ok "$GOOD_ROUND"; then
58
- echo "PASS: good round validates"
59
- else
60
- echo "FAIL: good round should validate"; FAIL=1
61
- fi
62
-
63
- if handoff_session_section_ok "$BAD_ORDER"; then
64
- echo "FAIL: bad order should fail"; FAIL=1
65
- else
66
- echo "PASS: bad order rejected"
67
- fi
68
-
69
- if handoff_session_section_ok "$MISSING"; then
70
- echo "FAIL: missing 待解問題 should fail"; FAIL=1
71
- else
72
- echo "PASS: missing subsection rejected"
73
- fi
74
-
75
- OUT="$(handoff_extract_file_body "$GOOD_ROUND")" || { echo "FAIL: extract exited $?"; FAIL=1; OUT=""; }
76
- if printf '%s\n' "$OUT" | grep -q '^## Session Handoff' \
77
- && printf '%s\n' "$OUT" | grep -q '^### 決策' \
78
- && printf '%s\n' "$OUT" | grep -q 'pick route A' \
79
- && printf '%s\n' "$OUT" | grep -q '^### 待解問題' \
80
- && printf '%s\n' "$OUT" | grep -q '(無)' \
81
- && printf '%s\n' "$OUT" | grep -q '^### 失敗嘗試' \
82
- && printf '%s\n' "$OUT" | grep -q 'tried parse-in-place'; then
83
- echo "PASS: extract shape"
91
+ TARGET="$TMP/handoff.md"
92
+ if handoff_write "$TARGET" "$GOOD_ROUND"; then
93
+ if [ "$(cat "$TARGET")" = "$EXPECTED" ]; then
94
+ echo "PASS: write stores <session-handoff> block verbatim"
95
+ else
96
+ echo "FAIL: handoff.md content, got:"; cat "$TARGET"; FAIL=1
97
+ fi
84
98
  else
85
- echo "FAIL: extract shape, got: $OUT"; FAIL=1
99
+ echo "FAIL: write good round returned non-zero"; FAIL=1
86
100
  fi
87
101
 
88
- TARGET="$TMP/handoff.md"
89
- handoff_write "$TARGET" "$GOOD_ROUND" || { echo "FAIL: write"; FAIL=1; }
90
- [ -s "$TARGET" ] && grep -q 'pick route A' "$TARGET" \
91
- && echo "PASS: write creates file" \
92
- || { echo "FAIL: write"; FAIL=1; }
102
+ expect_rejected() {
103
+ local name="$1" blob="$2" bad_target="$TMP/bad-$1.md"
104
+ if handoff_write "$bad_target" "$blob"; then
105
+ echo "FAIL: $name should be rejected"; FAIL=1
106
+ elif [ -e "$bad_target" ]; then
107
+ echo "FAIL: $name left a file behind"; FAIL=1
108
+ else
109
+ echo "PASS: $name rejected, no file written"
110
+ fi
111
+ }
112
+ expect_rejected BAD_ORDER "$BAD_ORDER"
113
+ expect_rejected MISSING "$MISSING"
114
+ expect_rejected LEGACY "$LEGACY"
93
115
 
94
116
  handoff_clear "$TARGET"
95
117
  [ ! -f "$TARGET" ] && echo "PASS: clear removes file" \