devlog-tracker 0.27.0 → 0.30.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 (45) hide show
  1. package/README.md +109 -100
  2. package/README.zh-TW.md +235 -0
  3. package/claude/hooks.json +21 -0
  4. package/cli/agents-md.js +1 -0
  5. package/commands/keep.md +18 -2
  6. package/commands/lessons-drift.md +2 -2
  7. package/commands/search.md +22 -0
  8. package/commands/status.md +2 -1
  9. package/core/scripts/clean-devlog.sh +1 -0
  10. package/core/scripts/devlog-lock.sh +17 -1
  11. package/core/scripts/devlog-path.sh +15 -8
  12. package/core/scripts/enforce-devlog.sh +25 -0
  13. package/core/scripts/handoff-file.sh +106 -0
  14. package/core/scripts/lessons-advisory-state.sh +51 -0
  15. package/core/scripts/lessons-append.sh +16 -0
  16. package/core/scripts/lessons-drift-set.sh +14 -10
  17. package/core/scripts/lessons-on.sh +8 -3
  18. package/core/scripts/lessons-subagent-done.sh +36 -0
  19. package/core/scripts/lessons-subagent-start.sh +36 -0
  20. package/core/scripts/round-start.sh +65 -16
  21. package/core/scripts/search-devlog.sh +63 -0
  22. package/core/scripts/session-start-devlog.sh +7 -0
  23. package/core/scripts/status-devlog.sh +4 -4
  24. package/core/scripts/tests/test-clean-devlog.sh +12 -0
  25. package/core/scripts/tests/test-devlog-lock.sh +39 -0
  26. package/core/scripts/tests/test-devlog-path.sh +44 -0
  27. package/core/scripts/tests/test-enforce-devlog-files.sh +11 -0
  28. package/core/scripts/tests/test-enforce-devlog-session-handoff.sh +178 -0
  29. package/core/scripts/tests/test-enforce-devlog-workspace.sh +35 -0
  30. package/core/scripts/tests/test-enforce-devlog.sh +174 -0
  31. package/core/scripts/tests/test-handoff-file.sh +98 -0
  32. package/core/scripts/tests/test-lessons-advisory-state.sh +55 -0
  33. package/core/scripts/tests/test-lessons-append.sh +18 -0
  34. package/core/scripts/tests/test-lessons-drift-set.sh +14 -5
  35. package/core/scripts/tests/test-lessons-on-off.sh +16 -6
  36. package/core/scripts/tests/test-lessons-subagent-hooks.sh +95 -0
  37. package/core/scripts/tests/test-round-start.sh +296 -13
  38. package/core/scripts/tests/test-search-devlog.sh +126 -0
  39. package/core/scripts/tests/test-session-start-devlog.sh +59 -0
  40. package/core/scripts/tests/test-status-span.sh +3 -3
  41. package/package.json +2 -2
  42. package/skills/devlog-tracker/SKILL.md +83 -13
  43. package/skills/devlog-tracker/references/checkpoint-mode.md +21 -6
  44. package/skills/devlog-tracker/references/contract.md +83 -0
  45. package/skills/devlog-tracker/references/lessons-mode.md +38 -8
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env bash
2
+ # Self-check for Session Handoff enforce + handoff.md sync.
3
+ # docs/design/session-handoff-file.md
4
+ set -uo pipefail
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
7
+ TMP_ROOT="$(mktemp -d)"
8
+ trap 'rm -rf "$TMP_ROOT"' EXIT
9
+
10
+ export CLAUDE_PROJECT_DIR="$TMP_ROOT"
11
+ DEVLOG_DIR="$TMP_ROOT/.devlog"
12
+ mkdir -p "$DEVLOG_DIR"
13
+ touch "$DEVLOG_DIR/.enabled"
14
+
15
+ git -C "$TMP_ROOT" init -q -b main
16
+ git -C "$TMP_ROOT" config user.email test@example.com
17
+ git -C "$TMP_ROOT" config user.name test
18
+ echo '.devlog/' > "$TMP_ROOT/.gitignore"
19
+ git -C "$TMP_ROOT" add .gitignore
20
+ git -C "$TMP_ROOT" commit -q -m init
21
+ HASH="$(git -C "$TMP_ROOT" rev-parse --short HEAD)"
22
+
23
+ FAIL=0
24
+ assert_exit() {
25
+ local desc="$1" expected="$2" actual="$3"
26
+ if [ "$actual" -eq "$expected" ]; then
27
+ echo "PASS: $desc"
28
+ else
29
+ echo "FAIL: $desc (expected exit $expected, got $actual)"
30
+ FAIL=1
31
+ fi
32
+ }
33
+
34
+ SESSION_OK='
35
+ ### Session Handoff
36
+
37
+ #### 決策
38
+ - pick route A
39
+
40
+ #### 待解問題
41
+ - still wiring enforce
42
+
43
+ #### 失敗嘗試
44
+ - (無)
45
+ '
46
+
47
+ write_unfinished() {
48
+ local status="$1" session="$2"
49
+ {
50
+ echo "## Round 1 — 2026-09-22T00:00:00+08:00"
51
+ echo ""
52
+ echo "### Summary"
53
+ echo "fixture"
54
+ echo ""
55
+ echo "### Reply"
56
+ echo "fixture reply."
57
+ echo ""
58
+ echo "### Handoff"
59
+ echo "#### 工作區"
60
+ echo "main @ ${HASH},工作樹乾淨"
61
+ echo "#### 現況"
62
+ if [ "$status" = "BLOCKED" ]; then
63
+ echo "缺使用者提供的 token(出現即繼續)"
64
+ else
65
+ echo "fixture"
66
+ fi
67
+ echo "#### 完成條件"
68
+ echo "observable done via test."
69
+ echo "#### 下一步"
70
+ if [ "$status" = "BLOCKED" ]; then
71
+ echo "等使用者提供 token 後改 hooks/foo.sh"
72
+ else
73
+ echo "edit core/scripts/enforce-devlog.sh"
74
+ fi
75
+ if [ -n "$session" ]; then
76
+ printf '%s\n' "$session"
77
+ fi
78
+ echo ""
79
+ echo "### Status"
80
+ echo "$status"
81
+ } > "$DEVLOG_DIR/.round-current.md"
82
+ }
83
+
84
+ # 1) IN_PROGRESS without Session Handoff -> blocked
85
+ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
86
+ write_unfinished IN_PROGRESS ""
87
+ MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
88
+ assert_exit "IN_PROGRESS without Session Handoff -> blocked" 2 $?
89
+ case "$MSG" in
90
+ *"Session Handoff"*) echo "PASS: message mentions Session Handoff" ;;
91
+ *) echo "FAIL: expected Session Handoff in message, got: $MSG"; FAIL=1 ;;
92
+ esac
93
+
94
+ # 2) IN_PROGRESS with Session Handoff -> write handoff.md
95
+ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
96
+ write_unfinished IN_PROGRESS "$SESSION_OK"
97
+ echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
98
+ assert_exit "IN_PROGRESS with Session Handoff -> allowed" 0 $?
99
+ 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
101
+ echo "PASS: handoff.md written from Session Handoff"
102
+ else
103
+ echo "FAIL: handoff.md missing or wrong"; FAIL=1
104
+ fi
105
+
106
+ # 3) BLOCKED with Session Handoff -> updates file
107
+ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
108
+ write_unfinished BLOCKED "
109
+ ### Session Handoff
110
+
111
+ #### 決策
112
+ - (無)
113
+
114
+ #### 待解問題
115
+ - waiting on token
116
+
117
+ #### 失敗嘗試
118
+ - (無)
119
+ "
120
+ echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
121
+ assert_exit "BLOCKED with Session Handoff -> allowed" 0 $?
122
+ if grep -q 'waiting on token' "$DEVLOG_DIR/handoff.md" 2>/dev/null; then
123
+ echo "PASS: handoff.md updated on BLOCKED"
124
+ else
125
+ echo "FAIL: handoff.md not updated on BLOCKED"; FAIL=1
126
+ fi
127
+
128
+ # 4) DONE clears handoff.md
129
+ echo "stale" > "$DEVLOG_DIR/handoff.md"
130
+ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
131
+ {
132
+ echo "## Round 1 — 2026-09-22T01:00:00+08:00"
133
+ echo ""
134
+ echo "### Summary"
135
+ echo "done"
136
+ echo ""
137
+ echo "### Reply"
138
+ echo "done reply."
139
+ echo ""
140
+ echo "### Handoff"
141
+ echo "#### 現況"
142
+ echo "finished"
143
+ echo ""
144
+ echo "### Status"
145
+ echo "DONE"
146
+ } > "$DEVLOG_DIR/.round-current.md"
147
+ echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
148
+ assert_exit "DONE -> allowed" 0 $?
149
+ if [ ! -f "$DEVLOG_DIR/handoff.md" ]; then
150
+ echo "PASS: DONE deletes handoff.md"
151
+ else
152
+ echo "FAIL: handoff.md survived DONE"; FAIL=1
153
+ fi
154
+
155
+ # 5) wrong order -> blocked; existing handoff unchanged
156
+ echo "keep-me" > "$DEVLOG_DIR/handoff.md"
157
+ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
158
+ write_unfinished IN_PROGRESS "
159
+ ### Session Handoff
160
+
161
+ #### 待解問題
162
+ - x
163
+
164
+ #### 決策
165
+ - y
166
+
167
+ #### 失敗嘗試
168
+ - z
169
+ "
170
+ echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
171
+ assert_exit "wrong Session Handoff order -> blocked" 2 $?
172
+ if [ -f "$DEVLOG_DIR/handoff.md" ] && grep -q 'keep-me' "$DEVLOG_DIR/handoff.md"; then
173
+ echo "PASS: handoff.md unchanged on validation failure"
174
+ else
175
+ echo "FAIL: handoff.md should be untouched on validation failure"; FAIL=1
176
+ fi
177
+
178
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1
@@ -75,6 +75,17 @@ write_round() {
75
75
  echo "#### 下一步"
76
76
  echo "edit hooks/scripts/enforce-devlog.sh"
77
77
  echo ""
78
+ echo "### Session Handoff"
79
+ echo ""
80
+ echo "#### 決策"
81
+ echo "- (無)"
82
+ echo ""
83
+ echo "#### 待解問題"
84
+ echo "- fixture open"
85
+ echo ""
86
+ echo "#### 失敗嘗試"
87
+ echo "- (無)"
88
+ echo ""
78
89
  echo "### Status"
79
90
  echo "IN_PROGRESS"
80
91
  } > "$DEVLOG_DIR/.round-current.md"
