devlog-tracker 0.25.1 → 0.29.2

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 (130) hide show
  1. package/README.md +131 -151
  2. package/README.zh-TW.md +235 -0
  3. package/claude/hooks.json +75 -0
  4. package/cli/agents-md.js +15 -8
  5. package/cli/agents-md.test.js +21 -1
  6. package/cli/init.js +6 -4
  7. package/cli/init.test.js +5 -4
  8. package/cli/merge-hooks.js +5 -2
  9. package/cli/merge-hooks.test.js +52 -0
  10. package/cli/platforms/claude.js +55 -0
  11. package/cli/platforms/claude.test.js +178 -0
  12. package/cli/platforms/codex.js +6 -18
  13. package/cli/skills-from-commands.js +32 -0
  14. package/cli/skills-from-commands.test.js +51 -0
  15. package/cli/vendor.js +8 -1
  16. package/cli/vendor.test.js +24 -5
  17. package/codex/hooks/on-pre-tool.sh +3 -3
  18. package/codex/hooks/on-session-end.sh +2 -2
  19. package/codex/hooks/on-session-start.sh +2 -2
  20. package/codex/hooks/on-stop.sh +3 -3
  21. package/codex/hooks/on-user-prompt-submit.sh +2 -2
  22. package/codex/hooks/test-adapters.sh +2 -2
  23. package/commands/checkpoint.md +2 -2
  24. package/commands/clean.md +3 -3
  25. package/commands/compact.md +3 -3
  26. package/commands/continue.md +4 -4
  27. package/commands/keep.md +21 -5
  28. package/commands/lessons-drift.md +4 -4
  29. package/commands/lessons-off.md +3 -3
  30. package/commands/lessons-on.md +3 -3
  31. package/commands/lessons.md +3 -3
  32. package/commands/overview.md +3 -3
  33. package/commands/pause.md +3 -3
  34. package/commands/resume.md +3 -3
  35. package/commands/search.md +22 -0
  36. package/commands/segment-watch.md +3 -3
  37. package/commands/span.md +2 -2
  38. package/commands/start.md +3 -3
  39. package/commands/status.md +5 -4
  40. package/{hooks → core}/scripts/await-open.sh +1 -1
  41. package/{hooks → core}/scripts/checkpoint-set.sh +1 -1
  42. package/{hooks → core}/scripts/clean-devlog.sh +2 -1
  43. package/{hooks → core}/scripts/close-open-round.sh +1 -1
  44. package/{hooks → core}/scripts/compact-devlog.sh +1 -1
  45. package/{hooks → core}/scripts/devlog-lock.sh +17 -1
  46. package/{hooks → core}/scripts/devlog-path.sh +15 -8
  47. package/{hooks → core}/scripts/enforce-devlog.sh +26 -1
  48. package/core/scripts/handoff-file.sh +106 -0
  49. package/{hooks → core}/scripts/keep-move.sh +1 -1
  50. package/{hooks → core}/scripts/kept-list.sh +1 -1
  51. package/core/scripts/lessons-advisory-state.sh +51 -0
  52. package/{hooks → core}/scripts/lessons-append.sh +18 -2
  53. package/core/scripts/lessons-drift-set.sh +46 -0
  54. package/{hooks → core}/scripts/lessons-off.sh +1 -1
  55. package/{hooks → core}/scripts/lessons-on.sh +9 -4
  56. package/{hooks → core}/scripts/lessons-read.sh +1 -1
  57. package/{hooks → core}/scripts/on-tool-failure.sh +1 -1
  58. package/{hooks → core}/scripts/pause-devlog.sh +1 -1
  59. package/core/scripts/project-dir.sh +19 -0
  60. package/{hooks → core}/scripts/resume-devlog.sh +1 -1
  61. package/{hooks → core}/scripts/round-start.sh +52 -17
  62. package/{hooks → core}/scripts/run-tests.sh +1 -1
  63. package/core/scripts/search-devlog.sh +63 -0
  64. package/{hooks → core}/scripts/segment-watch-set.sh +1 -1
  65. package/{hooks → core}/scripts/segment-watch.sh +1 -1
  66. package/{hooks → core}/scripts/session-start-devlog.sh +8 -1
  67. package/{hooks → core}/scripts/span-close.sh +1 -1
  68. package/{hooks → core}/scripts/span-open.sh +1 -1
  69. package/{hooks → core}/scripts/start-devlog.sh +1 -1
  70. package/{hooks → core}/scripts/status-devlog.sh +5 -5
  71. package/{hooks → core}/scripts/tests/test-clean-devlog.sh +12 -0
  72. package/core/scripts/tests/test-cli-init-e2e.sh +68 -0
  73. package/core/scripts/tests/test-devlog-lock.sh +76 -0
  74. package/{hooks → core}/scripts/tests/test-devlog-path.sh +44 -0
  75. package/{hooks → core}/scripts/tests/test-enforce-devlog-files.sh +11 -0
  76. package/core/scripts/tests/test-enforce-devlog-session-handoff.sh +178 -0
  77. package/{hooks → core}/scripts/tests/test-enforce-devlog-workspace.sh +35 -0
  78. package/{hooks → core}/scripts/tests/test-enforce-devlog.sh +174 -0
  79. package/core/scripts/tests/test-handoff-file.sh +98 -0
  80. package/core/scripts/tests/test-lessons-advisory-state.sh +55 -0
  81. package/{hooks → core}/scripts/tests/test-lessons-append.sh +18 -0
  82. package/{hooks → core}/scripts/tests/test-lessons-drift-set.sh +14 -5
  83. package/{hooks → core}/scripts/tests/test-lessons-on-off.sh +16 -6
  84. package/core/scripts/tests/test-project-dir.sh +30 -0
  85. package/{hooks → core}/scripts/tests/test-round-start.sh +247 -13
  86. package/core/scripts/tests/test-search-devlog.sh +126 -0
  87. package/{hooks → core}/scripts/tests/test-session-start-devlog.sh +61 -2
  88. package/{hooks → core}/scripts/tests/test-status-span.sh +3 -3
  89. package/{hooks → core}/scripts/workspace-snapshot.sh +2 -2
  90. package/cursor/hooks/on-pre-tool.sh +2 -2
  91. package/cursor/hooks/on-session-end.sh +2 -2
  92. package/cursor/hooks/on-session-start.sh +2 -2
  93. package/cursor/hooks/on-stop.sh +2 -2
  94. package/cursor/hooks/on-submit-prompt.sh +2 -2
  95. package/cursor/hooks/on-tool-failure.sh +2 -2
  96. package/package.json +5 -4
  97. package/skills/devlog-tracker/SKILL.md +90 -22
  98. package/skills/devlog-tracker/references/checkpoint-mode.md +21 -6
  99. package/skills/devlog-tracker/references/contract.md +83 -0
  100. package/skills/devlog-tracker/references/lessons-mode.md +33 -9
  101. package/skills/devlog-tracker/references/reply-fold.md +1 -1
  102. package/hooks/scripts/lessons-drift-set.sh +0 -42
  103. package/hooks/scripts/tests/test-cli-init-e2e.sh +0 -38
  104. package/hooks/scripts/tests/test-devlog-lock.sh +0 -37
  105. /package/{hooks → core}/scripts/detect-pending-question.sh +0 -0
  106. /package/{hooks → core}/scripts/devlog-md.sh +0 -0
  107. /package/{hooks → core}/scripts/files-snapshot.sh +0 -0
  108. /package/{hooks → core}/scripts/json-field.sh +0 -0
  109. /package/{hooks → core}/scripts/on-session-end.sh +0 -0
  110. /package/{hooks → core}/scripts/on-stop-failure.sh +0 -0
  111. /package/{hooks → core}/scripts/redact-prompt.sh +0 -0
  112. /package/{hooks → core}/scripts/tests/test-await-open.sh +0 -0
  113. /package/{hooks → core}/scripts/tests/test-branch-scoped-integration.sh +0 -0
  114. /package/{hooks → core}/scripts/tests/test-checkpoint-set.sh +0 -0
  115. /package/{hooks → core}/scripts/tests/test-close-open-round.sh +0 -0
  116. /package/{hooks → core}/scripts/tests/test-compact-devlog.sh +0 -0
  117. /package/{hooks → core}/scripts/tests/test-devlog-md.sh +0 -0
  118. /package/{hooks → core}/scripts/tests/test-enforce-devlog-handoff-order.sh +0 -0
  119. /package/{hooks → core}/scripts/tests/test-files-snapshot.sh +0 -0
  120. /package/{hooks → core}/scripts/tests/test-json-field.sh +0 -0
  121. /package/{hooks → core}/scripts/tests/test-keep-move.sh +0 -0
  122. /package/{hooks → core}/scripts/tests/test-kept-list.sh +0 -0
  123. /package/{hooks → core}/scripts/tests/test-lessons-read.sh +0 -0
  124. /package/{hooks → core}/scripts/tests/test-on-interrupt.sh +0 -0
  125. /package/{hooks → core}/scripts/tests/test-redact-prompt.sh +0 -0
  126. /package/{hooks → core}/scripts/tests/test-resume-devlog.sh +0 -0
  127. /package/{hooks → core}/scripts/tests/test-segment-watch-set.sh +0 -0
  128. /package/{hooks → core}/scripts/tests/test-segment-watch.sh +0 -0
  129. /package/{hooks → core}/scripts/tests/test-start-pause-devlog.sh +0 -0
  130. /package/{hooks → core}/scripts/tests/test-workspace-snapshot.sh +0 -0
