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.
- package/LICENSE +201 -0
- package/README.md +234 -0
- package/bin/devlog-tracker.js +51 -0
- package/cli/init.js +55 -0
- package/cli/init.test.js +37 -0
- package/cli/merge-hooks.js +60 -0
- package/cli/merge-hooks.test.js +187 -0
- package/cli/platforms/codex.js +13 -0
- package/cli/platforms/cursor.js +13 -0
- package/cli/status.js +14 -0
- package/cli/status.test.js +33 -0
- package/cli/vendor.js +37 -0
- package/cli/vendor.test.js +84 -0
- package/codex/hooks/on-pre-tool.sh +19 -0
- package/codex/hooks/on-session-end.sh +31 -0
- package/codex/hooks/on-session-start.sh +37 -0
- package/codex/hooks/on-stop.sh +18 -0
- package/codex/hooks/on-user-prompt-submit.sh +15 -0
- package/codex/hooks/project-dir.sh +13 -0
- package/codex/hooks/test-adapters.sh +98 -0
- package/codex/hooks.json +54 -0
- package/commands/checkpoint.md +17 -0
- package/commands/clean.md +50 -0
- package/commands/compact.md +24 -0
- package/commands/continue.md +30 -0
- package/commands/keep.md +100 -0
- package/commands/lessons-drift.md +19 -0
- package/commands/lessons-off.md +16 -0
- package/commands/lessons-on.md +20 -0
- package/commands/lessons.md +20 -0
- package/commands/overview.md +32 -0
- package/commands/pause.md +20 -0
- package/commands/resume.md +18 -0
- package/commands/segment-watch.md +21 -0
- package/commands/span.md +13 -0
- package/commands/start.md +19 -0
- package/commands/status.md +14 -0
- package/cursor/hooks/on-pre-tool.sh +24 -0
- package/cursor/hooks/on-session-end.sh +11 -0
- package/cursor/hooks/on-session-start.sh +15 -0
- package/cursor/hooks/on-stop.sh +44 -0
- package/cursor/hooks/on-submit-prompt.sh +28 -0
- package/cursor/hooks/on-tool-failure.sh +16 -0
- package/cursor/hooks/project-dir.sh +10 -0
- package/cursor/hooks/test-adapters.sh +201 -0
- package/cursor/hooks.json +26 -0
- package/hooks/scripts/await-open.sh +23 -0
- package/hooks/scripts/checkpoint-set.sh +44 -0
- package/hooks/scripts/clean-devlog.sh +69 -0
- package/hooks/scripts/close-open-round.sh +186 -0
- package/hooks/scripts/compact-devlog.sh +72 -0
- package/hooks/scripts/detect-pending-question.sh +31 -0
- package/hooks/scripts/devlog-lock.sh +38 -0
- package/hooks/scripts/devlog-md.sh +222 -0
- package/hooks/scripts/devlog-path.sh +70 -0
- package/hooks/scripts/enforce-devlog.sh +629 -0
- package/hooks/scripts/files-snapshot.sh +70 -0
- package/hooks/scripts/json-field.sh +76 -0
- package/hooks/scripts/keep-move.sh +155 -0
- package/hooks/scripts/kept-list.sh +40 -0
- package/hooks/scripts/lessons-append.sh +93 -0
- package/hooks/scripts/lessons-drift-set.sh +42 -0
- package/hooks/scripts/lessons-off.sh +15 -0
- package/hooks/scripts/lessons-on.sh +21 -0
- package/hooks/scripts/lessons-read.sh +60 -0
- package/hooks/scripts/on-session-end.sh +14 -0
- package/hooks/scripts/on-stop-failure.sh +18 -0
- package/hooks/scripts/on-tool-failure.sh +25 -0
- package/hooks/scripts/pause-devlog.sh +13 -0
- package/hooks/scripts/redact-prompt.sh +19 -0
- package/hooks/scripts/resume-devlog.sh +41 -0
- package/hooks/scripts/round-start.sh +333 -0
- package/hooks/scripts/run-tests.sh +26 -0
- package/hooks/scripts/segment-watch-set.sh +47 -0
- package/hooks/scripts/segment-watch.sh +197 -0
- package/hooks/scripts/session-start-devlog.sh +157 -0
- package/hooks/scripts/span-close.sh +9 -0
- package/hooks/scripts/span-open.sh +21 -0
- package/hooks/scripts/start-devlog.sh +24 -0
- package/hooks/scripts/status-devlog.sh +43 -0
- package/hooks/scripts/tests/test-await-open.sh +112 -0
- package/hooks/scripts/tests/test-branch-scoped-integration.sh +108 -0
- package/hooks/scripts/tests/test-checkpoint-set.sh +36 -0
- package/hooks/scripts/tests/test-clean-devlog.sh +160 -0
- package/hooks/scripts/tests/test-cli-init-e2e.sh +37 -0
- package/hooks/scripts/tests/test-close-open-round.sh +325 -0
- package/hooks/scripts/tests/test-compact-devlog.sh +62 -0
- package/hooks/scripts/tests/test-devlog-lock.sh +37 -0
- package/hooks/scripts/tests/test-devlog-md.sh +530 -0
- package/hooks/scripts/tests/test-devlog-path.sh +118 -0
- package/hooks/scripts/tests/test-enforce-devlog-files.sh +233 -0
- package/hooks/scripts/tests/test-enforce-devlog-handoff-order.sh +223 -0
- package/hooks/scripts/tests/test-enforce-devlog-workspace.sh +333 -0
- package/hooks/scripts/tests/test-enforce-devlog.sh +1290 -0
- package/hooks/scripts/tests/test-files-snapshot.sh +142 -0
- package/hooks/scripts/tests/test-json-field.sh +57 -0
- package/hooks/scripts/tests/test-keep-move.sh +127 -0
- package/hooks/scripts/tests/test-kept-list.sh +80 -0
- package/hooks/scripts/tests/test-lessons-append.sh +138 -0
- package/hooks/scripts/tests/test-lessons-drift-set.sh +36 -0
- package/hooks/scripts/tests/test-lessons-on-off.sh +75 -0
- package/hooks/scripts/tests/test-lessons-read.sh +97 -0
- package/hooks/scripts/tests/test-on-interrupt.sh +186 -0
- package/hooks/scripts/tests/test-redact-prompt.sh +27 -0
- package/hooks/scripts/tests/test-resume-devlog.sh +35 -0
- package/hooks/scripts/tests/test-round-start.sh +825 -0
- package/hooks/scripts/tests/test-segment-watch-set.sh +75 -0
- package/hooks/scripts/tests/test-segment-watch.sh +468 -0
- package/hooks/scripts/tests/test-session-start-devlog.sh +508 -0
- package/hooks/scripts/tests/test-start-pause-devlog.sh +84 -0
- package/hooks/scripts/tests/test-status-span.sh +50 -0
- package/hooks/scripts/tests/test-workspace-snapshot.sh +130 -0
- package/hooks/scripts/workspace-snapshot.sh +69 -0
- package/package.json +37 -0
- package/skills/devlog-tracker/SKILL.md +411 -0
- package/skills/devlog-tracker/references/checkpoint-mode.md +42 -0
- package/skills/devlog-tracker/references/lessons-mode.md +32 -0
- package/skills/devlog-tracker/references/reply-fold.md +129 -0
- package/skills/devlog-tracker/references/round-segments.md +55 -0
- package/skills/devlog-tracker/references/span-mode.md +65 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
4
|
+
. "$SCRIPT_DIR/devlog-md.sh"
|
|
5
|
+
TMP="$(mktemp -d)"
|
|
6
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
7
|
+
FAIL=0
|
|
8
|
+
assert_eq() {
|
|
9
|
+
local d="$1" e="$2" a="$3"
|
|
10
|
+
if [ "$e" = "$a" ]; then echo "PASS: $d"; else echo "FAIL: $d expected [$e] got [$a]"; FAIL=1; fi
|
|
11
|
+
}
|
|
12
|
+
perm_of() {
|
|
13
|
+
stat -f '%Lp' "$1" 2>/dev/null || stat -c '%a' "$1" 2>/dev/null
|
|
14
|
+
}
|
|
15
|
+
make_round() {
|
|
16
|
+
local file="$1" n="$2" status="$3"
|
|
17
|
+
cat >> "$file" <<EOF
|
|
18
|
+
## Round $n — 2026-09-09T12:00:00+08:00
|
|
19
|
+
|
|
20
|
+
### Status
|
|
21
|
+
$status
|
|
22
|
+
EOF
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# no --confirmed -> exit 1, nothing changes, no stdout
|
|
26
|
+
NOFLAG="$TMP/noflag"
|
|
27
|
+
mkdir -p "$NOFLAG/.devlog"
|
|
28
|
+
export CLAUDE_PROJECT_DIR="$NOFLAG"
|
|
29
|
+
printf '# project\n\n' > "$NOFLAG/.devlog/devlog.md"
|
|
30
|
+
make_round "$NOFLAG/.devlog/devlog.md" 1 DONE
|
|
31
|
+
OUT="$(bash "$SCRIPT_DIR/clean-devlog.sh" 2>/dev/null)"
|
|
32
|
+
assert_eq "no --confirmed: exit 1" 1 "$?"
|
|
33
|
+
assert_eq "no --confirmed: empty stdout" "" "$OUT"
|
|
34
|
+
grep -q '^## Round 1' "$NOFLAG/.devlog/devlog.md" && echo "PASS: no --confirmed leaves devlog.md untouched" || { echo "FAIL: devlog.md changed without --confirmed"; FAIL=1; }
|
|
35
|
+
|
|
36
|
+
# missing devlog.md -> exit 1, nothing created, no stdout
|
|
37
|
+
MISSING="$TMP/missing"
|
|
38
|
+
mkdir -p "$MISSING/.devlog"
|
|
39
|
+
export CLAUDE_PROJECT_DIR="$MISSING"
|
|
40
|
+
OUT="$(bash "$SCRIPT_DIR/clean-devlog.sh" --confirmed 2>/dev/null)"
|
|
41
|
+
assert_eq "missing log exit 1" 1 "$?"
|
|
42
|
+
assert_eq "missing log: empty stdout" "" "$OUT"
|
|
43
|
+
[ ! -e "$MISSING/.devlog/devlog.md" ] || { echo "FAIL: devlog.md created for missing case"; FAIL=1; }
|
|
44
|
+
|
|
45
|
+
# .round-open names a Round that isn't in the file -> refuse, don't touch anything
|
|
46
|
+
STALEOPEN="$TMP/staleopen"
|
|
47
|
+
mkdir -p "$STALEOPEN/.devlog"
|
|
48
|
+
export CLAUDE_PROJECT_DIR="$STALEOPEN"
|
|
49
|
+
printf '# project\n\n' > "$STALEOPEN/.devlog/devlog.md"
|
|
50
|
+
make_round "$STALEOPEN/.devlog/devlog.md" 1 DONE
|
|
51
|
+
make_round "$STALEOPEN/.devlog/devlog.md" 2 DONE
|
|
52
|
+
printf '%s\n' '{"round": 7, "opened_at": "now"}' > "$STALEOPEN/.devlog/.round-open"
|
|
53
|
+
OUT="$(bash "$SCRIPT_DIR/clean-devlog.sh" --confirmed 2>/dev/null)"
|
|
54
|
+
assert_eq "stale round-open: exit 1" 1 "$?"
|
|
55
|
+
assert_eq "stale round-open: empty stdout" "" "$OUT"
|
|
56
|
+
[ -f "$STALEOPEN/.devlog/devlog.md" ] && grep -q '^## Round 1' "$STALEOPEN/.devlog/devlog.md" && echo "PASS: stale round-open leaves devlog.md untouched" || { echo "FAIL: stale round-open touched devlog.md"; FAIL=1; }
|
|
57
|
+
grep -q '"round": 7' "$STALEOPEN/.devlog/.round-open" && echo "PASS: stale round-open file untouched" || { echo "FAIL: round-open file changed"; FAIL=1; }
|
|
58
|
+
|
|
59
|
+
# .round-open has an unparseable round while a real open Round exists -> refuse, don't touch anything
|
|
60
|
+
BADOPEN="$TMP/badopen"
|
|
61
|
+
mkdir -p "$BADOPEN/.devlog"
|
|
62
|
+
export CLAUDE_PROJECT_DIR="$BADOPEN"
|
|
63
|
+
printf '# project\n\n' > "$BADOPEN/.devlog/devlog.md"
|
|
64
|
+
make_round "$BADOPEN/.devlog/devlog.md" 1 IN_PROGRESS
|
|
65
|
+
printf '%s\n' '{"round": "abc", "opened_at": "now"}' > "$BADOPEN/.devlog/.round-open"
|
|
66
|
+
OUT="$(bash "$SCRIPT_DIR/clean-devlog.sh" --confirmed 2>/dev/null)"
|
|
67
|
+
assert_eq "unparseable round-open: exit 1" 1 "$?"
|
|
68
|
+
assert_eq "unparseable round-open: empty stdout" "" "$OUT"
|
|
69
|
+
grep -q '^## Round 1' "$BADOPEN/.devlog/devlog.md" && echo "PASS: unparseable round-open leaves the real open round in place" || { echo "FAIL: unparseable round-open deleted the open round"; FAIL=1; }
|
|
70
|
+
|
|
71
|
+
# open round: everything else discarded, this round renumbered to 1, body preserved verbatim
|
|
72
|
+
# (including a fenced fake heading, to prove the rename only touches the real heading line)
|
|
73
|
+
OPEN="$TMP/open"
|
|
74
|
+
mkdir -p "$OPEN/.devlog"
|
|
75
|
+
export CLAUDE_PROJECT_DIR="$OPEN"
|
|
76
|
+
printf '# project summary\n\nsome durable notes\n\n' > "$OPEN/.devlog/devlog.md"
|
|
77
|
+
make_round "$OPEN/.devlog/devlog.md" 1 DONE
|
|
78
|
+
make_round "$OPEN/.devlog/devlog.md" 2 DONE
|
|
79
|
+
{
|
|
80
|
+
printf '## Round 3 — 2026-09-09T12:00:00+08:00\n\n'
|
|
81
|
+
printf '### User Input\n```text\nplease do not touch this fake heading:\n## Round 1 fake\n## Round 99 — fake heading\n```\n\n'
|
|
82
|
+
printf '### Status\nIN_PROGRESS\n'
|
|
83
|
+
} > "$OPEN/.devlog/.round-current.md"
|
|
84
|
+
printf '%s\n' '{"round": 3, "opened_at": "now"}' > "$OPEN/.devlog/.round-open"
|
|
85
|
+
printf '%s\n' '{"round": 2, "ticks": 1, "max_silent_ticks": 5}' > "$OPEN/.devlog/.span-open"
|
|
86
|
+
printf '%s\n' '{"rounds_since_checkpoint": 19, "max_silent_rounds": 20, "checkpoint_marker_count": 2}' > "$OPEN/.devlog/.checkpoint-state"
|
|
87
|
+
: > "$OPEN/.devlog/.interrupted"
|
|
88
|
+
: > "$OPEN/.devlog/.awaiting-reply"
|
|
89
|
+
OUT="$(bash "$SCRIPT_DIR/clean-devlog.sh" --confirmed)"
|
|
90
|
+
assert_eq "open round: exit 0" 0 "$?"
|
|
91
|
+
assert_eq "open round: stdout" "CLEANED KEPT_ROUND=1" "$OUT"
|
|
92
|
+
# Round-current split invariant: the surviving round lands in
|
|
93
|
+
# .round-current.md (renumbered to Round 1), and devlog.md is emptied/gone
|
|
94
|
+
# — never the other way around (that would leave .round-open pointing at a
|
|
95
|
+
# round Stop/close-open-round.sh can no longer find).
|
|
96
|
+
[ ! -s "$OPEN/.devlog/devlog.md" ] && echo "PASS: devlog.md emptied/absent (project summary + old rounds discarded)" || { echo "FAIL: devlog.md still has content"; FAIL=1; }
|
|
97
|
+
grep -q '^## Round 1 ' "$OPEN/.devlog/.round-current.md" && echo "PASS: open round renumbered to 1" || { echo "FAIL: renumber"; FAIL=1; }
|
|
98
|
+
grep -q '^## Round 2 ' "$OPEN/.devlog/.round-current.md" && { echo "FAIL: old round 2 survived"; FAIL=1; } || echo "PASS: old rounds gone"
|
|
99
|
+
[ "$(devlog_list_round_starts "$OPEN/.devlog/.round-current.md" | wc -l | tr -d ' ')" = "1" ] && echo "PASS: exactly one real heading remains (fence-aware count)" || { echo "FAIL: heading count"; FAIL=1; }
|
|
100
|
+
grep -q '^please do not touch this fake heading:$' "$OPEN/.devlog/.round-current.md" && echo "PASS: fenced body preserved verbatim" || { echo "FAIL: fenced body altered"; FAIL=1; }
|
|
101
|
+
grep -q '^## Round 1 fake$' "$OPEN/.devlog/.round-current.md" && echo "PASS: fenced fake heading untouched" || { echo "FAIL: fenced fake heading renamed"; FAIL=1; }
|
|
102
|
+
grep -q '^## Round 99 — fake heading$' "$OPEN/.devlog/.round-current.md" && echo "PASS: second fenced fake heading untouched" || { echo "FAIL: second fenced fake heading renamed"; FAIL=1; }
|
|
103
|
+
[ -s "$OPEN/.devlog/.round-current.md" ] && echo "PASS: .round-current.md holds the renumbered round" || { echo "FAIL: .round-current.md empty/missing"; FAIL=1; }
|
|
104
|
+
grep -q '"round": 1' "$OPEN/.devlog/.round-open" && echo "PASS: round-open reset to 1" || { echo "FAIL: round-open"; FAIL=1; }
|
|
105
|
+
[ ! -e "$OPEN/.devlog/.span-open" ] && echo "PASS: span-open removed" || { echo "FAIL: span-open remains"; FAIL=1; }
|
|
106
|
+
[ ! -e "$OPEN/.devlog/.interrupted" ] && echo "PASS: interrupted removed" || { echo "FAIL: interrupted remains"; FAIL=1; }
|
|
107
|
+
[ ! -e "$OPEN/.devlog/.awaiting-reply" ] && echo "PASS: awaiting-reply removed" || { echo "FAIL: awaiting-reply remains"; FAIL=1; }
|
|
108
|
+
grep -q '"rounds_since_checkpoint": 0' "$OPEN/.devlog/.checkpoint-state" && echo "PASS: checkpoint rounds reset" || { echo "FAIL: checkpoint rounds"; FAIL=1; }
|
|
109
|
+
grep -q '"checkpoint_marker_count": 0' "$OPEN/.devlog/.checkpoint-state" && echo "PASS: checkpoint marker count reset" || { echo "FAIL: checkpoint marker count"; FAIL=1; }
|
|
110
|
+
grep -q '"max_silent_rounds": 20' "$OPEN/.devlog/.checkpoint-state" && echo "PASS: checkpoint max kept" || { echo "FAIL: checkpoint max"; FAIL=1; }
|
|
111
|
+
PERM="$(perm_of "$OPEN/.devlog/.round-current.md")"
|
|
112
|
+
[ "$PERM" != "600" ] && echo "PASS: rewritten .round-current.md is not owner-only (mode $PERM)" || { echo "FAIL: rewritten .round-current.md is 600"; FAIL=1; }
|
|
113
|
+
|
|
114
|
+
# no open round (never started / paused): whole file deleted
|
|
115
|
+
NOOPEN="$TMP/noopen"
|
|
116
|
+
mkdir -p "$NOOPEN/.devlog"
|
|
117
|
+
export CLAUDE_PROJECT_DIR="$NOOPEN"
|
|
118
|
+
printf '# project summary\n\n' > "$NOOPEN/.devlog/devlog.md"
|
|
119
|
+
make_round "$NOOPEN/.devlog/devlog.md" 1 DONE
|
|
120
|
+
make_round "$NOOPEN/.devlog/devlog.md" 2 DONE
|
|
121
|
+
printf '%s\n' '{"rounds_since_checkpoint": 5, "max_silent_rounds": 20, "checkpoint_marker_count": 1}' > "$NOOPEN/.devlog/.checkpoint-state"
|
|
122
|
+
printf '%s\n' '# archive' > "$NOOPEN/.devlog/devlog.archive.md"
|
|
123
|
+
printf '%s\n' '# kept' > "$NOOPEN/.devlog/devlog.span-mode.md"
|
|
124
|
+
printf '%s\n' 'enabled' > "$NOOPEN/.devlog/.enabled"
|
|
125
|
+
OUT="$(bash "$SCRIPT_DIR/clean-devlog.sh" --confirmed)"
|
|
126
|
+
assert_eq "no open round: exit 0" 0 "$?"
|
|
127
|
+
assert_eq "no open round: stdout" "CLEANED KEPT_ROUND=0" "$OUT"
|
|
128
|
+
[ ! -e "$NOOPEN/.devlog/devlog.md" ] && echo "PASS: devlog.md deleted" || { echo "FAIL: devlog.md remains"; FAIL=1; }
|
|
129
|
+
grep -q '"rounds_since_checkpoint": 0' "$NOOPEN/.devlog/.checkpoint-state" && echo "PASS: checkpoint rounds reset (no-open)" || { echo "FAIL: checkpoint rounds (no-open)"; FAIL=1; }
|
|
130
|
+
grep -q '"checkpoint_marker_count": 0' "$NOOPEN/.devlog/.checkpoint-state" && echo "PASS: checkpoint marker count reset (no-open)" || { echo "FAIL: checkpoint marker count (no-open)"; FAIL=1; }
|
|
131
|
+
grep -q '^# archive' "$NOOPEN/.devlog/devlog.archive.md" && echo "PASS: archive.md untouched (no-open)" || { echo "FAIL: archive.md touched (no-open)"; FAIL=1; }
|
|
132
|
+
grep -q '^# kept' "$NOOPEN/.devlog/devlog.span-mode.md" && echo "PASS: named keep file untouched (no-open)" || { echo "FAIL: named keep file touched (no-open)"; FAIL=1; }
|
|
133
|
+
[ -f "$NOOPEN/.devlog/.enabled" ] && echo "PASS: .enabled untouched (no-open)" || { echo "FAIL: .enabled removed (no-open)"; FAIL=1; }
|
|
134
|
+
|
|
135
|
+
# does not touch archive.md or named keep files (open-round branch)
|
|
136
|
+
SIB="$TMP/siblings"
|
|
137
|
+
mkdir -p "$SIB/.devlog"
|
|
138
|
+
export CLAUDE_PROJECT_DIR="$SIB"
|
|
139
|
+
printf '%s\n' '# archive' > "$SIB/.devlog/devlog.archive.md"
|
|
140
|
+
printf '%s\n' '# kept' > "$SIB/.devlog/devlog.span-mode.md"
|
|
141
|
+
printf '# project\n\n' > "$SIB/.devlog/devlog.md"
|
|
142
|
+
make_round "$SIB/.devlog/.round-current.md" 1 DONE
|
|
143
|
+
printf '%s\n' '{"round": 1, "opened_at": "now"}' > "$SIB/.devlog/.round-open"
|
|
144
|
+
bash "$SCRIPT_DIR/clean-devlog.sh" --confirmed >/dev/null
|
|
145
|
+
grep -q '^# archive' "$SIB/.devlog/devlog.archive.md" && echo "PASS: archive.md untouched" || { echo "FAIL: archive.md touched"; FAIL=1; }
|
|
146
|
+
grep -q '^# kept' "$SIB/.devlog/devlog.span-mode.md" && echo "PASS: named keep file untouched" || { echo "FAIL: named keep file touched"; FAIL=1; }
|
|
147
|
+
|
|
148
|
+
# .enabled is never touched either way
|
|
149
|
+
ENAB="$TMP/enabled"
|
|
150
|
+
mkdir -p "$ENAB/.devlog"
|
|
151
|
+
export CLAUDE_PROJECT_DIR="$ENAB"
|
|
152
|
+
printf '%s\n' 'enabled' > "$ENAB/.devlog/.enabled"
|
|
153
|
+
printf '# project\n\n' > "$ENAB/.devlog/devlog.md"
|
|
154
|
+
make_round "$ENAB/.devlog/.round-current.md" 1 IN_PROGRESS
|
|
155
|
+
printf '%s\n' '{"round": 1, "opened_at": "now"}' > "$ENAB/.devlog/.round-open"
|
|
156
|
+
bash "$SCRIPT_DIR/clean-devlog.sh" --confirmed >/dev/null
|
|
157
|
+
[ -f "$ENAB/.devlog/.enabled" ] && echo "PASS: .enabled untouched" || { echo "FAIL: .enabled removed"; FAIL=1; }
|
|
158
|
+
|
|
159
|
+
if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
|
|
160
|
+
else echo "Some checks FAILED."; exit 1; fi
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
4
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
|
5
|
+
TMP="$(mktemp -d)"
|
|
6
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
7
|
+
FAIL=0
|
|
8
|
+
|
|
9
|
+
command -v node >/dev/null 2>&1 || { echo "SKIP: node not installed"; exit 0; }
|
|
10
|
+
|
|
11
|
+
PROJECT="$TMP/project"
|
|
12
|
+
mkdir -p "$PROJECT"
|
|
13
|
+
|
|
14
|
+
cd "$PROJECT" || exit 1
|
|
15
|
+
NODE_BIN="$(command -v node)"
|
|
16
|
+
"$NODE_BIN" "$REPO_ROOT/bin/devlog-tracker.js" init --codex --cursor >/dev/null 2>"$TMP/stderr" \
|
|
17
|
+
|| { echo "FAIL: init exited non-zero"; cat "$TMP/stderr"; FAIL=1; }
|
|
18
|
+
|
|
19
|
+
[ -f "$PROJECT/.devlog-tracker/VERSION" ] && echo "PASS: vendored VERSION exists" || { echo "FAIL: no VERSION"; FAIL=1; }
|
|
20
|
+
[ -f "$PROJECT/.devlog-tracker/env.sh" ] && echo "PASS: vendored env.sh exists" || { echo "FAIL: no env.sh"; FAIL=1; }
|
|
21
|
+
[ -f "$PROJECT/.codex/hooks.json" ] && echo "PASS: .codex/hooks.json written" || { echo "FAIL: no .codex/hooks.json"; FAIL=1; }
|
|
22
|
+
[ -f "$PROJECT/.cursor/hooks.json" ] && echo "PASS: .cursor/hooks.json written" || { echo "FAIL: no .cursor/hooks.json"; FAIL=1; }
|
|
23
|
+
|
|
24
|
+
# re-running must not duplicate entries
|
|
25
|
+
COUNT_BEFORE=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$PROJECT/.codex/hooks.json')).hooks.Stop.length)")
|
|
26
|
+
"$NODE_BIN" "$REPO_ROOT/bin/devlog-tracker.js" init --codex --cursor >/dev/null 2>"$TMP/stderr" \
|
|
27
|
+
|| { echo "FAIL: second init exited non-zero"; cat "$TMP/stderr"; FAIL=1; }
|
|
28
|
+
COUNT_AFTER=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$PROJECT/.codex/hooks.json')).hooks.Stop.length)")
|
|
29
|
+
if [ "$COUNT_BEFORE" = "$COUNT_AFTER" ]; then
|
|
30
|
+
echo "PASS: re-running init does not duplicate hook entries"
|
|
31
|
+
else
|
|
32
|
+
echo "FAIL: hook entries duplicated ($COUNT_BEFORE -> $COUNT_AFTER)"; FAIL=1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
|
|
36
|
+
else echo "Some checks FAILED."; exit 1
|
|
37
|
+
fi
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Self-check for close-open-round.sh. Run:
|
|
3
|
+
# bash hooks/scripts/test-close-open-round.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
|
+
assert_not_contains() {
|
|
33
|
+
local desc="$1" needle="$2" haystack="$3"
|
|
34
|
+
case "$haystack" in
|
|
35
|
+
*"$needle"*) echo "FAIL: $desc (unexpected: $needle)"; FAIL=1 ;;
|
|
36
|
+
*) echo "PASS: $desc" ;;
|
|
37
|
+
esac
|
|
38
|
+
}
|
|
39
|
+
assert_file_absent() {
|
|
40
|
+
local desc="$1" path="$2"
|
|
41
|
+
if [ -f "$path" ]; then
|
|
42
|
+
echo "FAIL: $desc (file still present: $path)"
|
|
43
|
+
FAIL=1
|
|
44
|
+
else
|
|
45
|
+
echo "PASS: $desc"
|
|
46
|
+
fi
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
write_skeleton() {
|
|
50
|
+
: > "$DEVLOG_DIR/devlog.md"
|
|
51
|
+
cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
|
|
52
|
+
## Round 1 — 2026-09-09T12:00:00+08:00
|
|
53
|
+
|
|
54
|
+
### User Input
|
|
55
|
+
```text
|
|
56
|
+
hello
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Status
|
|
60
|
+
IN_PROGRESS
|
|
61
|
+
EOF
|
|
62
|
+
printf '%s\n' '{"round": 1, "opened_at": "2026-09-09T12:00:00+08:00"}' > "$DEVLOG_DIR/.round-open"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# --- 1: no .enabled -> no-op, .round-current.md untouched -------------------
|
|
66
|
+
rm -f "$DEVLOG_DIR/.enabled"
|
|
67
|
+
write_skeleton
|
|
68
|
+
BEFORE="$(cat "$DEVLOG_DIR/.round-current.md")"
|
|
69
|
+
bash "$SCRIPT_DIR/close-open-round.sh" "dangling:next_prompt"
|
|
70
|
+
assert_exit "no .enabled -> exit 0" 0 $?
|
|
71
|
+
AFTER="$(cat "$DEVLOG_DIR/.round-current.md")"
|
|
72
|
+
if [ "$BEFORE" = "$AFTER" ]; then
|
|
73
|
+
echo "PASS: no .enabled -> .round-current.md unchanged"
|
|
74
|
+
else
|
|
75
|
+
echo "FAIL: no .enabled should not edit .round-current.md"
|
|
76
|
+
FAIL=1
|
|
77
|
+
fi
|
|
78
|
+
touch "$DEVLOG_DIR/.enabled"
|
|
79
|
+
|
|
80
|
+
# --- 2: matching skeleton, never touched (not recovered) -> INTERRUPTED +
|
|
81
|
+
# "no record left" stub wording, merged into devlog.md, markers gone -
|
|
82
|
+
write_skeleton
|
|
83
|
+
touch "$DEVLOG_DIR/.interrupted"
|
|
84
|
+
bash "$SCRIPT_DIR/close-open-round.sh" "StopFailure:server_error"
|
|
85
|
+
assert_exit "matching open round -> exit 0" 0 $?
|
|
86
|
+
assert_file_absent "stamp: .round-current.md removed after merge" "$DEVLOG_DIR/.round-current.md"
|
|
87
|
+
BODY="$(cat "$DEVLOG_DIR/devlog.md")"
|
|
88
|
+
assert_contains "status INTERRUPTED" $'### Status\nINTERRUPTED' "$BODY"
|
|
89
|
+
assert_contains "reason line" "StopFailure:server_error" "$BODY"
|
|
90
|
+
assert_contains "summary stub (not recovered)" "devlog.md 沒有留下任何後續處理紀錄就結束了" "$BODY"
|
|
91
|
+
assert_contains "handoff stub heading" "#### 現況" "$BODY"
|
|
92
|
+
assert_contains "handoff stub body (not recovered)" "這一輪沒有留下任何處理紀錄" "$BODY"
|
|
93
|
+
assert_contains "user input kept" "hello" "$BODY"
|
|
94
|
+
|
|
95
|
+
# --- 2b: skeleton + a segment was recorded (recovered) but Summary/Handoff
|
|
96
|
+
# still missing -> INTERRUPTED + the old "interrupted mid-way" stub --
|
|
97
|
+
write_skeleton
|
|
98
|
+
cksum < "$DEVLOG_DIR/devlog.md" > "$DEVLOG_DIR/.turn-start"
|
|
99
|
+
printf '\n### 段落 1 - 12:01\n```text\nsome progress\n```\n\n' >> "$DEVLOG_DIR/devlog.md"
|
|
100
|
+
bash "$SCRIPT_DIR/close-open-round.sh" "user_interrupt"
|
|
101
|
+
BODY="$(cat "$DEVLOG_DIR/devlog.md")"
|
|
102
|
+
assert_contains "recovered summary stub" "這輪意外中斷。" "$BODY"
|
|
103
|
+
assert_contains "recovered handoff stub" "這輪沒有正常收尾。" "$BODY"
|
|
104
|
+
assert_contains "recovered segment kept" "some progress" "$BODY"
|
|
105
|
+
if [ -f "$DEVLOG_DIR/.round-open" ] || [ -f "$DEVLOG_DIR/.interrupted" ]; then
|
|
106
|
+
echo "FAIL: markers should be deleted after a successful stamp"
|
|
107
|
+
FAIL=1
|
|
108
|
+
else
|
|
109
|
+
echo "PASS: .round-open and .interrupted deleted"
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# --- 3: already has headings -> do not duplicate stubs, still stamp+merge ---
|
|
113
|
+
: > "$DEVLOG_DIR/devlog.md"
|
|
114
|
+
cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
|
|
115
|
+
## Round 1 — 2026-09-09T12:00:00+08:00
|
|
116
|
+
|
|
117
|
+
### User Input
|
|
118
|
+
```text
|
|
119
|
+
hello
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Summary
|
|
123
|
+
partial
|
|
124
|
+
|
|
125
|
+
### Reply
|
|
126
|
+
fixture reply.
|
|
127
|
+
|
|
128
|
+
### Handoff
|
|
129
|
+
#### 現況
|
|
130
|
+
partial
|
|
131
|
+
|
|
132
|
+
### Status
|
|
133
|
+
IN_PROGRESS
|
|
134
|
+
EOF
|
|
135
|
+
printf '%s\n' '{"round": 1, "opened_at": "2026-09-09T12:00:00+08:00"}' > "$DEVLOG_DIR/.round-open"
|
|
136
|
+
rm -f "$DEVLOG_DIR/.turn-start"
|
|
137
|
+
bash "$SCRIPT_DIR/close-open-round.sh" "user_interrupt"
|
|
138
|
+
assert_file_absent "existing-headings: .round-current.md removed after merge" "$DEVLOG_DIR/.round-current.md"
|
|
139
|
+
BODY="$(cat "$DEVLOG_DIR/devlog.md")"
|
|
140
|
+
assert_contains "kept existing summary" "partial" "$BODY"
|
|
141
|
+
COUNT="$(printf '%s\n' "$BODY" | grep -c '^### Summary' || true)"
|
|
142
|
+
if [ "$COUNT" = "1" ]; then
|
|
143
|
+
echo "PASS: did not add a second ### Summary"
|
|
144
|
+
else
|
|
145
|
+
echo "FAIL: expected one ### Summary, got $COUNT"
|
|
146
|
+
FAIL=1
|
|
147
|
+
fi
|
|
148
|
+
assert_contains "stamped interrupt" "INTERRUPTED" "$BODY"
|
|
149
|
+
assert_contains "user_interrupt reason" "user_interrupt" "$BODY"
|
|
150
|
+
|
|
151
|
+
# --- 4: no .round-open -> delete .interrupted, do not touch either file -----
|
|
152
|
+
write_skeleton
|
|
153
|
+
rm -f "$DEVLOG_DIR/.round-open"
|
|
154
|
+
touch "$DEVLOG_DIR/.interrupted"
|
|
155
|
+
BEFORE_CUR="$(cat "$DEVLOG_DIR/.round-current.md")"
|
|
156
|
+
BEFORE_MAIN="$(cat "$DEVLOG_DIR/devlog.md")"
|
|
157
|
+
bash "$SCRIPT_DIR/close-open-round.sh" "SessionEnd:other"
|
|
158
|
+
AFTER_CUR="$(cat "$DEVLOG_DIR/.round-current.md")"
|
|
159
|
+
AFTER_MAIN="$(cat "$DEVLOG_DIR/devlog.md")"
|
|
160
|
+
if [ "$BEFORE_CUR" = "$AFTER_CUR" ] && [ "$BEFORE_MAIN" = "$AFTER_MAIN" ]; then
|
|
161
|
+
echo "PASS: missing .round-open -> nothing merged or edited"
|
|
162
|
+
else
|
|
163
|
+
echo "FAIL: missing .round-open touched a file"
|
|
164
|
+
FAIL=1
|
|
165
|
+
fi
|
|
166
|
+
if [ -f "$DEVLOG_DIR/.interrupted" ]; then
|
|
167
|
+
echo "FAIL: missing .round-open should still delete .interrupted"
|
|
168
|
+
FAIL=1
|
|
169
|
+
else
|
|
170
|
+
echo "PASS: missing .round-open deleted .interrupted"
|
|
171
|
+
fi
|
|
172
|
+
rm -f "$DEVLOG_DIR/.round-current.md"
|
|
173
|
+
|
|
174
|
+
# --- 4b: malformed .round-open -> delete both markers, do not touch either --
|
|
175
|
+
write_skeleton
|
|
176
|
+
printf '%s\n' 'not valid json' > "$DEVLOG_DIR/.round-open"
|
|
177
|
+
touch "$DEVLOG_DIR/.interrupted"
|
|
178
|
+
BEFORE_CUR="$(cat "$DEVLOG_DIR/.round-current.md")"
|
|
179
|
+
BEFORE_MAIN="$(cat "$DEVLOG_DIR/devlog.md")"
|
|
180
|
+
bash "$SCRIPT_DIR/close-open-round.sh" "SessionEnd:other"
|
|
181
|
+
AFTER_CUR="$(cat "$DEVLOG_DIR/.round-current.md")"
|
|
182
|
+
AFTER_MAIN="$(cat "$DEVLOG_DIR/devlog.md")"
|
|
183
|
+
if [ "$BEFORE_CUR" = "$AFTER_CUR" ] && [ "$BEFORE_MAIN" = "$AFTER_MAIN" ]; then
|
|
184
|
+
echo "PASS: malformed .round-open -> nothing merged or edited"
|
|
185
|
+
else
|
|
186
|
+
echo "FAIL: malformed .round-open touched a file"
|
|
187
|
+
FAIL=1
|
|
188
|
+
fi
|
|
189
|
+
if [ -f "$DEVLOG_DIR/.interrupted" ] || [ -f "$DEVLOG_DIR/.round-open" ]; then
|
|
190
|
+
echo "FAIL: malformed .round-open should delete both markers"
|
|
191
|
+
FAIL=1
|
|
192
|
+
else
|
|
193
|
+
echo "PASS: malformed .round-open deleted both markers"
|
|
194
|
+
fi
|
|
195
|
+
rm -f "$DEVLOG_DIR/.round-current.md"
|
|
196
|
+
|
|
197
|
+
# --- 5: prior finished history in devlog.md is preserved across the merge --
|
|
198
|
+
cat > "$DEVLOG_DIR/devlog.md" <<'EOF'
|
|
199
|
+
## Round 1 — 2026-09-09T11:00:00+08:00
|
|
200
|
+
|
|
201
|
+
### Summary
|
|
202
|
+
old
|
|
203
|
+
|
|
204
|
+
### Reply
|
|
205
|
+
fixture reply.
|
|
206
|
+
|
|
207
|
+
### Handoff
|
|
208
|
+
#### 現況
|
|
209
|
+
old
|
|
210
|
+
|
|
211
|
+
### Status
|
|
212
|
+
DONE
|
|
213
|
+
|
|
214
|
+
## Checkpoint(Round 1 摘要)
|
|
215
|
+
keep me
|
|
216
|
+
EOF
|
|
217
|
+
cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
|
|
218
|
+
## Round 2 — 2026-09-09T12:00:00+08:00
|
|
219
|
+
|
|
220
|
+
### User Input
|
|
221
|
+
```text
|
|
222
|
+
new
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Status
|
|
226
|
+
IN_PROGRESS
|
|
227
|
+
EOF
|
|
228
|
+
printf '%s\n' '{"round": 2, "opened_at": "2026-09-09T12:00:00+08:00"}' > "$DEVLOG_DIR/.round-open"
|
|
229
|
+
bash "$SCRIPT_DIR/close-open-round.sh" "SessionEnd:clear"
|
|
230
|
+
assert_file_absent "prior-history: .round-current.md removed after merge" "$DEVLOG_DIR/.round-current.md"
|
|
231
|
+
BODY="$(cat "$DEVLOG_DIR/devlog.md")"
|
|
232
|
+
assert_contains "round 1 stays DONE" $'### Status\nDONE' "$BODY"
|
|
233
|
+
assert_contains "round 2 interrupted" "INTERRUPTED" "$BODY"
|
|
234
|
+
assert_contains "checkpoint kept" "keep me" "$BODY"
|
|
235
|
+
|
|
236
|
+
# --- 6: headings present but hash equal -> still stamp (no recovery proof) --
|
|
237
|
+
# (main's independent "completed last Round + hash moved -> do not stamp"
|
|
238
|
+
# fixture, which asserted devlog.md stayed byte-for-byte unchanged, tested a
|
|
239
|
+
# pre-merge assumption this branch deliberately changes: a recovered round
|
|
240
|
+
# now always gets merged into devlog.md rather than left in place untouched.
|
|
241
|
+
# That property is covered below by scenario 8 ("recovered case also
|
|
242
|
+
# merges"), which asserts the merge instead of asserting no-op.)
|
|
243
|
+
: > "$DEVLOG_DIR/devlog.md"
|
|
244
|
+
cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
|
|
245
|
+
## Round 1 — 2026-09-09T12:00:00+08:00
|
|
246
|
+
|
|
247
|
+
### User Input
|
|
248
|
+
```text
|
|
249
|
+
hello
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Summary
|
|
253
|
+
partial
|
|
254
|
+
|
|
255
|
+
### Reply
|
|
256
|
+
fixture reply.
|
|
257
|
+
|
|
258
|
+
### Handoff
|
|
259
|
+
#### 現況
|
|
260
|
+
partial
|
|
261
|
+
|
|
262
|
+
### Status
|
|
263
|
+
IN_PROGRESS
|
|
264
|
+
EOF
|
|
265
|
+
printf '%s\n' '{"round": 1, "opened_at": "2026-09-09T12:00:00+08:00"}' > "$DEVLOG_DIR/.round-open"
|
|
266
|
+
cksum < "$DEVLOG_DIR/.round-current.md" > "$DEVLOG_DIR/.turn-start"
|
|
267
|
+
bash "$SCRIPT_DIR/close-open-round.sh" "user_interrupt"
|
|
268
|
+
assert_file_absent "hash-equal: .round-current.md removed after merge" "$DEVLOG_DIR/.round-current.md"
|
|
269
|
+
BODY="$(cat "$DEVLOG_DIR/devlog.md")"
|
|
270
|
+
assert_contains "hash-equal headings still stamp" "INTERRUPTED" "$BODY"
|
|
271
|
+
assert_contains "hash-equal reason" "user_interrupt" "$BODY"
|
|
272
|
+
|
|
273
|
+
# --- 7: stamp-then-merge, prior round in devlog.md preserved ---------------
|
|
274
|
+
STAMP_DIR="$(mktemp -d)"
|
|
275
|
+
export CLAUDE_PROJECT_DIR="$STAMP_DIR"
|
|
276
|
+
mkdir -p "$STAMP_DIR/.devlog"
|
|
277
|
+
touch "$STAMP_DIR/.devlog/.enabled"
|
|
278
|
+
printf '## Round 7 — 2026-09-17T09:00:00+0800\n\n### User Input\n```text\ndo X\n```\n\n### Status\nIN_PROGRESS\n' > "$STAMP_DIR/.devlog/devlog.md"
|
|
279
|
+
printf '## Round 8 — 2026-09-17T09:30:00+0800\n\n### User Input\n```text\ndo Y\n```\n\n### Status\nIN_PROGRESS\n' > "$STAMP_DIR/.devlog/.round-current.md"
|
|
280
|
+
printf '{"round": 8}\n' > "$STAMP_DIR/.devlog/.round-open"
|
|
281
|
+
cksum < "$STAMP_DIR/.devlog/.round-current.md" > "$STAMP_DIR/.devlog/.turn-start"
|
|
282
|
+
|
|
283
|
+
bash "$SCRIPT_DIR/close-open-round.sh" "test_reason"
|
|
284
|
+
|
|
285
|
+
assert_file_absent "stamp: .round-current.md removed after merge" "$STAMP_DIR/.devlog/.round-current.md"
|
|
286
|
+
MAIN_AFTER="$(cat "$STAMP_DIR/.devlog/devlog.md")"
|
|
287
|
+
assert_contains "stamp: round 7 (prior history) still present" "## Round 7" "$MAIN_AFTER"
|
|
288
|
+
assert_contains "stamp: round 8 merged into devlog.md" "## Round 8" "$MAIN_AFTER"
|
|
289
|
+
assert_contains "stamp: INTERRUPTED status merged in" "INTERRUPTED" "$MAIN_AFTER"
|
|
290
|
+
assert_contains "stamp: reason tag merged in" "[reason: test_reason]" "$MAIN_AFTER"
|
|
291
|
+
rm -rf "$STAMP_DIR"
|
|
292
|
+
export CLAUDE_PROJECT_DIR="$TMP_ROOT"
|
|
293
|
+
|
|
294
|
+
# --- 8: recovered case also merges (Claude finished before the signal) -----
|
|
295
|
+
REC_DIR="$(mktemp -d)"
|
|
296
|
+
export CLAUDE_PROJECT_DIR="$REC_DIR"
|
|
297
|
+
mkdir -p "$REC_DIR/.devlog"
|
|
298
|
+
touch "$REC_DIR/.devlog/.enabled"
|
|
299
|
+
: > "$REC_DIR/.devlog/devlog.md"
|
|
300
|
+
printf '## Round 1 — 2026-09-17T09:00:00+0800\n\n### User Input\n```text\ndo X\n```\n\n### Summary\n完成了\n\n### Handoff\n#### 現況\n收尾\n\n### Status\nDONE\n' > "$REC_DIR/.devlog/.round-current.md"
|
|
301
|
+
printf '{"round": 1}\n' > "$REC_DIR/.devlog/.round-open"
|
|
302
|
+
echo "stale-hash-that-does-not-match" > "$REC_DIR/.devlog/.turn-start"
|
|
303
|
+
|
|
304
|
+
bash "$SCRIPT_DIR/close-open-round.sh" "user_interrupt"
|
|
305
|
+
|
|
306
|
+
assert_file_absent "recovered: .round-current.md removed after merge" "$REC_DIR/.devlog/.round-current.md"
|
|
307
|
+
MAIN_AFTER="$(cat "$REC_DIR/.devlog/devlog.md")"
|
|
308
|
+
assert_contains "recovered: finished round merged into devlog.md" "完成了" "$MAIN_AFTER"
|
|
309
|
+
assert_not_contains "recovered: no INTERRUPTED stamped over real content" "INTERRUPTED" "$MAIN_AFTER"
|
|
310
|
+
if [ -f "$REC_DIR/.devlog/.round-open" ]; then
|
|
311
|
+
echo "FAIL: recovered case should still delete .round-open"
|
|
312
|
+
FAIL=1
|
|
313
|
+
else
|
|
314
|
+
echo "PASS: recovered case deleted .round-open"
|
|
315
|
+
fi
|
|
316
|
+
rm -rf "$REC_DIR"
|
|
317
|
+
export CLAUDE_PROJECT_DIR="$TMP_ROOT"
|
|
318
|
+
|
|
319
|
+
if [ "$FAIL" -eq 0 ]; then
|
|
320
|
+
echo "All checks passed."
|
|
321
|
+
exit 0
|
|
322
|
+
else
|
|
323
|
+
echo "Some checks FAILED."
|
|
324
|
+
exit 1
|
|
325
|
+
fi
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
assert_eq() {
|
|
10
|
+
local d="$1" e="$2" a="$3"
|
|
11
|
+
if [ "$e" = "$a" ]; then echo "PASS: $d"; else echo "FAIL: $d expected [$e] got [$a]"; FAIL=1; fi
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
bash "$SCRIPT_DIR/compact-devlog.sh" >/dev/null
|
|
15
|
+
assert_eq "missing log exit 1" "1" "$?"
|
|
16
|
+
|
|
17
|
+
round() {
|
|
18
|
+
local n="$1" st="$2"
|
|
19
|
+
cat >> "$TMP/.devlog/devlog.md" <<EOF
|
|
20
|
+
## Round ${n} — 2026-09-09T12:00:00+08:00
|
|
21
|
+
|
|
22
|
+
### Summary
|
|
23
|
+
s${n}
|
|
24
|
+
|
|
25
|
+
### Reply
|
|
26
|
+
fixture reply.
|
|
27
|
+
|
|
28
|
+
### Handoff
|
|
29
|
+
#### 現況
|
|
30
|
+
h${n}
|
|
31
|
+
|
|
32
|
+
### Status
|
|
33
|
+
${st}
|
|
34
|
+
EOF
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
: > "$TMP/.devlog/devlog.md"
|
|
38
|
+
printf '%s\n\n' '# preamble' >> "$TMP/.devlog/devlog.md"
|
|
39
|
+
n=1
|
|
40
|
+
while [ "$n" -le 8 ]; do round "$n" DONE; n=$((n+1)); done
|
|
41
|
+
round 9 IN_PROGRESS
|
|
42
|
+
cat >> "$TMP/.devlog/devlog.md" <<'EOF'
|
|
43
|
+
## Checkpoint(Round 1-3 摘要)
|
|
44
|
+
cp body
|
|
45
|
+
EOF
|
|
46
|
+
|
|
47
|
+
OUT="$(bash "$SCRIPT_DIR/compact-devlog.sh")"
|
|
48
|
+
assert_eq "compact exit 0" "0" "$?"
|
|
49
|
+
echo "$OUT" | grep -q 'MOVED=3' && echo "PASS: moved 1-3" || { echo "FAIL: $OUT"; FAIL=1; }
|
|
50
|
+
grep -q '# preamble' "$TMP/.devlog/devlog.md" && echo "PASS: preamble stays" || { echo "FAIL: preamble"; FAIL=1; }
|
|
51
|
+
grep -q '## Checkpoint' "$TMP/.devlog/devlog.md" && echo "PASS: checkpoint stays" || { echo "FAIL: cp"; FAIL=1; }
|
|
52
|
+
grep -q '## Round 9' "$TMP/.devlog/devlog.md" && echo "PASS: in-progress stays" || { echo "FAIL: r9"; FAIL=1; }
|
|
53
|
+
grep -q '## Round 4' "$TMP/.devlog/devlog.md" && echo "PASS: last-5 keep 4" || { echo "FAIL: r4"; FAIL=1; }
|
|
54
|
+
grep -q '## Round 1' "$TMP/.devlog/devlog.archive.md" && echo "PASS: archive has 1" || { echo "FAIL: archive"; FAIL=1; }
|
|
55
|
+
grep -q '## Round 9' "$TMP/.devlog/devlog.archive.md" && { echo "FAIL: archived in-progress"; FAIL=1; } || echo "PASS: did not archive 9"
|
|
56
|
+
test -f "$TMP/.devlog/devlog.foo.md" && { echo "FAIL: touched keep file"; FAIL=1; } || echo "PASS: no keep file"
|
|
57
|
+
|
|
58
|
+
OUT2="$(bash "$SCRIPT_DIR/compact-devlog.sh")"
|
|
59
|
+
echo "$OUT2" | grep -q 'MOVED=0' && echo "PASS: second compact moves 0" || { echo "FAIL: $OUT2"; FAIL=1; }
|
|
60
|
+
|
|
61
|
+
if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
|
|
62
|
+
else echo "Some checks FAILED."; exit 1; fi
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
4
|
+
. "$SCRIPT_DIR/devlog-lock.sh"
|
|
5
|
+
TMP="$(mktemp -d)"
|
|
6
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
7
|
+
DEVLOG_DIR="$TMP/.devlog"
|
|
8
|
+
mkdir -p "$DEVLOG_DIR"
|
|
9
|
+
FAIL=0
|
|
10
|
+
|
|
11
|
+
devlog_lock_acquire
|
|
12
|
+
[ "${LOCK_HELD:-0}" -eq 1 ] && [ -d "$DEVLOG_DIR/.lock" ] \
|
|
13
|
+
&& echo "PASS: acquired" || { echo "FAIL: acquire"; FAIL=1; }
|
|
14
|
+
mkdir "$DEVLOG_DIR/.lock" >/dev/null 2>&1
|
|
15
|
+
[ "$?" -ne 0 ] && echo "PASS: second mkdir excluded" || { echo "FAIL: duplicate mkdir"; FAIL=1; }
|
|
16
|
+
devlog_lock_release
|
|
17
|
+
devlog_lock_acquire
|
|
18
|
+
[ "${LOCK_HELD:-0}" -eq 1 ] && echo "PASS: reacquired after release" || { echo "FAIL: reacquire"; FAIL=1; }
|
|
19
|
+
devlog_lock_release
|
|
20
|
+
|
|
21
|
+
mkdir "$DEVLOG_DIR/.lock"
|
|
22
|
+
START="$(date +%s)"
|
|
23
|
+
devlog_lock_acquire
|
|
24
|
+
END="$(date +%s)"
|
|
25
|
+
ELAPSED=$((END - START))
|
|
26
|
+
[ "${LOCK_HELD:-0}" -eq 0 ] && [ "$ELAPSED" -ge 2 ] && [ "$ELAPSED" -le 3 ] \
|
|
27
|
+
&& echo "PASS: contention fails open" || { echo "FAIL: contention held=$LOCK_HELD elapsed=$ELAPSED"; FAIL=1; }
|
|
28
|
+
rmdir "$DEVLOG_DIR/.lock"
|
|
29
|
+
|
|
30
|
+
MIN_PATH="$TMP/min-path"
|
|
31
|
+
mkdir "$MIN_PATH"
|
|
32
|
+
ln -s "$(command -v bash)" "$MIN_PATH/bash"
|
|
33
|
+
HELD="$(PATH="$MIN_PATH" bash -c '. "$1"; DEVLOG_DIR="$2"; devlog_lock_acquire; echo "${LOCK_HELD:-0}"' _ "$SCRIPT_DIR/devlog-lock.sh" "$DEVLOG_DIR")"
|
|
34
|
+
[ "$HELD" = "0" ] && echo "PASS: missing lock tools fails open" || { echo "FAIL: restricted path held=$HELD"; FAIL=1; }
|
|
35
|
+
|
|
36
|
+
if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
|
|
37
|
+
else echo "Some checks FAILED."; exit 1; fi
|