@@ -193,6 +204,18 @@ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
193
204
  echo "#### 下一步"
194
205
  echo "edit hooks/scripts/enforce-devlog.sh"
195
206
  echo ""
207
+ echo ""
208
+ echo "### Session Handoff"
209
+ echo ""
210
+ echo "#### 決策"
211
+ echo "- (無)"
212
+ echo ""
213
+ echo "#### 待解問題"
214
+ echo "- fixture open"
215
+ echo ""
216
+ echo "#### 失敗嘗試"
217
+ echo "- (無)"
218
+ echo ""
196
219
  echo "### Status"
197
220
  echo "IN_PROGRESS"
198
221
  } > "$DEVLOG_DIR/.round-current.md"
@@ -229,6 +252,18 @@ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
229
252
  echo "#### 下一步"
230
253
  echo "edit hooks/scripts/enforce-devlog.sh"
231
254
  echo ""
255
+ echo ""
256
+ echo "### Session Handoff"
257
+ echo ""
258
+ echo "#### 決策"
259
+ echo "- (無)"
260
+ echo ""
261
+ echo "#### 待解問題"
262
+ echo "- fixture open"
263
+ echo ""
264
+ echo "#### 失敗嘗試"
265
+ echo "- (無)"
266
+ echo ""
232
267
  echo "### Status"