@@ -14,23 +14,32 @@ OUT="$(CLAUDE_PROJECT_DIR="$TMP" bash "$SCRIPT_DIR/lessons-drift-set.sh" 5)"
14
14
  [ "$OUT" = "LESSONS_NOT_ENABLED" ] && echo "PASS: lessons not enabled" || { echo "FAIL: $OUT"; FAIL=1; }
15
15
 
16
16
  touch "$TMP/.devlog/.lessons-enabled"
17
- printf '%s\n' '{"mismatch_count": 2, "threshold": 3}' > "$TMP/.devlog/.lessons-drift-state"
17
+ printf '%s\n' '{"count": 2, "threshold": 3}' > "$TMP/.devlog/.lessons-advisory-state"
18
18
 
19
19
  OUT="$(CLAUDE_PROJECT_DIR="$TMP" bash "$SCRIPT_DIR/lessons-drift-set.sh" 5)"
20
20
  case "$OUT" in *LESSONS_DRIFT_THRESHOLD=5*) echo "PASS: set 5" ;; *) echo "FAIL: $OUT"; FAIL=1 ;; esac
21
- grep -q '"mismatch_count": 2' "$TMP/.devlog/.lessons-drift-state" \
21
+ grep -q '"count": 2' "$TMP/.devlog/.lessons-advisory-state" \
22
22
  && echo "PASS: count preserved" || { echo "FAIL: count reset"; FAIL=1; }
23
- grep -q '"threshold": 5' "$TMP/.devlog/.lessons-drift-state" \
23
+ grep -q '"threshold": 5' "$TMP/.devlog/.lessons-advisory-state" \
24
24
  && echo "PASS: threshold written" || { echo "FAIL: threshold"; FAIL=1; }
25
25
 
26
26
  CLAUDE_PROJECT_DIR="$TMP" bash "$SCRIPT_DIR/lessons-drift-set.sh" 0 >/dev/null 2>&1
27
27
  [ $? -eq 1 ] && echo "PASS: reject 0" || { echo "FAIL: accepted 0"; FAIL=1; }
28
28
 
29
- rm -f "$TMP/.devlog/.lessons-drift-state"
29
+ rm -f "$TMP/.devlog/.lessons-advisory-state"
30
30
  OUT="$(CLAUDE_PROJECT_DIR="$TMP" bash "$SCRIPT_DIR/lessons-drift-set.sh" 7)"
31
31
  case "$OUT" in *LESSONS_DRIFT_THRESHOLD=7*) echo "PASS: create 7" ;; *) echo "FAIL: create $OUT"; FAIL=1 ;; esac
32
- grep -q '"mismatch_count": 0' "$TMP/.devlog/.lessons-drift-state" \
32
+ grep -q '"count": 0' "$TMP/.devlog/.lessons-advisory-state" \
33
33
  && echo "PASS: create count 0" || { echo "FAIL: create count"; FAIL=1; }
34
34
 
35
+ # --- migrates an old .lessons-drift-state before applying the new threshold
36
+ rm -f "$TMP/.devlog/.lessons-advisory-state"
37
+ printf '%s\n' '{"mismatch_count": 4, "threshold": 3}' > "$TMP/.devlog/.lessons-drift-state"
38
+ OUT="$(CLAUDE_PROJECT_DIR="$TMP" bash "$SCRIPT_DIR/lessons-drift-set.sh" 9)"
39
+ case "$OUT" in *LESSONS_DRIFT_THRESHOLD=9*) echo "PASS: migrate then set 9" ;; *) echo "FAIL: $OUT"; FAIL=1 ;; esac
40
+ [ ! -f "$TMP/.devlog/.lessons-drift-state" ] && echo "PASS: old file removed after migrate+set" || { echo "FAIL: old file still present"; FAIL=1; }
41
+ grep -q '"count": 4' "$TMP/.devlog/.lessons-advisory-state" && echo "PASS: migrated count preserved through set" || { echo "FAIL: migrated count lost"; FAIL=1; }
42
+ grep -q '"threshold": 9' "$TMP/.devlog/.lessons-advisory-state" && echo "PASS: new threshold applied after migrate" || { echo "FAIL: threshold not applied"; FAIL=1; }
43
+
35
44
  if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
