devlog-tracker 0.22.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 (120) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +234 -0
  3. package/bin/devlog-tracker.js +51 -0
  4. package/cli/init.js +55 -0
  5. package/cli/init.test.js +37 -0
  6. package/cli/merge-hooks.js +60 -0
  7. package/cli/merge-hooks.test.js +187 -0
  8. package/cli/platforms/codex.js +13 -0
  9. package/cli/platforms/cursor.js +13 -0
  10. package/cli/status.js +14 -0
  11. package/cli/status.test.js +33 -0
  12. package/cli/vendor.js +37 -0
  13. package/cli/vendor.test.js +84 -0
  14. package/codex/hooks/on-pre-tool.sh +19 -0
  15. package/codex/hooks/on-session-end.sh +31 -0
  16. package/codex/hooks/on-session-start.sh +37 -0
  17. package/codex/hooks/on-stop.sh +18 -0
  18. package/codex/hooks/on-user-prompt-submit.sh +15 -0
  19. package/codex/hooks/project-dir.sh +13 -0
  20. package/codex/hooks/test-adapters.sh +98 -0
  21. package/codex/hooks.json +54 -0
  22. package/commands/checkpoint.md +17 -0
  23. package/commands/clean.md +50 -0
  24. package/commands/compact.md +24 -0
  25. package/commands/continue.md +30 -0
  26. package/commands/keep.md +100 -0
  27. package/commands/lessons-drift.md +19 -0
  28. package/commands/lessons-off.md +16 -0
  29. package/commands/lessons-on.md +20 -0
  30. package/commands/lessons.md +20 -0
  31. package/commands/overview.md +32 -0
  32. package/commands/pause.md +20 -0
  33. package/commands/resume.md +18 -0
  34. package/commands/segment-watch.md +21 -0
  35. package/commands/span.md +13 -0
  36. package/commands/start.md +19 -0
  37. package/commands/status.md +14 -0
  38. package/cursor/hooks/on-pre-tool.sh +24 -0
  39. package/cursor/hooks/on-session-end.sh +11 -0
  40. package/cursor/hooks/on-session-start.sh +15 -0
  41. package/cursor/hooks/on-stop.sh +44 -0
  42. package/cursor/hooks/on-submit-prompt.sh +28 -0
  43. package/cursor/hooks/on-tool-failure.sh +16 -0
  44. package/cursor/hooks/project-dir.sh +10 -0
  45. package/cursor/hooks/test-adapters.sh +201 -0
  46. package/cursor/hooks.json +26 -0
  47. package/hooks/scripts/await-open.sh +23 -0
  48. package/hooks/scripts/checkpoint-set.sh +44 -0
  49. package/hooks/scripts/clean-devlog.sh +69 -0
  50. package/hooks/scripts/close-open-round.sh +186 -0
  51. package/hooks/scripts/compact-devlog.sh +72 -0
  52. package/hooks/scripts/detect-pending-question.sh +31 -0
  53. package/hooks/scripts/devlog-lock.sh +38 -0
  54. package/hooks/scripts/devlog-md.sh +222 -0
  55. package/hooks/scripts/devlog-path.sh +70 -0
  56. package/hooks/scripts/enforce-devlog.sh +629 -0
  57. package/hooks/scripts/files-snapshot.sh +70 -0
  58. package/hooks/scripts/json-field.sh +76 -0
  59. package/hooks/scripts/keep-move.sh +155 -0
  60. package/hooks/scripts/kept-list.sh +40 -0
  61. package/hooks/scripts/lessons-append.sh +93 -0
  62. package/hooks/scripts/lessons-drift-set.sh +42 -0
  63. package/hooks/scripts/lessons-off.sh +15 -0
  64. package/hooks/scripts/lessons-on.sh +21 -0
  65. package/hooks/scripts/lessons-read.sh +60 -0
  66. package/hooks/scripts/on-session-end.sh +14 -0
  67. package/hooks/scripts/on-stop-failure.sh +18 -0
  68. package/hooks/scripts/on-tool-failure.sh +25 -0
  69. package/hooks/scripts/pause-devlog.sh +13 -0
  70. package/hooks/scripts/redact-prompt.sh +19 -0
  71. package/hooks/scripts/resume-devlog.sh +41 -0
  72. package/hooks/scripts/round-start.sh +333 -0
  73. package/hooks/scripts/run-tests.sh +26 -0
  74. package/hooks/scripts/segment-watch-set.sh +47 -0
  75. package/hooks/scripts/segment-watch.sh +197 -0
  76. package/hooks/scripts/session-start-devlog.sh +157 -0
  77. package/hooks/scripts/span-close.sh +9 -0
  78. package/hooks/scripts/span-open.sh +21 -0
  79. package/hooks/scripts/start-devlog.sh +24 -0
  80. package/hooks/scripts/status-devlog.sh +43 -0
  81. package/hooks/scripts/tests/test-await-open.sh +112 -0
  82. package/hooks/scripts/tests/test-branch-scoped-integration.sh +108 -0
  83. package/hooks/scripts/tests/test-checkpoint-set.sh +36 -0
  84. package/hooks/scripts/tests/test-clean-devlog.sh +160 -0
  85. package/hooks/scripts/tests/test-cli-init-e2e.sh +37 -0
  86. package/hooks/scripts/tests/test-close-open-round.sh +325 -0
  87. package/hooks/scripts/tests/test-compact-devlog.sh +62 -0
  88. package/hooks/scripts/tests/test-devlog-lock.sh +37 -0
  89. package/hooks/scripts/tests/test-devlog-md.sh +530 -0
  90. package/hooks/scripts/tests/test-devlog-path.sh +118 -0
  91. package/hooks/scripts/tests/test-enforce-devlog-files.sh +233 -0
  92. package/hooks/scripts/tests/test-enforce-devlog-handoff-order.sh +223 -0
  93. package/hooks/scripts/tests/test-enforce-devlog-workspace.sh +333 -0
  94. package/hooks/scripts/tests/test-enforce-devlog.sh +1290 -0
  95. package/hooks/scripts/tests/test-files-snapshot.sh +142 -0
  96. package/hooks/scripts/tests/test-json-field.sh +57 -0
  97. package/hooks/scripts/tests/test-keep-move.sh +127 -0
  98. package/hooks/scripts/tests/test-kept-list.sh +80 -0
  99. package/hooks/scripts/tests/test-lessons-append.sh +138 -0
  100. package/hooks/scripts/tests/test-lessons-drift-set.sh +36 -0
  101. package/hooks/scripts/tests/test-lessons-on-off.sh +75 -0
  102. package/hooks/scripts/tests/test-lessons-read.sh +97 -0
  103. package/hooks/scripts/tests/test-on-interrupt.sh +186 -0
  104. package/hooks/scripts/tests/test-redact-prompt.sh +27 -0
  105. package/hooks/scripts/tests/test-resume-devlog.sh +35 -0
  106. package/hooks/scripts/tests/test-round-start.sh +825 -0
  107. package/hooks/scripts/tests/test-segment-watch-set.sh +75 -0
  108. package/hooks/scripts/tests/test-segment-watch.sh +468 -0
  109. package/hooks/scripts/tests/test-session-start-devlog.sh +508 -0
  110. package/hooks/scripts/tests/test-start-pause-devlog.sh +84 -0
  111. package/hooks/scripts/tests/test-status-span.sh +50 -0
  112. package/hooks/scripts/tests/test-workspace-snapshot.sh +130 -0
  113. package/hooks/scripts/workspace-snapshot.sh +69 -0
  114. package/package.json +37 -0
  115. package/skills/devlog-tracker/SKILL.md +411 -0
  116. package/skills/devlog-tracker/references/checkpoint-mode.md +42 -0
  117. package/skills/devlog-tracker/references/lessons-mode.md +32 -0
  118. package/skills/devlog-tracker/references/reply-fold.md +129 -0
  119. package/skills/devlog-tracker/references/round-segments.md +55 -0
  120. package/skills/devlog-tracker/references/span-mode.md +65 -0