233
268
  echo "IN_PROGRESS"
234
269
  } > "$DEVLOG_DIR/.round-current.md"
@@ -793,6 +793,18 @@ cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
793
793
  esc
794
794
  ```
795
795
 
796
+
797
+ ### Session Handoff
798
+
799
+ #### 決策
800
+ - (無)
801
+
802
+ #### 待解問題
803
+ - fixture open
804
+
805
+ #### 失敗嘗試
806
+ - (無)
807
+
796
808
  ### Status
797
809
  IN_PROGRESS
798
810
  EOF
@@ -909,6 +921,18 @@ cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
909
921
  stale interrupt
910
922
  ```
911
923
 
924
+
925
+ ### Session Handoff
926
+
927
+ #### 決策
928
+ - (無)
929
+
930
+ #### 待解問題
931
+ - fixture open
932
+
933
+ #### 失敗嘗試
934
+ - (無)
935
+
912
936
  ### Status
913
937
  IN_PROGRESS
914
938
  EOF
@@ -930,6 +954,18 @@ assert_exit "after stale interrupt cleared -> normal enforcement blocks (hash eq
930
954
  cat > "$DEVLOG_DIR/devlog.md" <<'EOF'
931
955
  ## Round 1 — 2026-09-09T12:00:00+08:00