36
45
  else echo "Some checks FAILED."; exit 1; fi
@@ -44,11 +44,21 @@ case "$OUT" in
44
44
  *"LESSONS_ENABLED="*) echo "PASS: reports LESSONS_ENABLED" ;;
45
45
  *) echo "FAIL: expected LESSONS_ENABLED=..., got $OUT"; FAIL=1 ;;
46
46
  esac
47
- assert_file ".lessons-drift-state created alongside .lessons-enabled" "$TMP_ROOT/.devlog/.lessons-drift-state"
48
- grep -q '"mismatch_count": 0' "$TMP_ROOT/.devlog/.lessons-drift-state" \
49
- && echo "PASS: drift count starts at 0" || { echo "FAIL: drift count not 0"; FAIL=1; }
50
- grep -q '"threshold": 3' "$TMP_ROOT/.devlog/.lessons-drift-state" \
51
- && echo "PASS: drift threshold defaults to 3" || { echo "FAIL: drift threshold not 3"; FAIL=1; }
47
+ assert_file ".lessons-advisory-state created alongside .lessons-enabled" "$TMP_ROOT/.devlog/.lessons-advisory-state"
48
+ grep -q '"count": 0' "$TMP_ROOT/.devlog/.lessons-advisory-state" \
49
+ && echo "PASS: advisory count starts at 0" || { echo "FAIL: advisory count not 0"; FAIL=1; }
50
+ grep -q '"threshold": 3' "$TMP_ROOT/.devlog/.lessons-advisory-state" \
51
+ && echo "PASS: advisory threshold defaults to 3" || { echo "FAIL: advisory threshold not 3"; FAIL=1; }
52
+
53
+ # --- lessons-on migrates an old .lessons-drift-state on re-run -------------
54
+ rm -f "$TMP_ROOT/.devlog/.lessons-advisory-state"
55
+ printf '%s\n' '{"mismatch_count": 2, "threshold": 7}' > "$TMP_ROOT/.devlog/.lessons-drift-state"
56
+ OUT="$(bash "$SCRIPT_DIR/lessons-on.sh")"
57
+ assert_exit "lessons-on migrates old state file -> 0" 0 $?
58
+ assert_file "migration produced .lessons-advisory-state" "$TMP_ROOT/.devlog/.lessons-advisory-state"
59
+ assert_not_file "migration removed .lessons-drift-state" "$TMP_ROOT/.devlog/.lessons-drift-state"
60
+ grep -q '"count": 2' "$TMP_ROOT/.devlog/.lessons-advisory-state" && echo "PASS: migrated count preserved" || { echo "FAIL: migrated count lost"; FAIL=1; }
61
+ grep -q '"threshold": 7' "$TMP_ROOT/.devlog/.lessons-advisory-state" && echo "PASS: migrated threshold preserved" || { echo "FAIL: migrated threshold lost"; FAIL=1; }
52
62
 
53
63
  # --- lessons-off disables, leaves main switch and any lessons files alone --
54
64
  echo 'keep me' > "$TMP_ROOT/.devlog/devlog.lessons.foo.md"
@@ -61,7 +71,7 @@ esac
61
71
  assert_not_file ".lessons-enabled removed" "$TMP_ROOT/.devlog/.lessons-enabled"
62
72
  assert_file "main .enabled untouched" "$TMP_ROOT/.devlog/.enabled"
63
73
  assert_file "lessons file untouched" "$TMP_ROOT/.devlog/devlog.lessons.foo.md"
64
- assert_file ".lessons-drift-state untouched by lessons-off" "$TMP_ROOT/.devlog/.lessons-drift-state"
74
+ assert_file ".lessons-advisory-state untouched by lessons-off" "$TMP_ROOT/.devlog/.lessons-advisory-state"
65
75
 
66
76
  # --- lessons-off when already off -> reports NOT_ENABLED, no error --------
67
77
  OUT="$(bash "$SCRIPT_DIR/lessons-off.sh")"
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
4
+ BASH_BIN="$(command -v bash)"
5
+ FAIL=0
6
+
7
+ run_case() {
8
+ local desc="$1" expected="$2"
9
+ shift 2
10
+ local actual
11
+ actual="$(env -i PATH="$PATH" "$@" "$BASH_BIN" -c '. "'"$DIR"'/project-dir.sh"; devlog_project_dir')"
12
+ if [ "$actual" = "$expected" ]; then
13
+ echo "PASS: $desc"
14
+ else
15
+ echo "FAIL: $desc (expected [$expected], got [$actual])"
16
+ FAIL=1
17
+ fi
18
+ }
19
+
20
+ run_case "neither var set falls back to ." "."
21
+ run_case "CLAUDE_PROJECT_DIR alone is honored" "/legacy" \
22
+ CLAUDE_PROJECT_DIR=/legacy
23
+ run_case "DEVLOG_PROJECT_DIR alone is honored" "/neutral" \
24
+ DEVLOG_PROJECT_DIR=/neutral
25
+ run_case "DEVLOG_PROJECT_DIR wins over CLAUDE_PROJECT_DIR" "/neutral" \
26
+ DEVLOG_PROJECT_DIR=/neutral CLAUDE_PROJECT_DIR=/legacy
27
+
28
+ if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
29
+ else echo "Some checks FAILED."; exit 1
30
+ fi
@@ -634,6 +634,49 @@ fi
634
634
  assert_file_absent "task-notification during open span: no .round-open" "$DEVLOG_DIR/.round-open"
635
635
  rm -f "$DEVLOG_DIR/.span-open"
636
636
 
