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.
- package/README.md +109 -100
- package/README.zh-TW.md +235 -0
- package/claude/hooks.json +21 -0
- package/cli/agents-md.js +1 -0
- package/commands/keep.md +18 -2
- package/commands/lessons-drift.md +2 -2
- package/commands/search.md +22 -0
- package/commands/status.md +2 -1
- package/core/scripts/clean-devlog.sh +1 -0
- package/core/scripts/devlog-lock.sh +17 -1
- package/core/scripts/devlog-path.sh +15 -8
- package/core/scripts/enforce-devlog.sh +25 -0
- package/core/scripts/handoff-file.sh +106 -0
- package/core/scripts/lessons-advisory-state.sh +51 -0
- package/core/scripts/lessons-append.sh +16 -0
- package/core/scripts/lessons-drift-set.sh +14 -10
- package/core/scripts/lessons-on.sh +8 -3
- package/core/scripts/lessons-subagent-done.sh +36 -0
- package/core/scripts/lessons-subagent-start.sh +36 -0
- package/core/scripts/round-start.sh +65 -16
- package/core/scripts/search-devlog.sh +63 -0
- package/core/scripts/session-start-devlog.sh +7 -0
- package/core/scripts/status-devlog.sh +4 -4
- package/core/scripts/tests/test-clean-devlog.sh +12 -0
- package/core/scripts/tests/test-devlog-lock.sh +39 -0
- package/core/scripts/tests/test-devlog-path.sh +44 -0
- package/core/scripts/tests/test-enforce-devlog-files.sh +11 -0
- package/core/scripts/tests/test-enforce-devlog-session-handoff.sh +178 -0
- package/core/scripts/tests/test-enforce-devlog-workspace.sh +35 -0
- package/core/scripts/tests/test-enforce-devlog.sh +174 -0
- package/core/scripts/tests/test-handoff-file.sh +98 -0
- package/core/scripts/tests/test-lessons-advisory-state.sh +55 -0
- package/core/scripts/tests/test-lessons-append.sh +18 -0
- package/core/scripts/tests/test-lessons-drift-set.sh +14 -5
- package/core/scripts/tests/test-lessons-on-off.sh +16 -6
- package/core/scripts/tests/test-lessons-subagent-hooks.sh +95 -0
- package/core/scripts/tests/test-round-start.sh +296 -13
- package/core/scripts/tests/test-search-devlog.sh +126 -0
- package/core/scripts/tests/test-session-start-devlog.sh +59 -0
- package/core/scripts/tests/test-status-span.sh +3 -3
- package/package.json +2 -2
- package/skills/devlog-tracker/SKILL.md +83 -13
- package/skills/devlog-tracker/references/checkpoint-mode.md +21 -6
- package/skills/devlog-tracker/references/contract.md +83 -0
- 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
|