932
956
 
957
+
958
+ ### Session Handoff
959
+
960
+ #### 決策
961
+ - (無)
962
+
963
+ #### 待解問題
964
+ - fixture open
965
+
966
+ #### 失敗嘗試
967
+ - (無)
968
+
933
969
  ### Status
934
970
  IN_PROGRESS
935
971
  EOF
@@ -999,6 +1035,18 @@ fixture reply.
999
1035
  #### 現況
1000
1036
  做到一半。
1001
1037
 
1038
+
1039
+ ### Session Handoff
1040
+
1041
+ #### 決策
1042
+ - (無)
1043
+
1044
+ #### 待解問題
1045
+ - fixture open
1046
+
1047
+ #### 失敗嘗試
1048
+ - (無)
1049
+
1002
1050
  ### Status
1003
1051
  IN_PROGRESS
1004
1052
  EOF
@@ -1026,6 +1074,18 @@ fixture reply.
1026
1074
  #### 下一步
1027
1075
  打開 foo.ts 繼續。
1028
1076
 
1077
+
1078
+ ### Session Handoff
1079
+
1080
+ #### 決策
1081
+ - (無)
1082
+
1083
+ #### 待解問題
1084
+ - fixture open
1085
+
1086
+ #### 失敗嘗試
1087
+ - (無)
1088
+
1029
1089
  ### Status
1030
1090
  IN_PROGRESS
1031
1091
  EOF
@@ -1074,6 +1134,18 @@ fixture reply.
1074
1134
  #### 下一步
1075
1135
  繼續完成
1076
1136
 
1137
+
1138
+ ### Session Handoff
1139
+
1140
+ #### 決策
1141
+ - (無)
1142
+
1143
+ #### 待解問題
1144
+ - fixture open
1145
+
1146
+ #### 失敗嘗試
1147
+ - (無)
1148
+
1077
1149
  ### Status
1078
1150
  IN_PROGRESS
1079
1151
  EOF
@@ -1103,6 +1175,18 @@ fixture reply.
1103
1175
  #### 下一步
1104
1176
  持續優化。
1105
1177
 
1178
+
1179
+ ### Session Handoff
1180
+
1181
+ #### 決策
1182
+ - (無)
1183
+
1184
+ #### 待解問題
1185
+ - fixture open
1186
+
1187
+ #### 失敗嘗試
1188
+ - (無)
1189
+
1106
1190
  ### Status
1107
1191
  BLOCKED
1108
1192
  EOF
@@ -1131,6 +1215,18 @@ fixture reply.
1131
1215
  #### 下一步