637
+ # --- 19: devlog-tracker admin slash commands are exempt from Round logging -
638
+ rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open" "$DEVLOG_DIR/.turn-start" "$DEVLOG_DIR/.round-current.md" "$DEVLOG_DIR/.checkpoint-state"
639
+ printf '%s\n' '{"rounds_since_checkpoint": 3, "max_silent_rounds": 20, "checkpoint_marker_count": 0}' > "$DEVLOG_DIR/.checkpoint-state"
640
+ ADMIN_PROMPT='<command-name>/devlog-tracker:status</command-name><command-message>status</command-message><command-args></command-args>'
641
+ printf '{"prompt":"%s"}' "$ADMIN_PROMPT" | bash "$SCRIPT_DIR/round-start.sh"
642
+ assert_file_absent "admin command: no .round-current.md" "$DEVLOG_DIR/.round-current.md"
643
+ assert_file_absent "admin command: no .round-open" "$DEVLOG_DIR/.round-open"
644
+ assert_file_absent "admin command: no .turn-start" "$DEVLOG_DIR/.turn-start"
645
+ assert_file_absent "admin command: no devlog.md created" "$DEVLOG_DIR/devlog.md"
646
+ CP="$(grep -o '"rounds_since_checkpoint"[[:space:]]*:[[:space:]]*[0-9]\+' "$DEVLOG_DIR/.checkpoint-state" | grep -o '[0-9]\+$')"
647
+ if [ "$CP" = "3" ]; then
648
+ echo "PASS: admin command does not increment rounds_since_checkpoint"
649
+ else
650
+ echo "FAIL: expected rounds_since_checkpoint to stay 3, got $CP"
651
+ FAIL=1
652
+ fi
653
+ rm -f "$DEVLOG_DIR/.checkpoint-state"
654
+
655
+ # --- 20: a namespaced-but-different command name is not falsely exempted ---
656
+ # "lessons-on" must not match the "lessons" case arm (prefix collision guard).
657
+ LESSONS_ON_PROMPT='<command-name>/devlog-tracker:lessons-on</command-name><command-message>lessons-on</command-message><command-args></command-args>'
658
+ printf '{"prompt":"%s"}' "$LESSONS_ON_PROMPT" | bash "$SCRIPT_DIR/round-start.sh"
659
+ if [ -f "$DEVLOG_DIR/.round-current.md" ]; then
660
+ echo "FAIL: lessons-on command should still be exempt (matched its own arm)"
661
+ FAIL=1
662
+ else
663
+ echo "PASS: lessons-on command exempt via its own case arm"
664
+ fi
665
+
666
+ # --- 21: /devlog-tracker:continue is NOT exempt -- real work may follow ----
667
+ rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open" "$DEVLOG_DIR/.turn-start" "$DEVLOG_DIR/.round-current.md"
668
+ CONTINUE_PROMPT='<command-name>/devlog-tracker:continue</command-name><command-message>continue</command-message><command-args></command-args>'
669
+ printf '{"prompt":"%s"}' "$CONTINUE_PROMPT" | bash "$SCRIPT_DIR/round-start.sh"
670
+ CUR_BODY="$(cat "$DEVLOG_DIR/.round-current.md" 2>/dev/null || echo '')"
671
+ assert_contains "continue command still opens a Round" "## Round 1 —" "$CUR_BODY"
672
+ if [ -f "$DEVLOG_DIR/.round-open" ]; then
673
+ echo "PASS: continue command still sets .round-open"
674
+ else
675
+ echo "FAIL: continue command should still set .round-open"
676
+ FAIL=1
677
+ fi
678
+ rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open" "$DEVLOG_DIR/.turn-start" "$DEVLOG_DIR/.round-current.md"
679
+
637
680
  # --- workspace claim on next prompt ----------------------------------------
638
681
  WS="$TMP_ROOT/ws"
639
682
  mkdir -p "$WS/.devlog"
@@ -674,6 +717,10 @@ OUT="$(printf '%s' '{"prompt":"keep going"}' | bash "$SCRIPT_DIR/round-start.sh"
674
717
  grep -q '^## Round 2' "$WS/.devlog/.round-current.md" && echo "PASS: still opened Round 2" || { echo "FAIL: no round 2"; FAIL=1; }
675
718
 
676
719
  rm -f "$WS/.devlog/.round-open" "$WS/.devlog/.round-current.md"
720
+ # This fixture is reused below by the drift-only counter assertions (1 -> 2 ->
721
+ # reset-at-3). Its ### Status must stay non-BLOCKED: if it were BLOCKED, the
722
+ # BLOCKED-round bump would also fire on the same invocation and double-count
723
+ # the shared counter, breaking those assertions with a confusing failure.
677
724
  cat > "$WS/.devlog/devlog.md" <<EOF
678
725
  ## Round 1 — 2026-09-11T00:00:00+08:00
679
726
 
@@ -708,35 +755,35 @@ MARKER="$(cat "$WS/.devlog/.workspace-mismatch")"
708
755
  # --- lessons drift counter: mechanical nudge on repeated mismatch --------
709
756
  rm -f "$WS/.devlog/.round-open" "$WS/.devlog/.round-current.md" "$WS/.devlog/.workspace-mismatch"
710
757
  touch "$WS/.devlog/.lessons-enabled"
711
- printf '%s\n' '{"mismatch_count": 0, "threshold": 3}' > "$WS/.devlog/.lessons-drift-state"
758
+ printf '%s\n' '{"count": 0, "threshold": 3}' > "$WS/.devlog/.lessons-advisory-state"
712
759
 
713
760
  OUT="$(printf '%s' '{"prompt":"keep going"}' | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
714
761
  assert_not_contains "drift count 1/3: no nudge yet" "[Lessons Mode 提示]" "$OUT"
715
- grep -q '"mismatch_count": 1' "$WS/.devlog/.lessons-drift-state" && echo "PASS: drift count -> 1" || { echo "FAIL: drift count not 1"; FAIL=1; }
762
+ grep -q '"count": 1' "$WS/.devlog/.lessons-advisory-state" && echo "PASS: drift count -> 1" || { echo "FAIL: drift count not 1"; FAIL=1; }
716
763
 
717
764
  rm -f "$WS/.devlog/.round-open" "$WS/.devlog/.round-current.md"
718
765
  OUT="$(printf '%s' '{"prompt":"keep going"}' | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
719
766
  assert_not_contains "drift count 2/3: no nudge yet" "[Lessons Mode 提示]" "$OUT"
720
- grep -q '"mismatch_count": 2' "$WS/.devlog/.lessons-drift-state" && echo "PASS: drift count -> 2" || { echo "FAIL: drift count not 2"; FAIL=1; }
767
+ grep -q '"count": 2' "$WS/.devlog/.lessons-advisory-state" && echo "PASS: drift count -> 2" || { echo "FAIL: drift count not 2"; FAIL=1; }
721
768
 
722
769
  rm -f "$WS/.devlog/.round-open" "$WS/.devlog/.round-current.md"
723
770
  OUT="$(printf '%s' '{"prompt":"keep going"}' | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
724
771
  assert_contains "drift count hits threshold: prints nudge" "[Lessons Mode 提示]" "$OUT"
725
- grep -q '"mismatch_count": 0' "$WS/.devlog/.lessons-drift-state" && echo "PASS: drift count reset after nudge" || { echo "FAIL: drift count not reset"; FAIL=1; }
772
+ grep -q '"count": 0' "$WS/.devlog/.lessons-advisory-state" && echo "PASS: drift count reset after nudge" || { echo "FAIL: drift count not reset"; FAIL=1; }
726
773
 