@@ -0,0 +1,508 @@
1
+ #!/usr/bin/env bash
2
+ # Self-check for session-start-devlog.sh, including the Span Mode
3
+ # resume-context note. No framework — plain assert-and-exit, matching this
4
+ # repo's existing style. Run directly:
5
+ # bash hooks/scripts/test-session-start-devlog.sh
6
+ set -uo pipefail
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
9
+ TMP_ROOT="$(mktemp -d)"
10
+ trap 'rm -rf "$TMP_ROOT"' EXIT
11
+
12
+ export CLAUDE_PROJECT_DIR="$TMP_ROOT"
13
+ DEVLOG_DIR="$TMP_ROOT/.devlog"
14
+ mkdir -p "$DEVLOG_DIR"
15
+
16
+ FAIL=0
17
+ assert_contains() {
18
+ local desc="$1" needle="$2" haystack="$3"
19
+ case "$haystack" in
20
+ *"$needle"*) echo "PASS: $desc" ;;
21
+ *) echo "FAIL: $desc (expected output to contain: $needle)"; FAIL=1 ;;
22
+ esac
23
+ }
24
+ assert_not_contains() {
25
+ local desc="$1" needle="$2" haystack="$3"
26
+ case "$haystack" in
27
+ *"$needle"*) echo "FAIL: $desc (expected output NOT to contain: $needle)"; FAIL=1 ;;
28
+ *) echo "PASS: $desc" ;;
29
+ esac
30
+ }
31
+ assert_exit() {
32
+ local desc="$1" expected="$2" actual="$3"
33
+ if [ "$actual" -eq "$expected" ]; then
34
+ echo "PASS: $desc"
35
+ else
36
+ echo "FAIL: $desc (expected exit $expected, got $actual)"
37
+ FAIL=1
38
+ fi
39
+ }
40
+
41
+ write_round() {
42
+ local n="$1" status="$2"
43
+ cat >> "$DEVLOG_DIR/devlog.md" <<EOF
44
+ ## Round ${n} — 2026-09-09T12:00:00+08:00
45
+
46
+ ### User Input
47
+ \`\`\`text
48
+ prompt ${n} with lots of bulk
49
+ \`\`\`
50
+
51
+ ### 段落 1 - 12:01
52
+ secret intermediate ${n}
53
+
54
+ ### Summary
55
+ summary ${n}
56
+
57
+ ### Reply
58
+ fixture reply.
59
+
60
+ ### Handoff
61
+ #### 現況
62
+ handoff ${n}
63
+
64
+ ### Status
65
+ ${status}
66
+ EOF
67
+ }
68
+
69
+ # --- Scenario 1: no devlog.md, no span -> silent exit 0 -------------------
70
+ OUTPUT="$(bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
71
+ EXIT_CODE=$?
72
+ if [ "$EXIT_CODE" -eq 0 ] && [ -z "$OUTPUT" ]; then
73
+ echo "PASS: no devlog.md, no span -> silent exit 0"
74
+ else
75
+ echo "FAIL: no devlog.md, no span -> expected silent exit 0, got exit $EXIT_CODE, output: $OUTPUT"
76
+ FAIL=1
77
+ fi
78
+
79
+ # --- Scenario 2: devlog.md exists, no span -> normal output, no span note -
80
+ echo "## Round 1 — 2026-09-08T00:00:00+08:00" >> "$DEVLOG_DIR/devlog.md"
81
+ OUTPUT="$(bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
82
+ assert_not_contains "no span -> no span warning in output" "開啟中的 span" "$OUTPUT"
83
+ assert_contains "no span -> still shows devlog content" "Round 1" "$OUTPUT"
84
+
85
+ # --- Scenario 3: span open with valid round/opened_at -> warning shown ----
86
+ cat > "$DEVLOG_DIR/.span-open" <<'SPANEOF'
87
+ {
88
+ "round": 1,
89
+ "opened_at": "2026-09-08T21:40:00+08:00",
90
+ "ticks_since_checkin": 2,
91
+ "max_silent_ticks": 5
92
+ }
93
+ SPANEOF
94
+ OUTPUT="$(bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
95
+ assert_contains "span open -> warning mentions Round 1" "Round 1" "$OUTPUT"
96
+ assert_contains "span open -> warning mentions opened_at timestamp" "2026-09-08T21:40:00+08:00" "$OUTPUT"
97
+ assert_contains "span open -> warning mentions .span-open" ".span-open" "$OUTPUT"
98
+
99
+ # --- Scenario 4: span file malformed -> no warning, no crash --------------
100
+ echo "not valid json" > "$DEVLOG_DIR/.span-open"
101
+ OUTPUT="$(bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
102
+ EXIT_CODE=$?
103
+ assert_not_contains "malformed span -> no warning shown" "開啟中的 span" "$OUTPUT"
104
+ if [ "$EXIT_CODE" -eq 0 ]; then
105
+ echo "PASS: malformed span -> still exits 0"
106
+ else
107
+ echo "FAIL: malformed span -> expected exit 0, got $EXIT_CODE"
108
+ FAIL=1
109
+ fi
110
+
111
+ # --- Scenario 5: startup heals open round then still injects --------------
112
+ rm -f "$DEVLOG_DIR/.span-open"
113
+ touch "$DEVLOG_DIR/.enabled"
114
+ rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-current.md"
115
+ cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
116
+ ## Round 1 — 2026-09-09T12:00:00+08:00
117
+
118
+ ### User Input
119
+ ```text
120
+ crash me
121
+ ```
122
+
123
+ ### Status
124
+ IN_PROGRESS
125
+ EOF
126
+ printf '%s\n' '{"round": 1, "opened_at": "2026-09-09T12:00:00+08:00"}' > "$DEVLOG_DIR/.round-open"
127
+ OUTPUT="$(echo '{"source":"startup"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
128
+ assert_contains "session start still shows Round 1" "Round 1" "$OUTPUT"
129
+ assert_contains "session start output includes INTERRUPTED" "INTERRUPTED" "$OUTPUT"
130
+ assert_contains "heal reason in the file/output" "dangling:session_start" "$OUTPUT"
131
+ if [ -f "$DEVLOG_DIR/.round-open" ]; then
132
+ echo "FAIL: SessionStart should delete .round-open after heal"
133
+ FAIL=1
134
+ else
135
+ echo "PASS: SessionStart deleted .round-open"
136
+ fi
137
+ FILE="$(cat "$DEVLOG_DIR/devlog.md")"
138
+ assert_contains "file stamped INTERRUPTED" "INTERRUPTED" "$FILE"
139
+
140
+ # --- Scenario 6: compact must NOT heal; Stop still enforces ---------------
141
+ # enforce-devlog.sh (round-current split) hashes/reads .round-current.md,
142
+ # not devlog.md, so the dangling "mid compact" round must be seeded there
143
+ # too, and .turn-start hashed from that same file, or the later Stop call's
144
+ # hash comparison can never observe "no write since turn start".
145
+ cat > "$DEVLOG_DIR/devlog.md" <<'EOF'
146
+ ## Round 1 — 2026-09-09T12:00:00+08:00
147
+
148
+ ### User Input
149
+ ```text
150
+ mid compact
151
+ ```
152
+
153
+ ### Status
154
+ IN_PROGRESS
155
+ EOF
156
+ cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
157
+ ## Round 1 — 2026-09-09T12:00:00+08:00
158
+
159
+ ### User Input
160
+ ```text
161
+ mid compact
162
+ ```
163
+
164
+ ### Status
165
+ IN_PROGRESS
166
+ EOF
167
+ printf '%s\n' '{"round": 1, "opened_at": "2026-09-09T12:00:00+08:00"}' > "$DEVLOG_DIR/.round-open"
168
+ cksum < "$DEVLOG_DIR/.round-current.md" > "$DEVLOG_DIR/.turn-start"
169
+ OUTPUT="$(echo '{"source":"compact"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
170
+ assert_contains "compact still injects Round 1" "Round 1" "$OUTPUT"
171
+ FILE="$(cat "$DEVLOG_DIR/devlog.md")"
172
+ assert_contains "compact leaves Status IN_PROGRESS" "IN_PROGRESS" "$FILE"
173
+ assert_not_contains "compact must not stamp INTERRUPTED" "INTERRUPTED" "$FILE"
174
+ if [ -f "$DEVLOG_DIR/.round-open" ]; then
175
+ echo "PASS: compact left .round-open in place"
176
+ else
177
+ echo "FAIL: compact SessionStart must not delete .round-open"
178
+ FAIL=1
179
+ fi
180
+ echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
181
+ assert_exit "after compact skip-heal, Stop still enforces (hash equal) -> exit 2" 2 $?
182
+
183
+ # --- Scenario 7: hooks.json SessionStart matcher includes fork -------------
184
+ HOOKS_JSON="$SCRIPT_DIR/../hooks.json"
185
+ if grep -q '"matcher": "startup|resume|clear|compact|fork"' "$HOOKS_JSON"; then
186
+ echo "PASS: SessionStart matcher includes fork"
187
+ else
188
+ echo "FAIL: SessionStart matcher must be exactly startup|resume|clear|compact|fork"
189
+ FAIL=1
190
+ fi
191
+
192
+ # --- Scenario 8: source=fork heals an open skeleton (script already does) --
193
+ rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-current.md"
194
+ cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
195
+ ## Round 1 — 2026-09-09T12:00:00+08:00
196
+
197
+ ### User Input
198
+ ```text
199
+ fork me
200
+ ```
201
+
202
+ ### Status
203
+ IN_PROGRESS
204
+ EOF
205
+ printf '%s\n' '{"round": 1, "opened_at": "2026-09-09T12:00:00+08:00"}' > "$DEVLOG_DIR/.round-open"
206
+ OUTPUT="$(echo '{"source":"fork"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
207
+ assert_contains "fork still shows Round 1" "Round 1" "$OUTPUT"
208
+ assert_contains "fork heal reason" "dangling:session_start" "$OUTPUT"
209
+ FILE="$(cat "$DEVLOG_DIR/devlog.md")"
210
+ assert_contains "fork stamped INTERRUPTED" "INTERRUPTED" "$FILE"
211
+ if [ -f "$DEVLOG_DIR/.round-open" ]; then
212
+ echo "FAIL: fork SessionStart should delete .round-open after heal"
213
+ FAIL=1
214
+ else
215
+ echo "PASS: fork SessionStart deleted .round-open"
216
+ fi
217
+
218
+ # --- Scenario 9: source=clear does not inject; still heals open round ------
219
+ rm -f "$DEVLOG_DIR/.span-open"
220
+ touch "$DEVLOG_DIR/.enabled"
221
+ rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-current.md"
222
+ cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
223
+ ## Round 1 — 2026-09-09T12:00:00+08:00
224
+
225
+ ### User Input
226
+ ```text
227
+ cleared
228
+ ```
229
+
230
+ ### Status
231
+ IN_PROGRESS
232
+ EOF
233
+ printf '%s\n' '{"round": 1, "opened_at": "2026-09-09T12:00:00+08:00"}' > "$DEVLOG_DIR/.round-open"
234
+ OUTPUT="$(echo '{"source":"clear"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
235
+ if [ -z "$OUTPUT" ]; then
236
+ echo "PASS: source=clear with open round -> silent stdout"
237
+ else
238
+ echo "FAIL: source=clear must not inject context, got: $OUTPUT"
239
+ FAIL=1
240
+ fi
241
+ FILE="$(cat "$DEVLOG_DIR/devlog.md")"
242
+ assert_contains "clear still stamps INTERRUPTED on disk" "INTERRUPTED" "$FILE"
243
+ assert_contains "clear heal reason is on disk not stdout" "dangling:session_start" "$FILE"
244
+ if [ -f "$DEVLOG_DIR/.round-open" ]; then
245
+ echo "FAIL: clear SessionStart should delete .round-open after heal"
246
+ FAIL=1
247
+ else
248
+ echo "PASS: clear SessionStart deleted .round-open"
249
+ fi
250
+
251
+ # --- Scenario 10: source=clear with closed log and open span -> silent -----
252
+ cat > "$DEVLOG_DIR/devlog.md" <<'EOF'
253
+ ## Round 1 — 2026-09-09T12:00:00+08:00
254
+
255
+ ### User Input
256
+ ```text
257
+ done
258
+ ```
259
+
260
+ ### Summary
261
+ finished
262
+
263
+ ### Reply
264
+ fixture reply.
265
+
266
+ ### Handoff
267
+ #### 現況
268
+ finished
269
+
270
+ ### Status
271
+ DONE
272
+ EOF
273
+ cat > "$DEVLOG_DIR/.span-open" <<'SPANEOF'
274
+ {
275
+ "round": 1,
276
+ "opened_at": "2026-09-08T21:40:00+08:00",
277
+ "ticks_since_checkin": 2,
278
+ "max_silent_ticks": 5
279
+ }
280
+ SPANEOF
281
+ OUTPUT="$(echo '{"source":"clear"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
282
+ if [ -z "$OUTPUT" ]; then
283
+ echo "PASS: source=clear with span -> silent stdout"
284
+ else
285
+ echo "FAIL: source=clear must not inject span warning or rounds, got: $OUTPUT"
286
+ FAIL=1
287
+ fi
288
+ assert_not_contains "clear must not mention 開啟中的 span" "開啟中的 span" "$OUTPUT"
289
+ rm -f "$DEVLOG_DIR/.span-open"
290
+
291
+ # --- Scenario 11: source=resume still injects after the clear exception ----
292
+ OUTPUT="$(echo '{"source":"resume"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
293
+ assert_contains "resume still injects Round 1" "Round 1" "$OUTPUT"
294
+
295
+ # --- Scenario 12: startup recovers a genuinely-completed last Round -------
296
+ # (recovered-complete: the round had already finished writing Summary/Handoff
297
+ # before the interrupt signal arrived; close-open-round.sh's "recovered" path
298
+ # — AWK_RC=3 — must detect this via .round-current.md's hash differing from
299
+ # .turn-start, not stamp INTERRUPTED, and still merge the completed round
300
+ # into devlog.md.) Round-current split: this only exercises
301
+ # close-open-round.sh's recovered-detection logic when .round-current.md is
302
+ # actually seeded and hashed into .turn-start — an empty/absent
303
+ # .round-current.md makes close-open-round.sh early-out (drop_markers, exit
304
+ # 0) before that logic ever runs, which was the bug the final whole-branch
305
+ # review found in this scenario: it passed because nothing ran, not because
306
+ # the recovered-detection logic worked.
307
+ : > "$DEVLOG_DIR/devlog.md"
308
+ cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
309
+ ## Round 1 — 2026-09-09T12:00:00+08:00
310
+
311
+ ### User Input
312
+ ```text
313
+ done
314
+ ```
315
+
316
+ ### Status
317
+ IN_PROGRESS
318
+ EOF
319
+ printf '%s\n' '{"round": 1, "opened_at": "2026-09-09T12:00:00+08:00"}' > "$DEVLOG_DIR/.round-open"
320
+ cksum < "$DEVLOG_DIR/.round-current.md" > "$DEVLOG_DIR/.turn-start"
321
+ cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
322
+ ## Round 1 — 2026-09-09T12:00:00+08:00
323
+
324
+ ### User Input
325
+ ```text
326
+ done
327
+ ```
328
+
329
+ ### Summary
330
+ finished
331
+
332
+ ### Reply
333
+ fixture reply.
334
+
335
+ ### Handoff
336
+ #### 現況
337
+ finished
338
+
339
+ ### Status
340
+ DONE
341
+ EOF
342
+ OUTPUT="$(echo '{"source":"startup"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
343
+ assert_contains "startup still injects Round 1" "Round 1" "$OUTPUT"
344
+ FILE="$(cat "$DEVLOG_DIR/devlog.md")"
345
+ assert_contains "startup recovered keeps DONE" $'### Status\nDONE' "$FILE"
346
+ assert_not_contains "startup must not stamp a completed Round" "INTERRUPTED" "$FILE"
347
+ if [ -f "$DEVLOG_DIR/.round-current.md" ]; then
348
+ echo "FAIL: startup recovered-complete should merge .round-current.md away"
349
+ FAIL=1
350
+ else
351
+ echo "PASS: startup recovered-complete merged .round-current.md into devlog.md"
352
+ fi
353
+ if [ -f "$DEVLOG_DIR/.round-open" ]; then
354
+ echo "FAIL: startup recovered-complete should delete .round-open"
355
+ FAIL=1
356
+ else
357
+ echo "PASS: startup recovered-complete deleted .round-open"
358
+ fi
359
+
360
+ # --- Scenario 13: excerpt is last two rounds, not eight -------------------
361
+ : > "$DEVLOG_DIR/devlog.md"
362
+ rm -f "$DEVLOG_DIR/.span-open"
363
+ n=1
364
+ while [ "$n" -le 8 ]; do
365
+ write_round "$n" DONE
366
+ n=$((n + 1))
367
+ done
368
+ OUTPUT="$(echo '{"source":"startup"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
369
+ assert_contains "excerpt names Round 8" "Round 8" "$OUTPUT"
370
+ assert_contains "excerpt names Round 7" "Round 7" "$OUTPUT"
371
+ assert_not_contains "excerpt drops Round 6" "Round 6" "$OUTPUT"
372
+ assert_not_contains "excerpt drops 段落" "secret intermediate" "$OUTPUT"
373
+ assert_not_contains "excerpt drops User Input when Summary exists" "prompt 8 with lots of bulk" "$OUTPUT"
374
+ assert_contains "excerpt keeps Summary" "summary 8" "$OUTPUT"
375
+ assert_contains "excerpt keeps Handoff" "handoff 8" "$OUTPUT"
376
+
377
+ # --- Scenario 14: last Checkpoint is included even if older than last 2 ---
378
+ cat >> "$DEVLOG_DIR/devlog.md" <<'EOF'
379
+
380
+ ## Checkpoint(Round 1-6 摘要)
381
+ old checkpoint body
382
+ EOF
383
+ write_round 9 DONE
384
+ write_round 10 DONE
385
+ OUTPUT="$(echo '{"source":"startup"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
386
+ assert_contains "includes last checkpoint" "old checkpoint body" "$OUTPUT"
387
+ assert_contains "still has Round 10" "Round 10" "$OUTPUT"
388
+ assert_not_contains "still drops Round 8 body after more writes" "summary 8" "$OUTPUT"
389
+
390
+ # --- Scenario 15: fenced ## Round inside User Input is not a real Round ---
391
+ cat > "$DEVLOG_DIR/devlog.md" <<'EOF'
392
+ ## Round 1 — 2026-09-09T12:00:00+08:00
393
+
394
+ ### User Input
395
+ ```text
396
+ ## Round 99 — fake
397
+ ```
398
+
399
+ ### Summary
400
+ real one
401
+
402
+ ### Reply
403
+ fixture reply.
404
+
405
+ ### Handoff
406
+ #### 現況
407
+ real handoff
408
+
409
+ ### Status
410
+ DONE
411
+ EOF
412
+ OUTPUT="$(echo '{"source":"startup"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
413
+ assert_contains "fence-aware uses Round 1" "Round 1" "$OUTPUT"
414
+ assert_not_contains "fence-aware ignores Round 99 heading as a round" "Round 99" "$OUTPUT"
415
+
416
+ # --- Scenario 16: skeleton last Round includes User Input -----------------
417
+ cat >> "$DEVLOG_DIR/devlog.md" <<'EOF'
418
+
419
+ ## Round 2 — 2026-09-09T12:01:00+08:00
420
+
421
+ ### User Input
422
+ ```text
423
+ unfinished prompt
424
+ ```
425
+
426
+ ### Status
427
+ IN_PROGRESS
428
+ EOF
429
+ OUTPUT="$(echo '{"source":"startup"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
430
+ assert_contains "skeleton User Input is shown" "unfinished prompt" "$OUTPUT"
431
+
432
+ # --- Scenario 17: clear still silent with a long log ----------------------
433
+ OUTPUT="$(echo '{"source":"clear"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
434
+ if [ -z "$OUTPUT" ]; then echo "PASS: clear still silent after excerpt change"
435
+ else echo "FAIL: clear injected: $OUTPUT"; FAIL=1; fi
436
+
437
+ # --- Scenario: Kept 索引 block is surfaced in the injected excerpt ---------
438
+ : > "$DEVLOG_DIR/devlog.md"
439
+ write_round 1 DONE
440
+ cat >> "$DEVLOG_DIR/devlog.md" <<'EOF'
441
+
442
+ ## Kept 索引
443
+ - `devlog.topic-a.md`:Round 1-1,kept_at 2026-09-10T00:00:00+08:00
444
+ EOF
445
+ OUTPUT="$(echo '{"source":"startup"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
446
+ assert_contains "startup excerpt includes Kept 索引 heading" "## Kept 索引" "$OUTPUT"
447
+ assert_contains "startup excerpt includes the kept-file line" "devlog.topic-a.md" "$OUTPUT"
448
+
449
+ # --- Scenario: no Kept 索引 block -> excerpt omits the heading entirely ----
450
+ : > "$DEVLOG_DIR/devlog.md"
451
+ write_round 1 DONE
452
+ OUTPUT="$(echo '{"source":"startup"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
453
+ assert_not_contains "no Kept 索引 block -> excerpt has no such heading" "## Kept 索引" "$OUTPUT"
454
+
455
+ # --- Scenario: source=clear still prints nothing, even with a Kept 索引 ----
456
+ cat >> "$DEVLOG_DIR/devlog.md" <<'EOF'
457
+
458
+ ## Kept 索引
459
+ - `devlog.topic-a.md`:Round 1-1,kept_at 2026-09-10T00:00:00+08:00
460
+ EOF
461
+ OUTPUT="$(echo '{"source":"clear"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
462
+ if [ -z "$OUTPUT" ]; then
463
+ echo "PASS: source=clear stays silent even with a Kept 索引 block present"
464
+ else
465
+ echo "FAIL: source=clear must not inject anything, got: $OUTPUT"
466
+ FAIL=1
467
+ fi
468
+
469
+ # --- Scenario: Lessons 索引 block is surfaced in the injected excerpt ------
470
+ # (docs/design/lessons-mode.md「## Lessons 索引」)
471
+ : > "$DEVLOG_DIR/devlog.md"
472
+ write_round 1 DONE
473
+ cat >> "$DEVLOG_DIR/devlog.md" <<'EOF'
474
+
475
+ ## Lessons 索引
476
+ - `devlog.lessons.span-mode-detour.md`:1 則,最新一則「先卡住了。」(updated_at 2026-09-12T00:00:00+08:00)
477
+ EOF
478
+ OUTPUT="$(echo '{"source":"startup"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
479
+ assert_contains "startup excerpt includes Lessons 索引 heading" "## Lessons 索引" "$OUTPUT"
480
+ assert_contains "startup excerpt includes the lessons-file line" "devlog.lessons.span-mode-detour.md" "$OUTPUT"
481
+
482
+ # --- Scenario: no Lessons 索引 block -> excerpt omits the heading entirely -
483
+ : > "$DEVLOG_DIR/devlog.md"
484
+ write_round 1 DONE
485
+ OUTPUT="$(echo '{"source":"startup"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
486
+ assert_not_contains "no Lessons 索引 block -> excerpt has no such heading" "## Lessons 索引" "$OUTPUT"
487
+
488
+ # --- Scenario: source=clear still prints nothing, even with a Lessons 索引 -
489
+ cat >> "$DEVLOG_DIR/devlog.md" <<'EOF'
490
+
491
+ ## Lessons 索引
492
+ - `devlog.lessons.span-mode-detour.md`:1 則,最新一則「先卡住了。」(updated_at 2026-09-12T00:00:00+08:00)
493
+ EOF
494
+ OUTPUT="$(echo '{"source":"clear"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
495
+ if [ -z "$OUTPUT" ]; then
496
+ echo "PASS: source=clear stays silent even with a Lessons 索引 block present"
497
+ else
498
+ echo "FAIL: source=clear must not inject anything, got: $OUTPUT"
499
+ FAIL=1
500
+ fi
501
+
502
+ if [ "$FAIL" -eq 0 ]; then
503
+ echo "All checks passed."
504
+ exit 0
505
+ else
506
+ echo "Some checks FAILED."
507
+ exit 1
508
+ fi
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
4
+ TMP_ROOT="$(mktemp -d)"
5
+ trap 'rm -rf "$TMP_ROOT"' EXIT
6
+ export CLAUDE_PROJECT_DIR="$TMP_ROOT"
7
+ FAIL=0
8
+ assert_exit() {
9
+ local desc="$1" expected="$2" actual="$3"
10
+ if [ "$actual" -eq "$expected" ]; then echo "PASS: $desc"
11
+ else echo "FAIL: $desc (expected $expected got $actual)"; FAIL=1; fi
12
+ }
13
+ assert_file() {
14
+ local desc="$1" path="$2"
15
+ if [ -f "$path" ]; then echo "PASS: $desc"
16
+ else echo "FAIL: $desc"; FAIL=1; fi
17
+ }
18
+ assert_not_file() {
19
+ local desc="$1" path="$2"
20
+ if [ ! -f "$path" ]; then echo "PASS: $desc"
21
+ else echo "FAIL: $desc"; FAIL=1; fi
22
+ }
23
+
24
+ # start on empty project
25
+ OUT="$(bash "$SCRIPT_DIR/start-devlog.sh")"
26
+ assert_exit "start empty -> 0" 0 $?
27
+ assert_file ".enabled" "$TMP_ROOT/.devlog/.enabled"
28
+ assert_file "checkpoint" "$TMP_ROOT/.devlog/.checkpoint-state"
29
+ assert_file "segment" "$TMP_ROOT/.devlog/.segment-state"
30
+ assert_not_file "no devlog.md created" "$TMP_ROOT/.devlog/devlog.md"
31
+ case "$OUT" in
32
+ *"GITIGNORE_DEVLOG=no"*) echo "PASS: reports missing gitignore entry" ;;
33
+ *) echo "FAIL: expected GITIGNORE_DEVLOG=no, got $OUT"; FAIL=1 ;;
34
+ esac
35
+ grep -q '"max_silent_rounds": 20' "$TMP_ROOT/.devlog/.checkpoint-state" \
36
+ && echo "PASS: default checkpoint" || { echo "FAIL: checkpoint defaults"; FAIL=1; }
37
+ grep -q '"max_silent_seconds": 600' "$TMP_ROOT/.devlog/.segment-state" \
38
+ && echo "PASS: default segment" || { echo "FAIL: segment defaults"; FAIL=1; }
39
+ grep -q '"session_id": ""' "$TMP_ROOT/.devlog/.segment-state" \
40
+ && echo "PASS: default segment session id" || { echo "FAIL: segment session id"; FAIL=1; }
41
+
42
+ # preserve thresholds on second start
43
+ printf '%s\n' '{"rounds_since_checkpoint": 7, "max_silent_rounds": 3, "checkpoint_marker_count": 1}' \
44
+ > "$TMP_ROOT/.devlog/.checkpoint-state"
45
+ printf '%s\n' '{"last_change_epoch": 1, "last_seen_cksum": "x", "max_silent_seconds": 60}' \
46
+ > "$TMP_ROOT/.devlog/.segment-state"
47
+ bash "$SCRIPT_DIR/start-devlog.sh" >/dev/null
48
+ grep -q '"max_silent_rounds": 3' "$TMP_ROOT/.devlog/.checkpoint-state" \
49
+ && echo "PASS: start does not reset max_silent_rounds" || { echo "FAIL: checkpoint reset"; FAIL=1; }
50
+ grep -q '"max_silent_seconds": 60' "$TMP_ROOT/.devlog/.segment-state" \
51
+ && echo "PASS: start does not reset max_silent_seconds" || { echo "FAIL: segment reset"; FAIL=1; }
52
+
53
+ # gitignore already has .devlog/
54
+ echo '.devlog/' > "$TMP_ROOT/.gitignore"
55
+ OUT="$(bash "$SCRIPT_DIR/start-devlog.sh")"
56
+ case "$OUT" in
57
+ *"GITIGNORE_DEVLOG=yes"*) echo "PASS: detects gitignore entry" ;;
58
+ *) echo "FAIL: expected GITIGNORE_DEVLOG=yes, got $OUT"; FAIL=1 ;;
59
+ esac
60
+
61
+ # pause
62
+ touch "$TMP_ROOT/.devlog/.span-open" "$TMP_ROOT/.devlog/.round-open" "$TMP_ROOT/.devlog/.interrupted" "$TMP_ROOT/.devlog/.awaiting-reply"
63
+ echo 'keep me' > "$TMP_ROOT/.devlog/devlog.md"
64
+ OUT="$(bash "$SCRIPT_DIR/pause-devlog.sh")"
65
+ assert_exit "pause -> 0" 0 $?
66
+ assert_not_file "enabled gone" "$TMP_ROOT/.devlog/.enabled"
67
+ assert_not_file "span gone" "$TMP_ROOT/.devlog/.span-open"
68
+ assert_not_file "round-open gone" "$TMP_ROOT/.devlog/.round-open"
69
+ assert_not_file "interrupted gone" "$TMP_ROOT/.devlog/.interrupted"
70
+ assert_not_file "awaiting-reply gone" "$TMP_ROOT/.devlog/.awaiting-reply"
71
+ assert_file "log kept" "$TMP_ROOT/.devlog/devlog.md"
72
+ assert_file "checkpoint kept" "$TMP_ROOT/.devlog/.checkpoint-state"
73
+
74
+ # pause when never started
75
+ rm -rf "$TMP_ROOT/.devlog"
76
+ OUT="$(bash "$SCRIPT_DIR/pause-devlog.sh")"
77
+ assert_exit "pause never-started -> 0" 0 $?
78
+ case "$OUT" in
79
+ *"NOT_ENABLED"*) echo "PASS: pause reports NOT_ENABLED" ;;
80
+ *) echo "FAIL: expected NOT_ENABLED, got $OUT"; FAIL=1 ;;
81
+ esac
82
+
83
+ if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
84
+ else echo "Some checks FAILED."; exit 1; fi
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
4
+ TMP="$(mktemp -d)"
5
+ trap 'rm -rf "$TMP"' EXIT
6
+ export CLAUDE_PROJECT_DIR="$TMP"
7
+ FAIL=0
8
+ pass() { echo "PASS: $1"; }
9
+ fail() { echo "FAIL: $1"; FAIL=1; }
10
+
11
+ OUT="$(bash "$SCRIPT_DIR/status-devlog.sh")"
12
+ [ "$OUT" = "NOT_STARTED" ] && pass "not started" || fail "not started [$OUT]"
13
+
14
+ mkdir -p "$TMP/.devlog"
15
+ touch "$TMP/.devlog/.enabled"
16
+ cat > "$TMP/.devlog/devlog.md" <<'EOF'
17
+ ## Round 1 — now
18
+
19
+ ### Status
20
+ DONE
21
+ EOF
22
+ OUT="$(bash "$SCRIPT_DIR/status-devlog.sh")"
23
+ case "$OUT" in *"ENABLED=yes"*) pass "enabled" ;; *) fail "enabled [$OUT]" ;; esac
24
+ case "$OUT" in *"LESSONS=no"*) pass "lessons off by default" ;; *) fail "lessons off by default [$OUT]" ;; esac
25
+ case "$OUT" in *"SPAN=closed"*) pass "span closed" ;; *) fail "span closed [$OUT]" ;; esac
26
+ case "$OUT" in *"LAST_STATUS=DONE"*) pass "last status" ;; *) fail "last status [$OUT]" ;; esac
27
+
28
+ touch "$TMP/.devlog/.lessons-enabled"
29
+ OUT="$(bash "$SCRIPT_DIR/status-devlog.sh")"
30
+ case "$OUT" in *"LESSONS=yes"*) pass "lessons on when flag present" ;; *) fail "lessons on when flag present [$OUT]" ;; esac
31
+ rm -f "$TMP/.devlog/.lessons-enabled"
32
+
33
+ printf '%s\n' '{"mismatch_count": 1, "threshold": 3}' > "$TMP/.devlog/.lessons-drift-state"
34
+ OUT="$(bash "$SCRIPT_DIR/status-devlog.sh")"
35
+ case "$OUT" in *"LESSONS_DRIFT=1/3"*) pass "lessons drift status line" ;; *) fail "lessons drift status line [$OUT]" ;; esac
36
+ rm -f "$TMP/.devlog/.lessons-drift-state"
37
+
38
+ OUT="$(bash "$SCRIPT_DIR/span-open.sh")"
39
+ [ "$OUT" = "OPENED=1" ] && pass "span opened" || fail "span open [$OUT]"
40
+ grep -q '"round": 1' "$TMP/.devlog/.span-open" && pass "round stored" || fail "round stored"
41
+ grep -q '"ticks_since_checkin": 0' "$TMP/.devlog/.span-open" && pass "ticks default" || fail "ticks default"
42
+ grep -q '"max_silent_ticks": 5' "$TMP/.devlog/.span-open" && pass "max default" || fail "max default"
43
+ bash "$SCRIPT_DIR/span-open.sh" >/dev/null 2>&1
44
+ [ "$?" -eq 1 ] && pass "second open rejected" || fail "second open"
45
+ OUT="$(bash "$SCRIPT_DIR/span-close.sh")"
46
+ [ "$OUT" = "CLOSED" ] && pass "span closed output" || fail "close output [$OUT]"
47
+ [ ! -e "$TMP/.devlog/.span-open" ] && pass "span file removed" || fail "span remains"
48
+
49
+ if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
50
+ else echo "Some checks FAILED."; exit 1; fi