1132
1216
  先繼續完成 foo.ts 的錯誤處理,再跑一次測試。
1133
1217
 
1218
+
1219
+ ### Session Handoff
1220
+
1221
+ #### 決策
1222
+ - (無)
1223
+
1224
+ #### 待解問題
1225
+ - fixture open
1226
+
1227
+ #### 失敗嘗試
1228
+ - (無)
1229
+
1134
1230
  ### Status
1135
1231
  IN_PROGRESS
1136
1232
  EOF
@@ -1162,6 +1258,18 @@ fixture reply.
1162
1258
  繼續完成
1163
1259
  打開 bar.ts 補上測試。
1164
1260
 
1261
+
1262
+ ### Session Handoff
1263
+
1264
+ #### 決策
1265
+ - (無)
1266
+
1267
+ #### 待解問題
1268
+ - fixture open
1269
+
1270
+ #### 失敗嘗試
1271
+ - (無)
1272
+
1165
1273
  ### Status
1166
1274
  IN_PROGRESS
1167
1275
  EOF
@@ -1212,6 +1320,18 @@ cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
1212
1320
  #### 下一步
1213
1321
  再想一下怎麼做比較好
1214
1322
 
1323
+
1324
+ ### Session Handoff
1325
+
1326
+ #### 決策
1327
+ - (無)
1328
+
1329
+ #### 待解問題
1330
+ - fixture open
1331
+
1332
+ #### 失敗嘗試
1333
+ - (無)
1334
+
1215
1335
  ### Status
1216
1336
  IN_PROGRESS
1217
1337
  EOF
@@ -1243,6 +1363,18 @@ cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
1243
1363
  #### 下一步
1244
1364
  改 hooks/foo.sh
1245
1365
 
1366
+
1367
+ ### Session Handoff
1368
+
1369
+ #### 決策
1370
+ - (無)
1371
+
1372
+ #### 待解問題
1373
+ - fixture open
1374
+
1375
+ #### 失敗嘗試
1376
+ - (無)
1377
+
1246
1378
  ### Status
1247
1379
  BLOCKED
1248
1380
  EOF
@@ -1274,12 +1406,54 @@ cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
1274
1406
  #### 下一步
1275
1407
  等使用者提供 key 後改 hooks/foo.sh
1276
1408
 
1409
+
1410
+ ### Session Handoff
1411
+
1412
+ #### 決策
1413
+ - (無)
1414
+
1415
+ #### 待解問題
1416
+ - fixture open
1417
+
1418
+ #### 失敗嘗試
1419
+ - (無)
1420
+
1277
1421
  ### Status
1278
1422
  BLOCKED
1279
1423
  EOF
1280
1424
  echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
1281
1425
  assert_exit "BLOCKED with 缺件句式 -> allowed" 0 $?
1282
1426
 