727
774
  # without .lessons-enabled: no counting, no nudge, state file untouched
728
775
  rm -f "$WS/.devlog/.round-open" "$WS/.devlog/.round-current.md" "$WS/.devlog/.lessons-enabled"
729
- printf '%s\n' '{"mismatch_count": 2, "threshold": 3}' > "$WS/.devlog/.lessons-drift-state"
776
+ printf '%s\n' '{"count": 2, "threshold": 3}' > "$WS/.devlog/.lessons-advisory-state"
730
777
  OUT="$(printf '%s' '{"prompt":"keep going"}' | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
731
778
  assert_not_contains "lessons off: no nudge even near threshold" "[Lessons Mode 提示]" "$OUT"
732
- grep -q '"mismatch_count": 2' "$WS/.devlog/.lessons-drift-state" && echo "PASS: drift count untouched when lessons off" || { echo "FAIL: drift count changed when lessons off"; FAIL=1; }
733
- rm -f "$WS/.devlog/.lessons-drift-state" "$WS/.devlog/.round-open" "$WS/.devlog/.round-current.md"
779
+ grep -q '"count": 2' "$WS/.devlog/.lessons-advisory-state" && echo "PASS: drift count untouched when lessons off" || { echo "FAIL: drift count changed when lessons off"; FAIL=1; }
780
+ rm -f "$WS/.devlog/.lessons-advisory-state" "$WS/.devlog/.round-open" "$WS/.devlog/.round-current.md"
734
781
 
735
782
  # --- lessons drift counter: upgrade path — .lessons-enabled exists (from
736
- # before this branch) but .lessons-drift-state was never created. round-start.sh
783
+ # before this branch) but .lessons-advisory-state was never created. round-start.sh
737
784
  # must create it with defaults and still act on this same invocation's
738
785
  # mismatch, not require a second message ------------------------------------
739
- rm -f "$WS/.devlog/.round-open" "$WS/.devlog/.round-current.md" "$WS/.devlog/.workspace-mismatch" "$WS/.devlog/.lessons-drift-state"
786
+ rm -f "$WS/.devlog/.round-open" "$WS/.devlog/.round-current.md" "$WS/.devlog/.workspace-mismatch" "$WS/.devlog/.lessons-advisory-state"
740
787
  touch "$WS/.devlog/.lessons-enabled"
741
788
  cat > "$WS/.devlog/devlog.md" <<EOF
742
789
  ## Round 1 — 2026-09-11T00:00:00+08:00
@@ -761,11 +808,11 @@ do x
761
808
  IN_PROGRESS
762
809
  EOF
763
810
  OUT="$(printf '%s' '{"prompt":"keep going"}' | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
764
- [ -f "$WS/.devlog/.lessons-drift-state" ] && echo "PASS: upgrade path creates .lessons-drift-state on first drift" || { echo "FAIL: .lessons-drift-state not created for pre-existing lessons-enabled"; FAIL=1; }
765
- grep -q '"mismatch_count": 1' "$WS/.devlog/.lessons-drift-state" 2>/dev/null && echo "PASS: newly-created state already counted this invocation's mismatch (1)" || { echo "FAIL: mismatch_count not 1 in newly-created state"; FAIL=1; }
766
- grep -q '"threshold": 3' "$WS/.devlog/.lessons-drift-state" 2>/dev/null && echo "PASS: newly-created state has default threshold 3" || { echo "FAIL: threshold not 3 in newly-created state"; FAIL=1; }
811
+ [ -f "$WS/.devlog/.lessons-advisory-state" ] && echo "PASS: upgrade path creates .lessons-advisory-state on first drift" || { echo "FAIL: .lessons-advisory-state not created for pre-existing lessons-enabled"; FAIL=1; }
812
+ grep -q '"count": 1' "$WS/.devlog/.lessons-advisory-state" 2>/dev/null && echo "PASS: newly-created state already counted this invocation's mismatch (1)" || { echo "FAIL: count not 1 in newly-created state"; FAIL=1; }
813
+ grep -q '"threshold": 3' "$WS/.devlog/.lessons-advisory-state" 2>/dev/null && echo "PASS: newly-created state has default threshold 3" || { echo "FAIL: threshold not 3 in newly-created state"; FAIL=1; }
767
814
  assert_not_contains "upgrade path: no nudge yet on first-ever drift (1 < 3)" "[Lessons Mode 提示]" "$OUT"
768
- rm -f "$WS/.devlog/.round-open" "$WS/.devlog/.round-current.md" "$WS/.devlog/.lessons-enabled" "$WS/.devlog/.lessons-drift-state"
815
+ rm -f "$WS/.devlog/.round-open" "$WS/.devlog/.round-current.md" "$WS/.devlog/.lessons-enabled" "$WS/.devlog/.lessons-advisory-state"
769
816
 
770
817
  cat > "$WS/.devlog/devlog.md" <<EOF
771
818
  ## Round 1 — 2026-09-11T00:00:00+08:00
@@ -816,6 +863,193 @@ printf '%s' '{"prompt":"tick"}' | bash "$SCRIPT_DIR/round-start.sh" >/dev/null
816
863
  [ ! -f "$WS/.devlog/.workspace-mismatch" ] && echo "PASS: span skip writes no marker" || { echo "FAIL: span marker"; FAIL=1; }
817
864
  rm -f "$WS/.devlog/.span-open"
818
865
 
