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,75 @@
1
+ #!/usr/bin/env bash
2
+ # Self-check for lessons-on.sh / lessons-off.sh (docs/design/lessons-mode.md
3
+ # "Enable / disable"). No framework, plain assert-and-exit, matching this
4
+ # repo's existing style.
5
+ set -uo pipefail
6
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
7
+ TMP_ROOT="$(mktemp -d)"
8
+ trap 'rm -rf "$TMP_ROOT"' EXIT
9
+ export CLAUDE_PROJECT_DIR="$TMP_ROOT"
10
+ FAIL=0
11
+ assert_exit() {
12
+ local desc="$1" expected="$2" actual="$3"
13
+ if [ "$actual" -eq "$expected" ]; then echo "PASS: $desc"
14
+ else echo "FAIL: $desc (expected $expected got $actual)"; FAIL=1; fi
15
+ }
16
+ assert_file() {
17
+ local desc="$1" path="$2"
18
+ if [ -f "$path" ]; then echo "PASS: $desc"
19
+ else echo "FAIL: $desc"; FAIL=1; fi
20
+ }
21
+ assert_not_file() {
22
+ local desc="$1" path="$2"
23
+ if [ ! -f "$path" ]; then echo "PASS: $desc"
24
+ else echo "FAIL: $desc"; FAIL=1; fi
25
+ }
26
+
27
+ mkdir -p "$TMP_ROOT/.devlog"
28
+
29
+ # --- lessons-on without the main .enabled switch -> refuses ---------------
30
+ OUT="$(bash "$SCRIPT_DIR/lessons-on.sh")"
31
+ assert_exit "lessons-on without main switch -> exit 0 (advisory refusal, not a hard error)" 0 $?
32
+ case "$OUT" in
33
+ *"NOT_ENABLED"*) echo "PASS: reports NOT_ENABLED when main switch is off" ;;
34
+ *) echo "FAIL: expected NOT_ENABLED, got $OUT"; FAIL=1 ;;
35
+ esac
36
+ assert_not_file "no .lessons-enabled created without main switch" "$TMP_ROOT/.devlog/.lessons-enabled"
37
+
38
+ # --- lessons-on with the main switch present -> enables --------------------
39
+ touch "$TMP_ROOT/.devlog/.enabled"
40
+ OUT="$(bash "$SCRIPT_DIR/lessons-on.sh")"
41
+ assert_exit "lessons-on with main switch -> 0" 0 $?
42
+ assert_file ".lessons-enabled created" "$TMP_ROOT/.devlog/.lessons-enabled"
43
+ case "$OUT" in
44
+ *"LESSONS_ENABLED="*) echo "PASS: reports LESSONS_ENABLED" ;;
45
+ *) echo "FAIL: expected LESSONS_ENABLED=..., got $OUT"; FAIL=1 ;;
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; }
52
+
53
+ # --- lessons-off disables, leaves main switch and any lessons files alone --
54
+ echo 'keep me' > "$TMP_ROOT/.devlog/devlog.lessons.foo.md"
55
+ OUT="$(bash "$SCRIPT_DIR/lessons-off.sh")"
56
+ assert_exit "lessons-off -> 0" 0 $?
57
+ case "$OUT" in
58
+ *"LESSONS_DISABLED"*) echo "PASS: reports LESSONS_DISABLED" ;;
59
+ *) echo "FAIL: expected LESSONS_DISABLED, got $OUT"; FAIL=1 ;;
60
+ esac
61
+ assert_not_file ".lessons-enabled removed" "$TMP_ROOT/.devlog/.lessons-enabled"
62
+ assert_file "main .enabled untouched" "$TMP_ROOT/.devlog/.enabled"
63
+ 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"
65
+
66
+ # --- lessons-off when already off -> reports NOT_ENABLED, no error --------
67
+ OUT="$(bash "$SCRIPT_DIR/lessons-off.sh")"
68
+ assert_exit "lessons-off when already off -> 0" 0 $?
69
+ case "$OUT" in
70
+ *"NOT_ENABLED"*) echo "PASS: lessons-off reports NOT_ENABLED when already off" ;;
71
+ *) echo "FAIL: expected NOT_ENABLED, got $OUT"; FAIL=1 ;;
72
+ esac
73
+
74
+ if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
75
+ else echo "Some checks FAILED."; exit 1; fi
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bash
2
+ # Self-check for lessons-read.sh (docs/design/lessons-mode.md "Reading
3
+ # lessons on demand"). No framework, plain assert-and-exit.
4
+ set -uo pipefail
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
+ TMP_ROOT="$(mktemp -d)"
7
+ trap 'rm -rf "$TMP_ROOT"' EXIT
8
+ export CLAUDE_PROJECT_DIR="$TMP_ROOT"
9
+ DEVLOG_DIR="$TMP_ROOT/.devlog"
10
+ mkdir -p "$DEVLOG_DIR"
11
+ FAIL=0
12
+ assert_exit() {
13
+ local desc="$1" expected="$2" actual="$3"
14
+ if [ "$actual" -eq "$expected" ]; then echo "PASS: $desc"
15
+ else echo "FAIL: $desc (expected $expected got $actual)"; FAIL=1; fi
16
+ }
17
+
18
+ # --- No devlog.md at all, no topic -> NO_INDEX ------------------------------
19
+ OUT="$(bash "$SCRIPT_DIR/lessons-read.sh" 2>&1)"
20
+ assert_exit "no devlog.md, no topic -> 0" 0 $?
21
+ case "$OUT" in
22
+ *"NO_INDEX"*) echo "PASS: reports NO_INDEX when devlog.md is absent" ;;
23
+ *) echo "FAIL: expected NO_INDEX, got $OUT"; FAIL=1 ;;
24
+ esac
25
+
26
+ # --- devlog.md exists but no Lessons 索引 -> NO_INDEX -----------------------
27
+ echo '## Round 1 — 2026-09-12T00:00:00+08:00
28
+
29
+ ### Summary
30
+ fixture
31
+
32
+ ### Reply
33
+ fixture reply.
34
+
35
+ ### Handoff
36
+ #### 現況
37
+ fixture
38
+
39
+ ### Status
40
+ DONE' > "$DEVLOG_DIR/devlog.md"
41
+ OUT="$(bash "$SCRIPT_DIR/lessons-read.sh" 2>&1)"
42
+ assert_exit "devlog.md with no index, no topic -> 0" 0 $?
43
+ case "$OUT" in
44
+ *"NO_INDEX"*) echo "PASS: reports NO_INDEX when no Lessons 索引 block exists" ;;
45
+ *) echo "FAIL: expected NO_INDEX, got $OUT"; FAIL=1 ;;
46
+ esac
47
+
48
+ # --- Index present -> printed verbatim, no topic ----------------------------
49
+ touch "$DEVLOG_DIR/.enabled" "$DEVLOG_DIR/.lessons-enabled"
50
+ bash "$SCRIPT_DIR/lessons-append.sh" --topic "foo-bar" --text "教訓內容一。" >/dev/null
51
+ OUT="$(bash "$SCRIPT_DIR/lessons-read.sh" 2>&1)"
52
+ assert_exit "index present, no topic -> 0" 0 $?
53
+ case "$OUT" in
54
+ *"devlog.lessons.foo-bar.md"*) echo "PASS: index line for foo-bar printed" ;;
55
+ *) echo "FAIL: expected index line, got $OUT"; FAIL=1 ;;
56
+ esac
57
+
58
+ # --- Read a specific existing topic, full content ---------------------------
59
+ OUT="$(bash "$SCRIPT_DIR/lessons-read.sh" "foo-bar" 2>&1)"
60
+ assert_exit "read existing topic -> 0" 0 $?
61
+ case "$OUT" in
62
+ *"PATH=.devlog/devlog.lessons.foo-bar.md"*) echo "PASS: reports PATH" ;;
63
+ *) echo "FAIL: expected PATH=..., got $OUT"; FAIL=1 ;;
64
+ esac
65
+ case "$OUT" in
66
+ *"教訓內容一。"*) echo "PASS: full entry text returned" ;;
67
+ *) echo "FAIL: expected entry text, got $OUT"; FAIL=1 ;;
68
+ esac
69
+
70
+ # --- devlog.lessons.<x>.md / .md input normalizes the same as --topic -------
71
+ OUT="$(bash "$SCRIPT_DIR/lessons-read.sh" "devlog.lessons.foo-bar.md" 2>&1)"
72
+ assert_exit "read with devlog.lessons.<x>.md input -> 0" 0 $?
73
+ case "$OUT" in
74
+ *"PATH=.devlog/devlog.lessons.foo-bar.md"*) echo "PASS: normalized input reads the same file" ;;
75
+ *) echo "FAIL: expected same PATH, got $OUT"; FAIL=1 ;;
76
+ esac
77
+
78
+ # --- Missing topic -> MISSING + candidates ----------------------------------
79
+ bash "$SCRIPT_DIR/lessons-append.sh" --topic "another-topic" --text "教訓內容二。" >/dev/null
80
+ OUT="$(bash "$SCRIPT_DIR/lessons-read.sh" "does-not-exist" 2>&1)"
81
+ assert_exit "missing topic -> exit 1" 1 $?
82
+ case "$OUT" in
83
+ *"MISSING"*) echo "PASS: reports MISSING" ;;
84
+ *) echo "FAIL: expected MISSING, got $OUT"; FAIL=1 ;;
85
+ esac
86
+ case "$OUT" in
87
+ *"CANDIDATES="*"foo-bar"*"another-topic"*|*"CANDIDATES="*"another-topic"*"foo-bar"*)
88
+ echo "PASS: candidates list both existing topics" ;;
89
+ *) echo "FAIL: expected candidates listing both topics, got $OUT"; FAIL=1 ;;
90
+ esac
91
+
92
+ # --- Path traversal in topic -> rejected -------------------------------------
93
+ bash "$SCRIPT_DIR/lessons-read.sh" "../etc" >/dev/null 2>&1
94
+ assert_exit "path-traversal topic -> exit 1" 1 $?
95
+
96
+ if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
97
+ else echo "Some checks FAILED."; exit 1; fi
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env bash
2
+ # Self-check for on-stop-failure.sh / on-session-end.sh / on-tool-failure.sh.
3
+ # Run: bash hooks/scripts/test-on-interrupt.sh
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
+ FAIL=0
16
+ assert_exit() {
17
+ local desc="$1" expected="$2" actual="$3"
18
+ if [ "$actual" -eq "$expected" ]; then
19
+ echo "PASS: $desc"
20
+ else
21
+ echo "FAIL: $desc (expected exit $expected, got $actual)"
22
+ FAIL=1
23
+ fi
24
+ }
25
+ assert_contains() {
26
+ local desc="$1" needle="$2" haystack="$3"
27
+ case "$haystack" in
28
+ *"$needle"*) echo "PASS: $desc" ;;
29
+ *) echo "FAIL: $desc (missing: $needle)"; FAIL=1 ;;
30
+ esac
31
+ }
32
+
33
+ write_open() {
34
+ rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-current.md"
35
+ cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
36
+ ## Round 1 — 2026-09-09T12:00:00+08:00
37
+
38
+ ### Status
39
+ IN_PROGRESS
40
+ EOF
41
+ printf '%s\n' '{"round": 1, "opened_at": "2026-09-09T12:00:00+08:00"}' > "$DEVLOG_DIR/.round-open"
42
+ }
43
+
44
+ # usage skips
45
+ for err in rate_limit billing_error account_on_hold; do
46
+ write_open
47
+ echo "{\"error\":\"$err\"}" | bash "$SCRIPT_DIR/on-stop-failure.sh"
48
+ assert_exit "StopFailure $err -> exit 0" 0 $?
49
+ BODY="$(cat "$DEVLOG_DIR/devlog.md" 2>/dev/null || echo '')"
50
+ case "$BODY" in
51
+ *INTERRUPTED*) echo "FAIL: $err must not stamp INTERRUPTED"; FAIL=1 ;;
52
+ *) echo "PASS: $err left Status IN_PROGRESS" ;;
53
+ esac
54
+ if [ -f "$DEVLOG_DIR/.round-open" ]; then
55
+ echo "PASS: $err left .round-open"
56
+ else
57
+ echo "FAIL: $err should not close .round-open"
58
+ FAIL=1
59
+ fi
60
+ done
61
+
62
+ # server_error stamps
63
+ write_open
64
+ echo '{"error":"server_error"}' | bash "$SCRIPT_DIR/on-stop-failure.sh"
65
+ assert_exit "StopFailure server_error -> exit 0" 0 $?
66
+ BODY="$(cat "$DEVLOG_DIR/devlog.md")"
67
+ assert_contains "server_error status" "INTERRUPTED" "$BODY"
68
+ assert_contains "server_error reason" "StopFailure:server_error" "$BODY"
69
+
70
+ # missing error -> unknown
71
+ write_open
72
+ echo '{}' | bash "$SCRIPT_DIR/on-stop-failure.sh"
73
+ BODY="$(cat "$DEVLOG_DIR/devlog.md")"
74
+ assert_contains "missing error -> unknown" "StopFailure:unknown" "$BODY"
75
+
76
+ # SessionEnd with marker
77
+ write_open
78
+ echo '{"reason":"clear"}' | bash "$SCRIPT_DIR/on-session-end.sh"
79
+ BODY="$(cat "$DEVLOG_DIR/devlog.md")"
80
+ assert_contains "session end reason" "SessionEnd:clear" "$BODY"
81
+
82
+ # SessionEnd without marker
83
+ write_open
84
+ rm -f "$DEVLOG_DIR/.round-open"
85
+ BEFORE="$(cat "$DEVLOG_DIR/devlog.md" 2>/dev/null || echo '')"
86
+ echo '{"reason":"other"}' | bash "$SCRIPT_DIR/on-session-end.sh"
87
+ AFTER="$(cat "$DEVLOG_DIR/devlog.md" 2>/dev/null || echo '')"
88
+ if [ "$BEFORE" = "$AFTER" ]; then
89
+ echo "PASS: SessionEnd without .round-open is a no-op"
90
+ else
91
+ echo "FAIL: SessionEnd edited a closed Round"
92
+ FAIL=1
93
+ fi
94
+
95
+ # PostToolUseFailure interrupt
96
+ rm -f "$DEVLOG_DIR/.interrupted"
97
+ echo '{"is_interrupt":true}' | bash "$SCRIPT_DIR/on-tool-failure.sh"
98
+ assert_exit "tool failure interrupt -> exit 0" 0 $?
99
+ if [ -f "$DEVLOG_DIR/.interrupted" ]; then
100
+ echo "PASS: is_interrupt created .interrupted"
101
+ else
102
+ echo "FAIL: .interrupted missing"
103
+ FAIL=1
104
+ fi
105
+ BODY="$(cat "$DEVLOG_DIR/devlog.md" 2>/dev/null || echo '')"
106
+ case "$BODY" in
107
+ *INTERRUPTED*) echo "FAIL: on-tool-failure must not patch devlog.md"; FAIL=1 ;;
108
+ *) echo "PASS: on-tool-failure did not patch devlog.md" ;;
109
+ esac
110
+
111
+ # is_interrupt false
112
+ rm -f "$DEVLOG_DIR/.interrupted"
113
+ echo '{"is_interrupt":false}' | bash "$SCRIPT_DIR/on-tool-failure.sh"
114
+ if [ -f "$DEVLOG_DIR/.interrupted" ]; then
115
+ echo "FAIL: is_interrupt false should not create the marker"
116
+ FAIL=1
117
+ else
118
+ echo "PASS: is_interrupt false created no marker"
119
+ fi
120
+
121
+ # no .enabled -> no marker
122
+ rm -f "$DEVLOG_DIR/.enabled" "$DEVLOG_DIR/.interrupted"
123
+ echo '{"is_interrupt":true}' | bash "$SCRIPT_DIR/on-tool-failure.sh"
124
+ if [ -f "$DEVLOG_DIR/.interrupted" ]; then
125
+ echo "FAIL: disabled project must not create .interrupted"
126
+ FAIL=1
127
+ else
128
+ echo "PASS: disabled -> no .interrupted"
129
+ fi
130
+
131
+ # SessionEnd on a completed open Round must not stamp, but is still merged
132
+ # into devlog.md (close-open-round.sh always merges; only the INTERRUPTED
133
+ # stamp is skipped when recovered).
134
+ touch "$DEVLOG_DIR/.enabled"
135
+ rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-current.md"
136
+ cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
137
+ ## Round 1 — 2026-09-09T12:00:00+08:00
138
+
139
+ ### User Input
140
+ ```text
141
+ done
142
+ ```
143
+
144
+ ### Summary
145
+ finished
146
+
147
+ ### Reply
148
+ fixture reply.
149
+
150
+ ### Handoff
151
+ #### 現況
152
+ finished
153
+
154
+ ### Status
155
+ DONE
156
+ EOF
157
+ printf '%s\n' '{"round": 1, "opened_at": "2026-09-09T12:00:00+08:00"}' > "$DEVLOG_DIR/.round-open"
158
+ cksum < "$DEVLOG_DIR/.round-current.md" > "$DEVLOG_DIR/.turn-start"
159
+ printf '\n' >> "$DEVLOG_DIR/.round-current.md"
160
+ echo '{"reason":"clear"}' | bash "$SCRIPT_DIR/on-session-end.sh"
161
+ BODY="$(cat "$DEVLOG_DIR/devlog.md")"
162
+ case "$BODY" in
163
+ *INTERRUPTED*) echo "FAIL: SessionEnd must not stamp a completed Round"; FAIL=1 ;;
164
+ *) echo "PASS: SessionEnd left completed Status DONE" ;;
165
+ esac
166
+ assert_contains "recovered-complete merged into devlog.md" $'### Status\nDONE' "$BODY"
167
+ if [ -f "$DEVLOG_DIR/.round-open" ]; then
168
+ echo "FAIL: SessionEnd recovered-complete should delete .round-open"
169
+ FAIL=1
170
+ else
171
+ echo "PASS: SessionEnd recovered-complete deleted .round-open"
172
+ fi
173
+ if [ -f "$DEVLOG_DIR/.round-current.md" ]; then
174
+ echo "FAIL: SessionEnd recovered-complete should delete .round-current.md"
175
+ FAIL=1
176
+ else
177
+ echo "PASS: SessionEnd recovered-complete deleted .round-current.md"
178
+ fi
179
+
180
+ if [ "$FAIL" -eq 0 ]; then
181
+ echo "All checks passed."
182
+ exit 0
183
+ else
184
+ echo "Some checks FAILED."
185
+ exit 1
186
+ fi
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
4
+ . "$SCRIPT_DIR/redact-prompt.sh"
5
+ FAIL=0
6
+ check() {
7
+ local d="$1" in="$2" needle="$3" forbidden="$4"
8
+ out="$(printf '%s' "$in" | redact_prompt)"
9
+ case "$out" in *"$needle"*) echo "PASS: $d masked" ;; *) echo "FAIL: $d no mask [$out]"; FAIL=1 ;; esac
10
+ if [ -n "$forbidden" ]; then
11
+ case "$out" in *"$forbidden"*) echo "FAIL: $d leaked"; FAIL=1 ;; *) echo "PASS: $d no leak" ;; esac
12
+ fi
13
+ }
14
+ check ant "token sk-ant-api03-ABCDEFG123456 end" "(已遮罩)" "sk-ant-api03-ABCDEFG123456"
15
+ check ghp "ghp_abcdefghijklmnopqrstuvwxyz0123456789" "(已遮罩)" "ghp_abcdefghijklmnopqrstuvwxyz0123456789"
16
+ check akia "AKIAIOSFODNN7EXAMPLE" "(已遮罩)" "AKIAIOSFODNN7EXAMPLE"
17
+ check openai "key sk-abcdefghijklmnopqrstuvwxyz123456 end" "(已遮罩)" "sk-abcdefghijklmnopqrstuvwxyz123456"
18
+ check bearer "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.aaaaabbbbbcccccdddddeeeee.fffffggggghhhhhiiiiijjjjj" "(已遮罩)" "Bearer eyJ"
19
+ check jwt "token eyJhbGciOiJIUzI1NiJ9.aaaaabbbbbcccccdddddeeeee.fffffggggghhhhhiiiiijjjjj end" "(已遮罩)" "eyJhbGciOiJIUzI1NiJ9"
20
+ keep="$(printf '%s' 'see https://example.com/user/alpha' | redact_prompt)"
21
+ [ "$keep" = "see https://example.com/user/alpha" ] && echo "PASS: url kept" || { echo "FAIL: url $keep"; FAIL=1; }
22
+ keep_uuid="$(printf '%s' 'id 550e8400-e29b-41d4-a716-446655440000' | redact_prompt)"
23
+ [ "$keep_uuid" = "id 550e8400-e29b-41d4-a716-446655440000" ] && echo "PASS: uuid kept" || { echo "FAIL: uuid $keep_uuid"; FAIL=1; }
24
+ pem="$(printf '%s\n' '-----BEGIN PRIVATE KEY-----' 'MIIB' '-----END PRIVATE KEY-----' | redact_prompt)"
25
+ case "$pem" in *PRIVATE\ KEY*) echo "FAIL: pem leaked"; FAIL=1 ;; *"(已遮罩)"*) echo "PASS: pem masked" ;; *) echo "FAIL: pem [$pem]"; FAIL=1 ;; esac
26
+ if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
27
+ else echo "Some checks FAILED."; exit 1; fi
@@ -0,0 +1,35 @@
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
+ mkdir -p "$TMP/.devlog"
8
+ FAIL=0
9
+ check_exit_1() {
10
+ bash "$SCRIPT_DIR/resume-devlog.sh" "$@" >/dev/null 2>&1
11
+ [ "$?" -eq 1 ] && echo "PASS: $1 rejected" || { echo "FAIL: $1"; FAIL=1; }
12
+ }
13
+ check_exit_1 --name
14
+ check_exit_1 --name archive
15
+ check_exit_1 --name ../x
16
+ INVALID="$(bash "$SCRIPT_DIR/resume-devlog.sh" --name devlog.md 2>&1)"
17
+ [ "$?" -eq 1 ] && [ "$INVALID" = "INVALID_NAME" ] \
18
+ && echo "PASS: devlog.md rejected" || { echo "FAIL: devlog.md [$INVALID]"; FAIL=1; }
19
+ cat > "$TMP/.devlog/devlog.foo.md" <<'EOF'
20
+ # Kept log
21
+
22
+ ## Round 4 — now
23
+
24
+ ### Status
25
+ DONE
26
+ EOF
27
+ OUT="$(bash "$SCRIPT_DIR/resume-devlog.sh" --name devlog.foo.md)"
28
+ case "$OUT" in *"PATH=.devlog/devlog.foo.md"*) echo "PASS: path" ;; *) echo "FAIL: path [$OUT]"; FAIL=1 ;; esac
29
+ case "$OUT" in *"## Round 4"*) echo "PASS: content" ;; *) echo "FAIL: content"; FAIL=1 ;; esac
30
+ OUT="$(bash "$SCRIPT_DIR/resume-devlog.sh" --name missing 2>&1)"
31
+ [ "$?" -eq 1 ] || { echo "FAIL: missing exit"; FAIL=1; }
32
+ case "$OUT" in *"MISSING"*"CANDIDATES=foo"*) echo "PASS: candidates" ;; *) echo "FAIL: candidates [$OUT]"; FAIL=1 ;; esac
33
+
34
+ if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
35
+ else echo "Some checks FAILED."; exit 1; fi