1427
+ # --- Lock contention: another live session holds .devlog/.lock -----------
1428
+ # Must block (exit 2, distinct message) instead of proceeding to read/merge
1429
+ # a devlog.md that the other session might be mid-write on, and must leave
1430
+ # .round-current.md / devlog.md untouched so the retry has something to
1431
+ # validate.
1432
+ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
1433
+ append_minimal_round 102 "2026-09-15T10:04:00+08:00"
1434
+ PRE_ROUND_CURRENT="$(cat "$DEVLOG_DIR/.round-current.md" 2>/dev/null || true)"
1435
+ PRE_DEVLOG="$(cat "$DEVLOG_DIR/devlog.md" 2>/dev/null || true)"
1436
+ sleep 5 &
1437
+ LOCK_HOLDER_PID=$!
1438
+ mkdir "$DEVLOG_DIR/.lock"
1439
+ echo "$LOCK_HOLDER_PID" > "$DEVLOG_DIR/.lock/pid"
1440
+ CONTENTION_MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1 >/dev/null)"
1441
+ assert_exit "another live session holds the lock -> blocked" 2 $?
1442
+ case "$CONTENTION_MSG" in
1443
+ *"$LOCK_HOLDER_PID"*) echo "PASS: contention message names the holder pid" ;;
1444
+ *) echo "FAIL: expected holder pid $LOCK_HOLDER_PID in message, got: $CONTENTION_MSG"; FAIL=1 ;;
1445
+ esac
1446
+ assert_contains "lock contention: .round-current.md left untouched" "$PRE_ROUND_CURRENT" "$(cat "$DEVLOG_DIR/.round-current.md" 2>/dev/null || true)"
1447
+ [ "$(cat "$DEVLOG_DIR/devlog.md" 2>/dev/null || true)" = "$PRE_DEVLOG" ] \
1448
+ && echo "PASS: lock contention: devlog.md left untouched" \
1449
+ || { echo "FAIL: devlog.md should be untouched while the lock is contended"; FAIL=1; }
1450
+ kill "$LOCK_HOLDER_PID" 2>/dev/null || true
1451
+ wait "$LOCK_HOLDER_PID" 2>/dev/null || true
1452
+ rm -rf "$DEVLOG_DIR/.lock"
1453
+ echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
1454
+ assert_exit "retry once the lock is free -> allowed (no lasting side effect)" 0 $?
1455
+ assert_round_merged "retry once the lock is free" "fixture"
1456
+
1283
1457
  if [ "$FAIL" -eq 0 ]; then
1284
1458
  echo "All checks passed."