866
+ # --- BLOCKED-round accumulation shares the advisory counter -----------------
867
+ BLK_DIR="$(mktemp -d)"
868
+ export CLAUDE_PROJECT_DIR="$BLK_DIR"
869
+ mkdir -p "$BLK_DIR/.devlog"
870
+ touch "$BLK_DIR/.devlog/.enabled" "$BLK_DIR/.devlog/.lessons-enabled"
871
+ printf '%s\n' '{"count": 0, "threshold": 3}' > "$BLK_DIR/.devlog/.lessons-advisory-state"
872
+ cat > "$BLK_DIR/.devlog/devlog.md" <<'EOF'
873
+ ## Round 1 — 2026-09-20T00:00:00+08:00
874
+
875
+ ### Summary
876
+ s
877
+
878
+ ### Reply
879
+ fixture reply.
880
+
881
+ ### Handoff
882
+ #### 現況
883
+ waiting
884
+ #### 完成條件
885
+ 缺的外部輸入已出現,且可觀察條件達成。
886
+ #### 下一步
887
+ apply the answer
888
+
889
+ ### Status
890
+ BLOCKED
891
+ EOF
892
+ OUT="$(printf '%s' '{"prompt":"next"}' | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
893
+ assert_not_contains "BLOCKED round 1/3: no advisory yet" "[Lessons Mode 提示]" "$OUT"
894
+ grep -q '"count": 1' "$BLK_DIR/.devlog/.lessons-advisory-state" && echo "PASS: BLOCKED round bumped counter to 1" || { echo "FAIL: counter not bumped to 1"; FAIL=1; }
895
+
896
+ rm -f "$BLK_DIR/.devlog/.round-open" "$BLK_DIR/.devlog/.round-current.md"
897
+ cat >> "$BLK_DIR/.devlog/devlog.md" <<'EOF'
898
+
899
+ ## Round 2 — 2026-09-20T00:05:00+08:00
900
+
901
+ ### Summary
902
+ s
903
+
904
+ ### Reply
905
+ fixture reply.
906
+
907
+ ### Handoff
908
+ #### 現況
909
+ waiting
910
+ #### 完成條件
911
+ 缺的外部輸入已出現,且可觀察條件達成。
912
+ #### 下一步
913
+ apply the answer
914
+
915
+ ### Status
916
+ BLOCKED
917
+ EOF
918
+ OUT="$(printf '%s' '{"prompt":"next again"}' | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
919
+ assert_not_contains "BLOCKED round 2/3: no advisory yet" "[Lessons Mode 提示]" "$OUT"
920
+ grep -q '"count": 2' "$BLK_DIR/.devlog/.lessons-advisory-state" && echo "PASS: BLOCKED round bumped counter to 2" || { echo "FAIL: counter not bumped to 2"; FAIL=1; }
921
+
922
+ rm -f "$BLK_DIR/.devlog/.round-open" "$BLK_DIR/.devlog/.round-current.md"
923
+ cat >> "$BLK_DIR/.devlog/devlog.md" <<'EOF'
924
+
925
+ ## Round 3 — 2026-09-20T00:10:00+08:00
926
+
927
+ ### Summary
928
+ s
929
+
930
+ ### Reply
931
+ fixture reply.
932
+
933
+ ### Handoff
934
+ #### 現況
935
+ waiting
936
+ #### 完成條件
937
+ 缺的外部輸入已出現,且可觀察條件達成。
938
+ #### 下一步
939
+ apply the answer
940
+
941
+ ### Status
942
+ BLOCKED
943
+ EOF
944
+ OUT="$(printf '%s' '{"prompt":"next once more"}' | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
945
+ assert_contains "BLOCKED round 3/3: hits threshold" "[Lessons Mode 提示]" "$OUT"
946
+ grep -q '"count": 0' "$BLK_DIR/.devlog/.lessons-advisory-state" && echo "PASS: counter reset after threshold" || { echo "FAIL: counter not reset"; FAIL=1; }
947
+ rm -rf "$BLK_DIR"
948
+
949
+ # --- BLOCKED->resolved: mechanical, one-shot print, not counted -------------
950
+ RES_DIR="$(mktemp -d)"
951
+ export CLAUDE_PROJECT_DIR="$RES_DIR"
952
+ mkdir -p "$RES_DIR/.devlog"
953
+ touch "$RES_DIR/.devlog/.enabled" "$RES_DIR/.devlog/.lessons-enabled"
954
+ cat > "$RES_DIR/.devlog/devlog.md" <<'EOF'
955
+ ## Round 1 — 2026-09-20T00:00:00+08:00
956
+
957
+ ### Summary
958
+ s
959
+
960
+ ### Reply
961
+ fixture reply.
962
+
963
+ ### Handoff
964
+ #### 現況
965
+ waiting
966
+ #### 完成條件
967
+ 缺的外部輸入已出現,且可觀察條件達成。
968
+ #### 下一步
969
+ apply the answer
970
+
971
+ ### Status
972
+ BLOCKED
973
+
974
+ ## Round 2 — 2026-09-20T00:05:00+08:00
975
+
976
+ ### Summary
977
+ resolved it
978
+
979
+ ### Reply
980
+ fixture reply.
981
+
982
+ ### Handoff
983
+ #### 現況
984
+ done
985
+
986
+ ### Status
987
+ DONE
988
+ EOF
989
+ OUT="$(printf '%s' '{"prompt":"whats next"}' | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
990
+ assert_contains "BLOCKED->resolved prints a one-shot advisory" "上一輪從 BLOCKED 解開了" "$OUT"
991
+ [ ! -f "$RES_DIR/.devlog/.lessons-advisory-state" ] && echo "PASS: resolved-transition print does not touch the shared counter" || { echo "FAIL: resolved-transition print created/touched the counter file"; FAIL=1; }
992
+
993
+ # resolved print requires two rounds of history: with only one round of
994
+ # history (regardless of its own status) there's no "second-to-last" round
995
+ # to compare against.
996
+ rm -rf "$RES_DIR"
997
+ RES2_DIR="$(mktemp -d)"
998
+ export CLAUDE_PROJECT_DIR="$RES2_DIR"
999
+ mkdir -p "$RES2_DIR/.devlog"
1000
+ touch "$RES2_DIR/.devlog/.enabled" "$RES2_DIR/.devlog/.lessons-enabled"
1001
+ cat > "$RES2_DIR/.devlog/devlog.md" <<'EOF'
1002
+ ## Round 1 — 2026-09-20T00:00:00+08:00
1003
+
1004
+ ### Summary
1005
+ s
1006
+
1007
+ ### Reply
1008
+ fixture reply.
1009
+
1010
+ ### Handoff
1011
+ #### 現況
1012
+ done
1013
+
1014
+ ### Status
1015
+ DONE
1016
+ EOF
1017
+ OUT="$(printf '%s' '{"prompt":"go"}' | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
1018
+ assert_not_contains "cold start (one round of history): no resolved print" "上一輪從 BLOCKED 解開了" "$OUT"
1019
+ rm -rf "$RES2_DIR"
1020
+ export CLAUDE_PROJECT_DIR="$TMP_ROOT"
1021
+
1022
+ # --- without .lessons-enabled: BLOCKED accumulation does not run at all ----
1023
+ NOLES_DIR="$(mktemp -d)"
1024
+ export CLAUDE_PROJECT_DIR="$NOLES_DIR"
1025
+ mkdir -p "$NOLES_DIR/.devlog"
1026
+ touch "$NOLES_DIR/.devlog/.enabled"
1027
+ cat > "$NOLES_DIR/.devlog/devlog.md" <<'EOF'
1028
+ ## Round 1 — 2026-09-20T00:00:00+08:00
1029
+
1030
+ ### Summary
1031
+ s
1032
+
1033
+ ### Reply
1034
+ fixture reply.
1035
+
1036
+ ### Handoff
1037
+ #### 現況
1038
+ waiting
1039
+ #### 完成條件
1040
+ 缺的外部輸入已出現,且可觀察條件達成。
1041
+ #### 下一步
1042
+ apply the answer
1043
+
1044
+ ### Status
1045
+ BLOCKED
1046
+ EOF
1047
+ OUT="$(printf '%s' '{"prompt":"next"}' | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
1048
+ assert_not_contains "lessons off: no BLOCKED-accumulation advisory" "[Lessons Mode 提示]" "$OUT"
1049
+ [ ! -f "$NOLES_DIR/.devlog/.lessons-advisory-state" ] && echo "PASS: lessons off leaves no advisory-state file" || { echo "FAIL: advisory-state file created while lessons off"; FAIL=1; }
1050
+ rm -rf "$NOLES_DIR"
1051
+ export CLAUDE_PROJECT_DIR="$TMP_ROOT"
1052
+
819
1053
  if [ "$FAIL" -eq 0 ]; then