1285
1459
  exit 0
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
4
+ # shellcheck source=../handoff-file.sh
5
+ . "$SCRIPT_DIR/handoff-file.sh"
6
+ TMP="$(mktemp -d)"
7
+ trap 'rm -rf "$TMP"' EXIT
8
+ FAIL=0
9
+
10
+ GOOD_ROUND='## Round 1 — 2026-09-22T00:00:00+08:00
11
+
12
+ ### Summary
13
+ s
14
+
15
+ ### Reply
16
+ r
17
+
18
+ ### Handoff
19
+ #### 現況
20
+ c
21
+ #### 完成條件
22
+ done when tests pass
23
+ #### 下一步
24
+ edit core/scripts/handoff-file.sh
25
+
26
+ ### Session Handoff
27
+
28
+ #### 決策
29
+ - pick route A
30
+
31
+ #### 待解問題
32
+ - (無)
33
+
34
+ #### 失敗嘗試
35
+ - tried parse-in-place
36
+
37
+ ### Status
38
+ IN_PROGRESS
39
+ '
40
+
41
+ BAD_ORDER='### Session Handoff
42
+ #### 待解問題
43
+ - x
44
+ #### 決策
45
+ - y
46
+ #### 失敗嘗試
47
+ - z
48
+ '
49
+
50
+ MISSING='### Session Handoff
51
+ #### 決策
52
+ - x
53
+ #### 失敗嘗試
54
+ - z
55
+ '
56
+
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"
84
+ else
85
+ echo "FAIL: extract shape, got: $OUT"; FAIL=1
86
+ fi
87
+
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; }
93
+
94
+ handoff_clear "$TARGET"
95
+ [ ! -f "$TARGET" ] && echo "PASS: clear removes file" \
96
+ || { echo "FAIL: clear left file"; FAIL=1; }
97
+
98
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
4
+ # shellcheck source=../lessons-advisory-state.sh
5
+ . "$SCRIPT_DIR/lessons-advisory-state.sh"
6
+ TMP_ROOT="$(mktemp -d)"
7
+ trap 'rm -rf "$TMP_ROOT"' EXIT
8
+ FAIL=0
9
+
10
+ # --- migrate: neither file present -> no-op --------------------------------
11
+ lessons_advisory_migrate "$TMP_ROOT"
12
+ if [ ! -f "$TMP_ROOT/.lessons-advisory-state" ] && [ ! -f "$TMP_ROOT/.lessons-drift-state" ]; then
13
+ echo "PASS: migrate no-op when neither file exists"
14
+ else
15
+ echo "FAIL: migrate created a file when neither existed"; FAIL=1
16
+ fi
17
+
18
+ # --- migrate: old exists, new doesn't -> migrates and removes old ----------
19
+ printf '%s\n' '{"mismatch_count": 2, "threshold": 5}' > "$TMP_ROOT/.lessons-drift-state"
20
+ lessons_advisory_migrate "$TMP_ROOT"
21
+ if [ -f "$TMP_ROOT/.lessons-advisory-state" ]; then echo "PASS: migrate creates new file"; else echo "FAIL: new file missing after migrate"; FAIL=1; fi
22
+ grep -q '"count": 2' "$TMP_ROOT/.lessons-advisory-state" && echo "PASS: migrate preserves count" || { echo "FAIL: count not migrated"; FAIL=1; }
23
+ grep -q '"threshold": 5' "$TMP_ROOT/.lessons-advisory-state" && echo "PASS: migrate preserves threshold" || { echo "FAIL: threshold not migrated"; FAIL=1; }
24
+ if [ ! -f "$TMP_ROOT/.lessons-drift-state" ]; then echo "PASS: migrate removes old file"; else echo "FAIL: old file still present after migrate"; FAIL=1; fi
25
+
26
+ # --- migrate: new already exists -> leaves it alone, doesn't touch old -----
27
+ rm -f "$TMP_ROOT/.lessons-advisory-state" "$TMP_ROOT/.lessons-drift-state"
28
+ printf '%s\n' '{"count": 9, "threshold": 9}' > "$TMP_ROOT/.lessons-advisory-state"
29
+ printf '%s\n' '{"mismatch_count": 1, "threshold": 1}' > "$TMP_ROOT/.lessons-drift-state"
30
+ lessons_advisory_migrate "$TMP_ROOT"
31
+ grep -q '"count": 9' "$TMP_ROOT/.lessons-advisory-state" && echo "PASS: migrate leaves existing new file untouched" || { echo "FAIL: existing new file was overwritten"; FAIL=1; }
32
+
33
+ # --- migrate: idempotent (second call after migration is a no-op) ----------
34
+ rm -f "$TMP_ROOT/.lessons-advisory-state" "$TMP_ROOT/.lessons-drift-state"
35
+ printf '%s\n' '{"mismatch_count": 1, "threshold": 3}' > "$TMP_ROOT/.lessons-drift-state"
36
+ lessons_advisory_migrate "$TMP_ROOT"
37
+ lessons_advisory_migrate "$TMP_ROOT"
38
+ grep -q '"count": 1' "$TMP_ROOT/.lessons-advisory-state" && echo "PASS: second migrate call is a no-op" || { echo "FAIL: second migrate call changed state"; FAIL=1; }
39
+
40
+ # --- bump: creates file with defaults, increments, no print below threshold
41
+ rm -f "$TMP_ROOT/.lessons-advisory-state"
42
+ OUT="$(lessons_advisory_bump "$TMP_ROOT/.lessons-advisory-state")"
43
+ [ -z "$OUT" ] && echo "PASS: bump 1/3 prints nothing" || { echo "FAIL: bump 1/3 printed [$OUT]"; FAIL=1; }
44
+ grep -q '"count": 1' "$TMP_ROOT/.lessons-advisory-state" && echo "PASS: bump creates file with count=1" || { echo "FAIL: bump did not create count=1"; FAIL=1; }
45
+ grep -q '"threshold": 3' "$TMP_ROOT/.lessons-advisory-state" && echo "PASS: bump defaults threshold to 3" || { echo "FAIL: bump did not default threshold"; FAIL=1; }
46
+
47
+ # --- bump: reaching threshold prints and resets -----------------------------
48
+ OUT="$(lessons_advisory_bump "$TMP_ROOT/.lessons-advisory-state")"
49
+ [ -z "$OUT" ] && echo "PASS: bump 2/3 prints nothing" || { echo "FAIL: bump 2/3 printed [$OUT]"; FAIL=1; }
50
+ OUT="$(lessons_advisory_bump "$TMP_ROOT/.lessons-advisory-state")"
51
+ case "$OUT" in *"[Lessons Mode 提示]"*) echo "PASS: bump 3/3 prints the advisory" ;; *) echo "FAIL: bump 3/3 did not print, got [$OUT]"; FAIL=1 ;; esac
52
+ grep -q '"count": 0' "$TMP_ROOT/.lessons-advisory-state" && echo "PASS: bump resets count to 0 after printing" || { echo "FAIL: count not reset"; FAIL=1; }
53
+
54
+ if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
55
+ else echo "Some checks FAILED."; exit 1; fi
@@ -134,5 +134,23 @@ case "$INDEX_BLOCK3" in
134
134
  *) echo "FAIL: expected title truncated at first 。, got: $INDEX_BLOCK3"; FAIL=1 ;;
135
135
  esac
136
136
 
137
+ # --- Topic-repeat advisory: prints on every 3rd entry in the same topic ----
138
+ OUT_R1="$(bash "$SCRIPT_DIR/lessons-append.sh" --topic "repeat-topic" --text "第一次。")"
139
+ case "$OUT_R1" in *"[Lessons Mode 提示]"*) echo "FAIL: unexpected advisory on 1st entry: $OUT_R1"; FAIL=1 ;; *) echo "PASS: no advisory on 1st entry" ;; esac
140
+ OUT_R2="$(bash "$SCRIPT_DIR/lessons-append.sh" --topic "repeat-topic" --text "第二次。")"
141
+ case "$OUT_R2" in *"[Lessons Mode 提示]"*) echo "FAIL: unexpected advisory on 2nd entry: $OUT_R2"; FAIL=1 ;; *) echo "PASS: no advisory on 2nd entry" ;; esac
142
+ OUT_R3="$(bash "$SCRIPT_DIR/lessons-append.sh" --topic "repeat-topic" --text "第三次。")"
143
+ case "$OUT_R3" in *"[Lessons Mode 提示] 這個主題已經累積 3 則"*) echo "PASS: advisory on 3rd entry" ;; *) echo "FAIL: expected advisory on 3rd entry, got: $OUT_R3"; FAIL=1 ;; esac
144
+ OUT_R4="$(bash "$SCRIPT_DIR/lessons-append.sh" --topic "repeat-topic" --text "第四次。")"
145
+ case "$OUT_R4" in *"[Lessons Mode 提示]"*) echo "FAIL: unexpected advisory on 4th entry: $OUT_R4"; FAIL=1 ;; *) echo "PASS: no advisory on 4th entry" ;; esac
146
+
147
+ # --- New-topic hint: lists existing topics, only on first-ever append -----
148
+ OUT_NEW="$(bash "$SCRIPT_DIR/lessons-append.sh" --topic "brand-new-topic" --text "第一次,全新主題。")"
149
+ case "$OUT_NEW" in *"NEW_TOPIC"*"既有主題:"*) echo "PASS: new-topic hint lists existing topics" ;; *) echo "FAIL: expected NEW_TOPIC hint, got: $OUT_NEW"; FAIL=1 ;; esac
150
+ case "$OUT_NEW" in *"repeat-topic"*) echo "PASS: new-topic hint includes a real existing topic name" ;; *) echo "FAIL: expected existing topic name in hint, got: $OUT_NEW"; FAIL=1 ;; esac
151
+
152
+ OUT_AGAIN="$(bash "$SCRIPT_DIR/lessons-append.sh" --topic "brand-new-topic" --text "第二次,同一個主題。")"
153
+ case "$OUT_AGAIN" in *"NEW_TOPIC"*) echo "FAIL: unexpected NEW_TOPIC hint on repeat append: $OUT_AGAIN"; FAIL=1 ;; *) echo "PASS: no NEW_TOPIC hint once the topic already exists" ;; esac
154
+
137
155
  if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
138
156
  else echo "Some checks FAILED."; exit 1; fi