820
1054
  echo "All checks passed."
821
1055
  exit 0
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env bash
2
+ # Self-check for search-devlog.sh (/devlog-tracker:search), a pure-read
3
+ # lightweight grep across every devlog*.md file under .devlog/ -- devlog.md,
4
+ # devlog.archive.md, kept devlog.<name>.md, devlog.lessons.<topic>.md, and
5
+ # branch-scoped files all match the same glob, so there is no per-type logic
6
+ # to keep in sync.
7
+ set -uo pipefail
8
+
9
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
10
+ TMP_ROOT="$(mktemp -d)"
11
+ trap 'rm -rf "$TMP_ROOT"' EXIT
12
+
13
+ export CLAUDE_PROJECT_DIR="$TMP_ROOT"
14
+ DEVLOG_DIR="$TMP_ROOT/.devlog"
15
+
16
+ FAIL=0
17
+ assert_exit() {
18
+ local desc="$1" expected="$2" actual="$3"
19
+ if [ "$actual" -eq "$expected" ]; then
20
+ echo "PASS: $desc"
21
+ else
22
+ echo "FAIL: $desc (expected exit $expected, got $actual)"
23
+ FAIL=1
24
+ fi
25
+ }
26
+ assert_contains() {
27
+ local desc="$1" needle="$2" haystack="$3"
28
+ case "$haystack" in
29
+ *"$needle"*) echo "PASS: $desc" ;;
30
+ *) echo "FAIL: $desc (expected to contain '$needle', got: $haystack)"; FAIL=1 ;;
31
+ esac
32
+ }
33
+ assert_not_contains() {
34
+ local desc="$1" needle="$2" haystack="$3"
35
+ case "$haystack" in
36
+ *"$needle"*) echo "FAIL: $desc (expected NOT to contain '$needle')"; FAIL=1 ;;
37
+ *) echo "PASS: $desc" ;;
38
+ esac
39
+ }
40
+
41
+ # --- No .devlog/ at all -> NO_INDEX ----------------------------------------
42
+ OUT="$(bash "$SCRIPT_DIR/search-devlog.sh" "foo" 2>&1)"
43
+ RC=$?
44
+ assert_exit "no .devlog/ -> exit 0" 0 "$RC"
45
+ [ "$OUT" = "NO_INDEX" ] && echo "PASS: no .devlog/ -> NO_INDEX" || { echo "FAIL: expected NO_INDEX, got: $OUT"; FAIL=1; }
46
+
47
+ mkdir -p "$DEVLOG_DIR"
48
+
49
+ # --- Missing keyword argument -> error, no files touched -------------------
50
+ bash "$SCRIPT_DIR/search-devlog.sh" >/dev/null 2>&1
51
+ assert_exit "missing keyword -> exit 1" 1 $?
52
+
53
+ # --- .devlog/ exists but has no devlog*.md files yet -> NO_INDEX -----------
54
+ OUT="$(bash "$SCRIPT_DIR/search-devlog.sh" "foo" 2>&1)"
55
+ [ "$OUT" = "NO_INDEX" ] && echo "PASS: empty .devlog/ -> NO_INDEX" || { echo "FAIL: expected NO_INDEX, got: $OUT"; FAIL=1; }
56
+
57
+ # --- Matches across main, archive, kept and lessons files -------------------
58
+ cat > "$DEVLOG_DIR/devlog.md" <<'EOF'
59
+ ## Round 1 — 2026-09-08T00:00:00+08:00
60
+
61
+ ### Summary
62
+ 討論 span mode 的門檻設計。
63
+
64
+ ### Status
65
+ DONE
66
+ EOF
67
+
68
+ cat > "$DEVLOG_DIR/devlog.archive.md" <<'EOF'
69
+ ## Round 0 — 2026-09-01T00:00:00+08:00
70
+
71
+ ### Summary
72
+ 舊的 span mode 討論,已封存。
73
+
74
+ ### Status
75
+ DONE
76
+ EOF
77
+
78
+ cat > "$DEVLOG_DIR/devlog.keep-plan.md" <<'EOF'
79
+ ## Round 5 — 2026-09-05T00:00:00+08:00
80
+
81
+ ### Summary
82
+ 跟 keep 完全無關的一輪,內容是別的主題。
83
+
84
+ ### Status
85
+ DONE
86
+ EOF
87
+
88
+ cat > "$DEVLOG_DIR/devlog.lessons.span-mode.md" <<'EOF'
89
+ ## 2026-09-10T00:00:00+08:00
90
+
91
+ Span Mode 的門檻一開始設太低,導致誤判。
92
+ EOF
93
+
94
+ OUT="$(bash "$SCRIPT_DIR/search-devlog.sh" "span mode" 2>&1)"
95
+ RC=$?
96
+ assert_exit "search finds hits -> exit 0" 0 "$RC"
97
+ assert_contains "hit in devlog.md" "FILE=.devlog/devlog.md" "$OUT"
98
+ assert_contains "hit in devlog.archive.md" "FILE=.devlog/devlog.archive.md" "$OUT"
99
+ assert_contains "hit in devlog.lessons.span-mode.md" "FILE=.devlog/devlog.lessons.span-mode.md" "$OUT"
100
+ assert_not_contains "no hit in unrelated kept file" "FILE=.devlog/devlog.keep-plan.md" "$OUT"
101
+ assert_contains "match line reports nearest (###) heading" 'HEADING="### Summary"' "$OUT"
102
+ assert_contains "match line reports the matched text" "討論 span mode 的門檻設計" "$OUT"
103
+
104
+ # --- Heading context falls back to the nearest ## when no ### is closer ----
105
+ cat > "$DEVLOG_DIR/devlog.no-sub.md" <<'EOF'
106
+ ## Round 9 — 2026-09-12T00:00:00+08:00
107
+ span mode 直接寫在 Round 標題底下,沒有任何 ### 子標題。
108
+ EOF
109
+ OUT_NOSUB="$(bash "$SCRIPT_DIR/search-devlog.sh" "span mode" 2>&1)"
110
+ assert_contains "falls back to nearest ## heading" 'HEADING="## Round 9' "$OUT_NOSUB"
111
+
112
+ # --- Case-insensitive match --------------------------------------------------
113
+ OUT_UPPER="$(bash "$SCRIPT_DIR/search-devlog.sh" "SPAN MODE" 2>&1)"
114
+ assert_contains "case-insensitive match" "FILE=.devlog/devlog.md" "$OUT_UPPER"
115
+
116
+ # --- No hits anywhere -> NO_MATCH -------------------------------------------
117
+ OUT_NONE="$(bash "$SCRIPT_DIR/search-devlog.sh" "找不到的關鍵字xyz" 2>&1)"
118
+ [ "$OUT_NONE" = "NO_MATCH" ] && echo "PASS: no hits -> NO_MATCH" || { echo "FAIL: expected NO_MATCH, got: $OUT_NONE"; FAIL=1; }
119
+
120
+ if [ "$FAIL" -eq 0 ]; then
121
+ echo "All checks passed."
122
+ exit 0
123
+ else
124
+ echo "Some checks FAILED."
125
+ exit 1
126
+ fi
@@ -2,7 +2,7 @@
2
2
  # Self-check for session-start-devlog.sh, including the Span Mode
3
3
  # resume-context note. No framework — plain assert-and-exit, matching this
4
4
  # repo's existing style. Run directly:
5
- # bash hooks/scripts/test-session-start-devlog.sh
5
+ # bash core/scripts/tests/test-session-start-devlog.sh
6
6
  set -uo pipefail
7
7
 
8
8
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
@@ -181,7 +181,7 @@ echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
181
181
  assert_exit "after compact skip-heal, Stop still enforces (hash equal) -> exit 2" 2 $?
182
182
 
183
183
  # --- Scenario 7: hooks.json SessionStart matcher includes fork -------------
184
- HOOKS_JSON="$SCRIPT_DIR/../hooks.json"
184
+ HOOKS_JSON="$SCRIPT_DIR/../../claude/hooks.json"
185
185
  if grep -q '"matcher": "startup|resume|clear|compact|fork"' "$HOOKS_JSON"; then
186
186
  echo "PASS: SessionStart matcher includes fork"
187
187
  else
@@ -499,6 +499,65 @@ else
499
499
  FAIL=1
500
500
  fi
501
501
 
502
+ # --- handoff.md present: printed before excerpt header --------------------
503
+ rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/handoff.md" "$DEVLOG_DIR/.span-open"
504
+ cat > "$DEVLOG_DIR/devlog.md" <<'EOF'
505
+ ## Round 1 — 2026-09-22T00:00:00+08:00
506
+
507
+ ### Summary
508
+ summary 1
509
+
510
+ ### Handoff
511
+ #### 現況
512
+ handoff 1
513
+
514
+ ### Status
515
+ DONE
516
+ EOF
517
+ cat > "$DEVLOG_DIR/handoff.md" <<'EOF'
518
+ ## Session Handoff
519
+
520
+ ### 決策
521
+ - keep route A
522
+
523
+ ### 待解問題
524
+ - open Q
525
+
526
+ ### 失敗嘗試
527
+ - (無)
528
+ EOF
529
+ OUTPUT="$(echo '{"source":"startup"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
530
+ assert_contains "handoff header present" "Session Handoff" "$OUTPUT"
531
+ assert_contains "handoff body present" "open Q" "$OUTPUT"
532
+ assert_contains "excerpt still present" "接手摘要" "$OUTPUT"
533
+ HOFF_POS="$(printf '%s\n' "$OUTPUT" | grep -n 'open Q' | head -1 | cut -d: -f1)"
534
+ EX_POS="$(printf '%s\n' "$OUTPUT" | grep -n '接手摘要' | head -1 | cut -d: -f1)"
535
+ if [ -n "$HOFF_POS" ] && [ -n "$EX_POS" ] && [ "$HOFF_POS" -lt "$EX_POS" ]; then
536
+ echo "PASS: handoff prints before devlog excerpt"
537
+ else
538
+ echo "FAIL: handoff should precede excerpt (hoff=$HOFF_POS ex=$EX_POS)"
539
+ FAIL=1
540
+ fi
541
+
542
+ # --- no handoff.md: excerpt only -----------------------------------------
543
+ rm -f "$DEVLOG_DIR/handoff.md"
544
+ OUTPUT="$(echo '{"source":"startup"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
545
+ assert_not_contains "no handoff file -> no open Q" "open Q" "$OUTPUT"
546
+ assert_contains "excerpt still works" "接手摘要" "$OUTPUT"
547
+
548
+ # --- clear: still silent even with handoff.md ----------------------------
549
+ cat > "$DEVLOG_DIR/handoff.md" <<'EOF'
550
+ ## Session Handoff
551
+ ### 決策
552
+ - should not inject
553
+ EOF
554
+ OUTPUT="$(echo '{"source":"clear"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
555
+ if [ -z "$OUTPUT" ]; then
556
+ echo "PASS: clear stays silent with handoff.md present"
557
+ else
558
+ echo "FAIL: clear should be silent, got: $OUTPUT"; FAIL=1
559
+ fi
560
+
502
561
  if [ "$FAIL" -eq 0 ]; then
503
562
  echo "All checks passed."
504
563
  exit 0
@@ -30,10 +30,10 @@ OUT="$(bash "$SCRIPT_DIR/status-devlog.sh")"
30
30
  case "$OUT" in *"LESSONS=yes"*) pass "lessons on when flag present" ;; *) fail "lessons on when flag present [$OUT]" ;; esac
31
31
  rm -f "$TMP/.devlog/.lessons-enabled"
32
32
 
33
- printf '%s\n' '{"mismatch_count": 1, "threshold": 3}' > "$TMP/.devlog/.lessons-drift-state"
33
+ printf '%s\n' '{"count": 1, "threshold": 3}' > "$TMP/.devlog/.lessons-advisory-state"
34
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"
35
+ case "$OUT" in *"LESSONS_ADVISORY=1/3"*) pass "lessons advisory status line" ;; *) fail "lessons advisory status line [$OUT]" ;; esac
36
+ rm -f "$TMP/.devlog/.lessons-advisory-state"
37
37
 
38
38
  OUT="$(bash "$SCRIPT_DIR/span-open.sh")"
39
39
  [ "$OUT" = "OPENED=1" ] && pass "span opened" || fail "span open [$OUT]"