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,825 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Self-check for round-start.sh skeleton + dangling heal. Run:
|
|
3
|
+
# bash hooks/scripts/test-round-start.sh
|
|
4
|
+
set -uo pipefail
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
7
|
+
# shellcheck source=../json-field.sh
|
|
8
|
+
. "$SCRIPT_DIR/json-field.sh"
|
|
9
|
+
TMP_ROOT="$(mktemp -d)"
|
|
10
|
+
trap 'rm -rf "$TMP_ROOT"' EXIT
|
|
11
|
+
|
|
12
|
+
export CLAUDE_PROJECT_DIR="$TMP_ROOT"
|
|
13
|
+
DEVLOG_DIR="$TMP_ROOT/.devlog"
|
|
14
|
+
mkdir -p "$DEVLOG_DIR"
|
|
15
|
+
|
|
16
|
+
FAIL=0
|
|
17
|
+
assert_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 (missing: $needle)"; 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 (unexpected: $needle)"; FAIL=1 ;;
|
|
37
|
+
*) echo "PASS: $desc" ;;
|
|
38
|
+
esac
|
|
39
|
+
}
|
|
40
|
+
assert_file_absent() {
|
|
41
|
+
local desc="$1" path="$2"
|
|
42
|
+
if [ -e "$path" ]; then
|
|
43
|
+
echo "FAIL: $desc ($path exists)"
|
|
44
|
+
FAIL=1
|
|
45
|
+
else
|
|
46
|
+
echo "PASS: $desc"
|
|
47
|
+
fi
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
PROMPT_HELLO='{"prompt":"hello world"}'
|
|
51
|
+
|
|
52
|
+
# --- 1: disabled -> no files ----------------------------------------------
|
|
53
|
+
printf '%s' "$PROMPT_HELLO" | bash "$SCRIPT_DIR/round-start.sh"
|
|
54
|
+
assert_exit "disabled -> exit 0" 0 $?
|
|
55
|
+
assert_file_absent "disabled -> no devlog.md" "$DEVLOG_DIR/devlog.md"
|
|
56
|
+
assert_file_absent "disabled -> no .round-open" "$DEVLOG_DIR/.round-open"
|
|
57
|
+
assert_file_absent "disabled -> no .turn-start" "$DEVLOG_DIR/.turn-start"
|
|
58
|
+
|
|
59
|
+
touch "$DEVLOG_DIR/.enabled"
|
|
60
|
+
|
|
61
|
+
# --- 2: Round 1 skeleton + markers + hash after write ---------------------
|
|
62
|
+
printf '%s' "$PROMPT_HELLO" | bash "$SCRIPT_DIR/round-start.sh"
|
|
63
|
+
assert_exit "first prompt -> exit 0" 0 $?
|
|
64
|
+
BODY="$(cat "$DEVLOG_DIR/.round-current.md")"
|
|
65
|
+
assert_contains "Round 1 heading" "## Round 1 —" "$BODY"
|
|
66
|
+
assert_contains "fenced user input" $'```text\nhello world\n```' "$BODY"
|
|
67
|
+
assert_contains "status in progress" $'### Status\nIN_PROGRESS' "$BODY"
|
|
68
|
+
assert_file_absent "Round 1 skeleton -> devlog.md untouched" "$DEVLOG_DIR/devlog.md"
|
|
69
|
+
if [ -f "$DEVLOG_DIR/.round-open" ]; then
|
|
70
|
+
echo "PASS: .round-open created"
|
|
71
|
+
else
|
|
72
|
+
echo "FAIL: .round-open missing"
|
|
73
|
+
FAIL=1
|
|
74
|
+
fi
|
|
75
|
+
OPEN_N="$(grep -o '"round"[[:space:]]*:[[:space:]]*[0-9]\+' "$DEVLOG_DIR/.round-open" | grep -o '[0-9]\+$')"
|
|
76
|
+
if [ "$OPEN_N" = "1" ]; then
|
|
77
|
+
echo "PASS: .round-open round=1"
|
|
78
|
+
else
|
|
79
|
+
echo "FAIL: .round-open round should be 1, got $OPEN_N"
|
|
80
|
+
FAIL=1
|
|
81
|
+
fi
|
|
82
|
+
AFTER_HASH="$(cksum < "$DEVLOG_DIR/.round-current.md")"
|
|
83
|
+
START_HASH="$(cat "$DEVLOG_DIR/.turn-start")"
|
|
84
|
+
if [ "$AFTER_HASH" = "$START_HASH" ]; then
|
|
85
|
+
echo "PASS: .turn-start matches post-skeleton hash"
|
|
86
|
+
else
|
|
87
|
+
echo "FAIL: .turn-start should be captured after skeleton write"
|
|
88
|
+
FAIL=1
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# --- 2b: normal (non-fold) new-round path writes to .round-current.md,
|
|
92
|
+
# leaving devlog.md's prior history untouched, and .turn-start hashes
|
|
93
|
+
# .round-current.md -----------------------------------------------------
|
|
94
|
+
NEW_DIR="$(mktemp -d)"
|
|
95
|
+
export CLAUDE_PROJECT_DIR="$NEW_DIR"
|
|
96
|
+
mkdir -p "$NEW_DIR/.devlog"
|
|
97
|
+
touch "$NEW_DIR/.devlog/.enabled"
|
|
98
|
+
printf '## Round 3 — 2026-09-17T08:00:00+0800\n\n### Summary\ns3\n\n### Handoff\n#### 現況\nc3\n\n### Status\nDONE\n' > "$NEW_DIR/.devlog/devlog.md"
|
|
99
|
+
|
|
100
|
+
echo '{"session_id":"s2","prompt":"下一件事"}' | bash "$SCRIPT_DIR/round-start.sh"
|
|
101
|
+
|
|
102
|
+
MAIN_AFTER="$(cat "$NEW_DIR/.devlog/devlog.md")"
|
|
103
|
+
CURRENT_AFTER="$(cat "$NEW_DIR/.devlog/.round-current.md" 2>/dev/null || echo '')"
|
|
104
|
+
assert_not_contains "new round: not appended to devlog.md directly" "## Round 4" "$MAIN_AFTER"
|
|
105
|
+
assert_contains "new round: skeleton written to .round-current.md" "## Round 4" "$CURRENT_AFTER"
|
|
106
|
+
assert_contains "new round: devlog.md keeps prior history" "## Round 3" "$MAIN_AFTER"
|
|
107
|
+
|
|
108
|
+
TURN_HASH="$(cat "$NEW_DIR/.devlog/.turn-start" 2>/dev/null || echo '')"
|
|
109
|
+
EXPECTED_HASH="$(cksum < "$NEW_DIR/.devlog/.round-current.md")"
|
|
110
|
+
if [ "$TURN_HASH" = "$EXPECTED_HASH" ]; then echo "PASS: .turn-start hashes .round-current.md"; else echo "FAIL: .turn-start hashes .round-current.md"; FAIL=1; fi
|
|
111
|
+
rm -rf "$NEW_DIR"
|
|
112
|
+
export CLAUDE_PROJECT_DIR="$TMP_ROOT"
|
|
113
|
+
|
|
114
|
+
# --- 3: next prompt is Round 2 (dangling heal stamps devlog.md; the new
|
|
115
|
+
# round opens in .round-current.md) — a genuinely-open Round 1 sitting in
|
|
116
|
+
# .round-current.md is a hand-built fixture representing what
|
|
117
|
+
# close-open-round.sh operates on ------------------------------------------
|
|
118
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open" "$DEVLOG_DIR/.turn-start" "$DEVLOG_DIR/.round-current.md"
|
|
119
|
+
cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
|
|
120
|
+
## Round 1 — 2026-09-09T12:00:00+08:00
|
|
121
|
+
|
|
122
|
+
### User Input
|
|
123
|
+
```text
|
|
124
|
+
hello world
|
|
125
|
+
```
|
|
126
|
+
EOF
|
|
127
|
+
printf '%s\n' '{"round": 1, "opened_at": "2026-09-09T12:00:00+08:00"}' > "$DEVLOG_DIR/.round-open"
|
|
128
|
+
printf '%s' '{"prompt":"second"}' | bash "$SCRIPT_DIR/round-start.sh"
|
|
129
|
+
MAIN_BODY="$(cat "$DEVLOG_DIR/devlog.md")"
|
|
130
|
+
CUR_BODY="$(cat "$DEVLOG_DIR/.round-current.md" 2>/dev/null || echo '')"
|
|
131
|
+
assert_contains "Round 2 heading in .round-current.md" "## Round 2 —" "$CUR_BODY"
|
|
132
|
+
assert_contains "previous round interrupted (dangling heal) in devlog.md" "INTERRUPTED" "$MAIN_BODY"
|
|
133
|
+
assert_contains "dangling reason" "dangling:next_prompt" "$MAIN_BODY"
|
|
134
|
+
OPEN_N="$(grep -o '"round"[[:space:]]*:[[:space:]]*[0-9]\+' "$DEVLOG_DIR/.round-open" | grep -o '[0-9]\+$')"
|
|
135
|
+
if [ "$OPEN_N" = "2" ]; then
|
|
136
|
+
echo "PASS: .round-open now round=2"
|
|
137
|
+
else
|
|
138
|
+
echo "FAIL: .round-open round should be 2, got $OPEN_N"
|
|
139
|
+
FAIL=1
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
# --- 4: checkpoint increments once on dangling+new ------------------------
|
|
143
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open" "$DEVLOG_DIR/.turn-start"
|
|
144
|
+
printf '%s\n' '{"rounds_since_checkpoint": 0, "max_silent_rounds": 20, "checkpoint_marker_count": 0}' > "$DEVLOG_DIR/.checkpoint-state"
|
|
145
|
+
printf '%s' "$PROMPT_HELLO" | bash "$SCRIPT_DIR/round-start.sh"
|
|
146
|
+
printf '%s' '{"prompt":"again"}' | bash "$SCRIPT_DIR/round-start.sh"
|
|
147
|
+
CP="$(grep -o '"rounds_since_checkpoint"[[:space:]]*:[[:space:]]*[0-9]\+' "$DEVLOG_DIR/.checkpoint-state" | grep -o '[0-9]\+$')"
|
|
148
|
+
if [ "$CP" = "2" ]; then
|
|
149
|
+
echo "PASS: two UserPromptSubmit ticks -> rounds_since_checkpoint=2 (dangling+new still one increment each)"
|
|
150
|
+
else
|
|
151
|
+
echo "FAIL: expected rounds_since_checkpoint=2, got $CP"
|
|
152
|
+
FAIL=1
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
# --- 5: well-formed span skips new Round, still increments ticks ----------
|
|
156
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open"
|
|
157
|
+
: > "$DEVLOG_DIR/devlog.md"
|
|
158
|
+
cat > "$DEVLOG_DIR/.span-open" <<'SPANEOF'
|
|
159
|
+
{
|
|
160
|
+
"round": 9,
|
|
161
|
+
"opened_at": "2026-09-09T00:00:00+08:00",
|
|
162
|
+
"ticks_since_checkin": 0,
|
|
163
|
+
"max_silent_ticks": 5
|
|
164
|
+
}
|
|
165
|
+
SPANEOF
|
|
166
|
+
printf '%s' "$PROMPT_HELLO" | bash "$SCRIPT_DIR/round-start.sh"
|
|
167
|
+
if grep -q '## Round' "$DEVLOG_DIR/devlog.md"; then
|
|
168
|
+
echo "FAIL: span tick should not append a Round"
|
|
169
|
+
FAIL=1
|
|
170
|
+
else
|
|
171
|
+
echo "PASS: span tick appended no Round"
|
|
172
|
+
fi
|
|
173
|
+
assert_file_absent "span tick -> no .round-open" "$DEVLOG_DIR/.round-open"
|
|
174
|
+
TICKS="$(grep -o '"ticks_since_checkin"[[:space:]]*:[[:space:]]*[0-9]\+' "$DEVLOG_DIR/.span-open" | grep -o '[0-9]\+$')"
|
|
175
|
+
if [ "$TICKS" = "1" ]; then
|
|
176
|
+
echo "PASS: span ticks incremented to 1"
|
|
177
|
+
else
|
|
178
|
+
echo "FAIL: ticks_since_checkin should be 1, got $TICKS"
|
|
179
|
+
FAIL=1
|
|
180
|
+
fi
|
|
181
|
+
rm -f "$DEVLOG_DIR/.span-open"
|
|
182
|
+
|
|
183
|
+
# --- 6: missing prompt -> placeholder -------------------------------------
|
|
184
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open"
|
|
185
|
+
printf '%s' '{}' | bash "$SCRIPT_DIR/round-start.sh"
|
|
186
|
+
BODY="$(cat "$DEVLOG_DIR/.round-current.md")"
|
|
187
|
+
assert_contains "placeholder prompt" "(無 prompt)" "$BODY"
|
|
188
|
+
|
|
189
|
+
# --- 7a: inner fence neutralized before wrapping ----------------------------
|
|
190
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open"
|
|
191
|
+
printf '%s' '{"prompt":"before```inside```after"}' | bash "$SCRIPT_DIR/round-start.sh"
|
|
192
|
+
BODY="$(cat "$DEVLOG_DIR/.round-current.md")"
|
|
193
|
+
assert_contains "inner fence neutralized" "⟨fence⟩" "$BODY"
|
|
194
|
+
if printf '%s\n' "$BODY" | grep -q '```inside'; then
|
|
195
|
+
echo "FAIL: raw inner fence leaked into the wrapper"
|
|
196
|
+
FAIL=1
|
|
197
|
+
else
|
|
198
|
+
echo "PASS: wrapper fence stayed closed"
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
# --- 7aa: common provider tokens are masked in User Input -------------------
|
|
202
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open"
|
|
203
|
+
RAW_TOKEN="ghp_abcdefghijklmnopqrstuvwxyz0123456789"
|
|
204
|
+
printf '{"prompt":"token %s"}' "$RAW_TOKEN" | bash "$SCRIPT_DIR/round-start.sh"
|
|
205
|
+
BODY="$(cat "$DEVLOG_DIR/.round-current.md")"
|
|
206
|
+
assert_contains "prompt token masked" "(已遮罩)" "$BODY"
|
|
207
|
+
assert_not_contains "raw prompt token absent" "$RAW_TOKEN" "$BODY"
|
|
208
|
+
|
|
209
|
+
# --- 7b: truncation notice when prompt exceeds 4000 -------------------------
|
|
210
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open"
|
|
211
|
+
LONG="$(awk 'BEGIN { s=""; for (i=0;i<4005;i++) s=s "a"; print s }')"
|
|
212
|
+
printf '{"prompt":"%s"}' "$LONG" | bash "$SCRIPT_DIR/round-start.sh"
|
|
213
|
+
BODY="$(cat "$DEVLOG_DIR/.round-current.md")"
|
|
214
|
+
assert_contains "truncation notice" "(後略,已截斷至 4000 字)" "$BODY"
|
|
215
|
+
LEN="$(awk '/^```text$/{p=1;next} /^```$/{p=0;next} p{s=s $0} END{print length(s)}' "$DEVLOG_DIR/.round-current.md")"
|
|
216
|
+
if [ "$LEN" -le 4000 ]; then
|
|
217
|
+
echo "PASS: fenced body length <= 4000 (got $LEN)"
|
|
218
|
+
else
|
|
219
|
+
echo "FAIL: fenced body length $LEN exceeds 4000"
|
|
220
|
+
FAIL=1
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
# --- 8: heading injection stays inside fence --------------------------------
|
|
224
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open"
|
|
225
|
+
printf '%s' '{"prompt":"see\n## Round 99\n### Status"}' | bash "$SCRIPT_DIR/round-start.sh"
|
|
226
|
+
# Only one unfenced ## Round (the real heading). Count fence-aware:
|
|
227
|
+
UNFENCED="$(awk '/^[ \t]*```/{f=!f} !f && /^## Round /{c++} END{print c+0}' "$DEVLOG_DIR/.round-current.md")"
|
|
228
|
+
if [ "$UNFENCED" = "1" ]; then
|
|
229
|
+
echo "PASS: prompt ## Round 99 did not create a second unfenced Round heading"
|
|
230
|
+
else
|
|
231
|
+
echo "FAIL: expected 1 unfenced ## Round heading, got $UNFENCED"
|
|
232
|
+
FAIL=1
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
# --- 9: segment last_seen_cksum is post-skeleton, tracking .round-current.md
|
|
236
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open"
|
|
237
|
+
printf '%s\n' '{"last_change_epoch": 1, "last_seen_cksum": "old", "max_silent_seconds": 900}' > "$DEVLOG_DIR/.segment-state"
|
|
238
|
+
printf '%s' "$PROMPT_HELLO" | bash "$SCRIPT_DIR/round-start.sh"
|
|
239
|
+
POST="$(cksum < "$DEVLOG_DIR/.round-current.md" | tr -d '\n')"
|
|
240
|
+
SEEN="$(grep -o '"last_seen_cksum"[[:space:]]*:[[:space:]]*"[^"]*"' "$DEVLOG_DIR/.segment-state" | sed 's/.*"last_seen_cksum"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')"
|
|
241
|
+
if [ "$SEEN" = "$POST" ]; then
|
|
242
|
+
echo "PASS: segment last_seen_cksum matches post-skeleton hash"
|
|
243
|
+
else
|
|
244
|
+
echo "FAIL: last_seen_cksum=$SEEN expected $POST"
|
|
245
|
+
FAIL=1
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
# --- 9c: segment-state cksum identity tracks .round-current.md, in isolation
|
|
249
|
+
SEG_DIR="$(mktemp -d)"
|
|
250
|
+
export CLAUDE_PROJECT_DIR="$SEG_DIR"
|
|
251
|
+
mkdir -p "$SEG_DIR/.devlog"
|
|
252
|
+
touch "$SEG_DIR/.devlog/.enabled"
|
|
253
|
+
printf '{"last_change_epoch": 0, "last_seen_cksum": "", "max_silent_seconds": 600}\n' > "$SEG_DIR/.devlog/.segment-state"
|
|
254
|
+
|
|
255
|
+
echo '{"session_id":"s3","prompt":"go"}' | bash "$SCRIPT_DIR/round-start.sh"
|
|
256
|
+
|
|
257
|
+
STORED_CKSUM="$(json_str_get "$SEG_DIR/.devlog/.segment-state" last_seen_cksum)"
|
|
258
|
+
ACTUAL_CKSUM="$(cksum < "$SEG_DIR/.devlog/.round-current.md" | tr -d '\n')"
|
|
259
|
+
if [ "$STORED_CKSUM" = "$ACTUAL_CKSUM" ]; then echo "PASS: segment-state cksum tracks .round-current.md"; else echo "FAIL: segment-state cksum tracks .round-current.md (got $STORED_CKSUM want $ACTUAL_CKSUM)"; FAIL=1; fi
|
|
260
|
+
rm -rf "$SEG_DIR"
|
|
261
|
+
export CLAUDE_PROJECT_DIR="$TMP_ROOT"
|
|
262
|
+
|
|
263
|
+
# --- 9b: segment state stores the submitting session id ---------------------
|
|
264
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open"
|
|
265
|
+
printf '%s\n' '{"last_change_epoch": 1, "last_seen_cksum": "old", "max_silent_seconds": 900}' > "$DEVLOG_DIR/.segment-state"
|
|
266
|
+
printf '%s' '{"prompt":"hi","session_id":"s1"}' | bash "$SCRIPT_DIR/round-start.sh"
|
|
267
|
+
SESSION_ID="$(grep -o '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' "$DEVLOG_DIR/.segment-state" | sed 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')"
|
|
268
|
+
if [ "$SESSION_ID" = "s1" ]; then
|
|
269
|
+
echo "PASS: segment state stores session_id"
|
|
270
|
+
else
|
|
271
|
+
echo "FAIL: expected session_id=s1, got [$SESSION_ID]"
|
|
272
|
+
FAIL=1
|
|
273
|
+
fi
|
|
274
|
+
|
|
275
|
+
# --- 10: dangling heal skips a completed last Round, still opens Round 2 ----
|
|
276
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open" "$DEVLOG_DIR/.turn-start" "$DEVLOG_DIR/.span-open" "$DEVLOG_DIR/.round-current.md"
|
|
277
|
+
cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
|
|
278
|
+
## Round 1 — 2026-09-09T12:00:00+08:00
|
|
279
|
+
|
|
280
|
+
### User Input
|
|
281
|
+
```text
|
|
282
|
+
done
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Summary
|
|
286
|
+
finished
|
|
287
|
+
|
|
288
|
+
### Reply
|
|
289
|
+
fixture reply.
|
|
290
|
+
|
|
291
|
+
### Handoff
|
|
292
|
+
#### 現況
|
|
293
|
+
finished
|
|
294
|
+
|
|
295
|
+
### Status
|
|
296
|
+
DONE
|
|
297
|
+
EOF
|
|
298
|
+
printf '%s\n' '{"round": 1, "opened_at": "2026-09-09T12:00:00+08:00"}' > "$DEVLOG_DIR/.round-open"
|
|
299
|
+
cksum < "$DEVLOG_DIR/.round-current.md" > "$DEVLOG_DIR/.turn-start"
|
|
300
|
+
printf '\n' >> "$DEVLOG_DIR/.round-current.md"
|
|
301
|
+
printf '%s' '{"prompt":"next"}' | bash "$SCRIPT_DIR/round-start.sh"
|
|
302
|
+
BODY="$(cat "$DEVLOG_DIR/devlog.md")"
|
|
303
|
+
CUR_BODY="$(cat "$DEVLOG_DIR/.round-current.md" 2>/dev/null || echo '')"
|
|
304
|
+
assert_contains "completed dangling keeps DONE" $'### Status\nDONE' "$BODY"
|
|
305
|
+
assert_not_contains "completed dangling must not stamp Round 1" "INTERRUPTED" "$BODY"
|
|
306
|
+
assert_contains "still opened Round 2" "## Round 2 —" "$CUR_BODY"
|
|
307
|
+
|
|
308
|
+
# --- 11: .awaiting-reply matches last Round -> folds as a segment, reopening
|
|
309
|
+
# the Round out of devlog.md and into .round-current.md --------------------
|
|
310
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open" "$DEVLOG_DIR/.turn-start" "$DEVLOG_DIR/.span-open" "$DEVLOG_DIR/.checkpoint-state" "$DEVLOG_DIR/.round-current.md"
|
|
311
|
+
cat > "$DEVLOG_DIR/devlog.md" <<'EOF'
|
|
312
|
+
## Round 5 — 2026-09-09T12:00:00+08:00
|
|
313
|
+
|
|
314
|
+
### User Input
|
|
315
|
+
```text
|
|
316
|
+
what color should the button be?
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Summary
|
|
320
|
+
Asked the user to pick a color.
|
|
321
|
+
|
|
322
|
+
### Reply
|
|
323
|
+
fixture reply.
|
|
324
|
+
|
|
325
|
+
### Handoff
|
|
326
|
+
#### 現況
|
|
327
|
+
Waiting on the user's color choice.
|
|
328
|
+
#### 完成條件
|
|
329
|
+
缺的外部輸入已出現,且可觀察條件達成。
|
|
330
|
+
#### 下一步
|
|
331
|
+
Apply the chosen color once they answer.
|
|
332
|
+
|
|
333
|
+
### Status
|
|
334
|
+
BLOCKED
|
|
335
|
+
EOF
|
|
336
|
+
printf '%s\n' '{"round": 5, "opened_at": "2026-09-09T12:00:00+08:00"}' > "$DEVLOG_DIR/.awaiting-reply"
|
|
337
|
+
printf '%s' '{"prompt":"blue please"}' | bash "$SCRIPT_DIR/round-start.sh"
|
|
338
|
+
MAIN_BODY="$(cat "$DEVLOG_DIR/devlog.md" 2>/dev/null || echo '')"
|
|
339
|
+
CUR_BODY="$(cat "$DEVLOG_DIR/.round-current.md" 2>/dev/null || echo '')"
|
|
340
|
+
assert_not_contains "fold: round 5 no longer in devlog.md" "## Round 5" "$MAIN_BODY"
|
|
341
|
+
UNFENCED_ROUNDS="$(awk '/^[ \t]*```/{f=!f} !f && /^## Round /{c++} END{print c+0}' "$DEVLOG_DIR/.round-current.md")"
|
|
342
|
+
if [ "$UNFENCED_ROUNDS" = "1" ]; then
|
|
343
|
+
echo "PASS: fold did not open a second Round"
|
|
344
|
+
else
|
|
345
|
+
echo "FAIL: expected 1 unfenced Round heading, got $UNFENCED_ROUNDS"
|
|
346
|
+
FAIL=1
|
|
347
|
+
fi
|
|
348
|
+
assert_contains "folded segment heading" "### 段落 1 -" "$CUR_BODY"
|
|
349
|
+
assert_contains "folded segment marks it a reply" "(回覆上一輪的問題)" "$CUR_BODY"
|
|
350
|
+
assert_contains "folded segment keeps user's words" "blue please" "$CUR_BODY"
|
|
351
|
+
# segment must land before Summary, not after
|
|
352
|
+
SEG_LINE="$(grep -n '### 段落 1' "$DEVLOG_DIR/.round-current.md" | head -1 | cut -d: -f1)"
|
|
353
|
+
SUM_LINE="$(grep -n '^### Summary$' "$DEVLOG_DIR/.round-current.md" | head -1 | cut -d: -f1)"
|
|
354
|
+
if [ "$SEG_LINE" -lt "$SUM_LINE" ]; then
|
|
355
|
+
echo "PASS: folded segment inserted before Summary"
|
|
356
|
+
else
|
|
357
|
+
echo "FAIL: folded segment landed after Summary (seg=$SEG_LINE summary=$SUM_LINE)"
|
|
358
|
+
FAIL=1
|
|
359
|
+
fi
|
|
360
|
+
if [ -f "$DEVLOG_DIR/.awaiting-reply" ]; then
|
|
361
|
+
echo "FAIL: .awaiting-reply should be consumed after use"
|
|
362
|
+
FAIL=1
|
|
363
|
+
else
|
|
364
|
+
echo "PASS: .awaiting-reply consumed"
|
|
365
|
+
fi
|
|
366
|
+
OPEN_N="$(grep -o '"round"[[:space:]]*:[[:space:]]*[0-9]\+' "$DEVLOG_DIR/.round-open" | grep -o '[0-9]\+$')"
|
|
367
|
+
if [ "$OPEN_N" = "5" ]; then
|
|
368
|
+
echo "PASS: .round-open rewritten to the folded Round (5)"
|
|
369
|
+
else
|
|
370
|
+
echo "FAIL: expected .round-open round=5, got $OPEN_N"
|
|
371
|
+
FAIL=1
|
|
372
|
+
fi
|
|
373
|
+
AFTER_HASH="$(cksum < "$DEVLOG_DIR/.round-current.md")"
|
|
374
|
+
START_HASH="$(cat "$DEVLOG_DIR/.turn-start")"
|
|
375
|
+
if [ "$AFTER_HASH" = "$START_HASH" ]; then
|
|
376
|
+
echo "PASS: .turn-start matches post-fold hash"
|
|
377
|
+
else
|
|
378
|
+
echo "FAIL: .turn-start should be captured after the fold insert"
|
|
379
|
+
FAIL=1
|
|
380
|
+
fi
|
|
381
|
+
|
|
382
|
+
# --- 11b (Step 3 brief test): Reply Fold reopens the awaited round into
|
|
383
|
+
# .round-current.md, not editing it in place inside devlog.md — devlog.md
|
|
384
|
+
# should no longer contain it, and .round-current.md should hold exactly
|
|
385
|
+
# that round plus the folded segment. Own isolated fixture. --------------
|
|
386
|
+
FOLD_DIR="$(mktemp -d)"
|
|
387
|
+
export CLAUDE_PROJECT_DIR="$FOLD_DIR"
|
|
388
|
+
mkdir -p "$FOLD_DIR/.devlog"
|
|
389
|
+
touch "$FOLD_DIR/.devlog/.enabled"
|
|
390
|
+
printf '## Round 5 — 2026-09-17T09:00:00+0800\n\n### User Input\n```text\n要不要修 A?\n```\n\n### Summary\n問了一題\n\n### Handoff\n#### 現況\n等回覆\n\n### Status\nBLOCKED\n' > "$FOLD_DIR/.devlog/devlog.md"
|
|
391
|
+
printf '{"round": 5}\n' > "$FOLD_DIR/.devlog/.awaiting-reply"
|
|
392
|
+
|
|
393
|
+
echo '{"session_id":"s1","prompt":"要"}' | bash "$SCRIPT_DIR/round-start.sh"
|
|
394
|
+
|
|
395
|
+
MAIN_AFTER="$(cat "$FOLD_DIR/.devlog/devlog.md")"
|
|
396
|
+
CURRENT_AFTER="$(cat "$FOLD_DIR/.devlog/.round-current.md" 2>/dev/null || echo '')"
|
|
397
|
+
assert_not_contains "fold: round 5 no longer in devlog.md" "## Round 5" "$MAIN_AFTER"
|
|
398
|
+
assert_contains "fold: round 5 moved into .round-current.md" "## Round 5" "$CURRENT_AFTER"
|
|
399
|
+
assert_contains "fold: folded segment written into .round-current.md" "回覆上一輪的問題" "$CURRENT_AFTER"
|
|
400
|
+
rm -rf "$FOLD_DIR"
|
|
401
|
+
export CLAUDE_PROJECT_DIR="$TMP_ROOT"
|
|
402
|
+
|
|
403
|
+
# --- 12: second reply on the same Round numbers segments incrementally,
|
|
404
|
+
# standalone fixture: devlog.md holds the round already folded once
|
|
405
|
+
# (matching the merged-back-between-turns invariant) -----------------------
|
|
406
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open" "$DEVLOG_DIR/.turn-start" "$DEVLOG_DIR/.round-current.md"
|
|
407
|
+
cat > "$DEVLOG_DIR/devlog.md" <<'EOF'
|
|
408
|
+
## Round 5 — 2026-09-09T12:00:00+08:00
|
|
409
|
+
|
|
410
|
+
### User Input
|
|
411
|
+
```text
|
|
412
|
+
what color should the button be?
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### 段落 1 - 12:01(回覆上一輪的問題)
|
|
416
|
+
```text
|
|
417
|
+
blue please
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Summary
|
|
421
|
+
Asked the user to pick a color.
|
|
422
|
+
|
|
423
|
+
### Handoff
|
|
424
|
+
#### 現況
|
|
425
|
+
Waiting on the user's color choice.
|
|
426
|
+
#### 下一步
|
|
427
|
+
Apply the chosen color once they answer.
|
|
428
|
+
|
|
429
|
+
### Status
|
|
430
|
+
BLOCKED
|
|
431
|
+
EOF
|
|
432
|
+
printf '%s\n' '{"round": 5, "opened_at": "2026-09-09T12:05:00+08:00"}' > "$DEVLOG_DIR/.awaiting-reply"
|
|
433
|
+
printf '%s' '{"prompt":"actually make it green"}' | bash "$SCRIPT_DIR/round-start.sh"
|
|
434
|
+
CUR_BODY="$(cat "$DEVLOG_DIR/.round-current.md" 2>/dev/null || echo '')"
|
|
435
|
+
assert_contains "first segment still present" "### 段落 1 -" "$CUR_BODY"
|
|
436
|
+
assert_contains "second segment numbered 2" "### 段落 2 -" "$CUR_BODY"
|
|
437
|
+
assert_contains "second segment content" "actually make it green" "$CUR_BODY"
|
|
438
|
+
|
|
439
|
+
# --- 13: .awaiting-reply round is stale -> dropped, normal new Round opens -
|
|
440
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open" "$DEVLOG_DIR/.turn-start" "$DEVLOG_DIR/.round-current.md"
|
|
441
|
+
cat > "$DEVLOG_DIR/devlog.md" <<'EOF'
|
|
442
|
+
## Round 1 — 2026-09-09T12:00:00+08:00
|
|
443
|
+
|
|
444
|
+
### Summary
|
|
445
|
+
done
|
|
446
|
+
|
|
447
|
+
### Reply
|
|
448
|
+
fixture reply.
|
|
449
|
+
|
|
450
|
+
### Handoff
|
|
451
|
+
#### 現況
|
|
452
|
+
done
|
|
453
|
+
|
|
454
|
+
### Status
|
|
455
|
+
DONE
|
|
456
|
+
EOF
|
|
457
|
+
printf '%s\n' '{"round": 99, "opened_at": "2026-09-09T12:00:00+08:00"}' > "$DEVLOG_DIR/.awaiting-reply"
|
|
458
|
+
printf '%s' '{"prompt":"unrelated new request"}' | bash "$SCRIPT_DIR/round-start.sh"
|
|
459
|
+
CUR_BODY="$(cat "$DEVLOG_DIR/.round-current.md" 2>/dev/null || echo '')"
|
|
460
|
+
assert_contains "stale marker -> normal new Round 2 opened" "## Round 2 —" "$CUR_BODY"
|
|
461
|
+
assert_not_contains "stale marker -> no segment inserted" "### 段落" "$CUR_BODY"
|
|
462
|
+
if [ -f "$DEVLOG_DIR/.awaiting-reply" ]; then
|
|
463
|
+
echo "FAIL: stale .awaiting-reply should still be consumed"
|
|
464
|
+
FAIL=1
|
|
465
|
+
else
|
|
466
|
+
echo "PASS: stale .awaiting-reply consumed without folding"
|
|
467
|
+
fi
|
|
468
|
+
|
|
469
|
+
# --- 14: valid Span Mode suppresses the fold and just consumes the marker --
|
|
470
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open" "$DEVLOG_DIR/.turn-start"
|
|
471
|
+
cat > "$DEVLOG_DIR/devlog.md" <<'EOF'
|
|
472
|
+
## Round 9 — 2026-09-09T12:00:00+08:00
|
|
473
|
+
|
|
474
|
+
### Summary
|
|
475
|
+
in progress
|
|
476
|
+
|
|
477
|
+
### Reply
|
|
478
|
+
fixture reply.
|
|
479
|
+
|
|
480
|
+
### Handoff
|
|
481
|
+
#### 現況
|
|
482
|
+
running
|
|
483
|
+
#### 完成條件
|
|
484
|
+
`bash hooks/scripts/tests/test-enforce-devlog.sh` 相關情境通過。
|
|
485
|
+
#### 下一步
|
|
486
|
+
keep going
|
|
487
|
+
|
|
488
|
+
### Status
|
|
489
|
+
IN_PROGRESS
|
|
490
|
+
EOF
|
|
491
|
+
cat > "$DEVLOG_DIR/.span-open" <<'SPANEOF'
|
|
492
|
+
{
|
|
493
|
+
"round": 9,
|
|
494
|
+
"opened_at": "2026-09-09T00:00:00+08:00",
|
|
495
|
+
"ticks_since_checkin": 0,
|
|
496
|
+
"max_silent_ticks": 5
|
|
497
|
+
}
|
|
498
|
+
SPANEOF
|
|
499
|
+
printf '%s\n' '{"round": 9, "opened_at": "2026-09-09T12:00:00+08:00"}' > "$DEVLOG_DIR/.awaiting-reply"
|
|
500
|
+
printf '%s' '{"prompt":"automated tick"}' | bash "$SCRIPT_DIR/round-start.sh"
|
|
501
|
+
BODY="$(cat "$DEVLOG_DIR/devlog.md")"
|
|
502
|
+
assert_not_contains "span suppresses fold: no segment" "### 段落" "$BODY"
|
|
503
|
+
UNFENCED_ROUNDS="$(awk '/^[ \t]*```/{f=!f} !f && /^## Round /{c++} END{print c+0}' "$DEVLOG_DIR/devlog.md")"
|
|
504
|
+
if [ "$UNFENCED_ROUNDS" = "1" ]; then
|
|
505
|
+
echo "PASS: span suppresses fold: no new Round either"
|
|
506
|
+
else
|
|
507
|
+
echo "FAIL: expected 1 unfenced Round heading, got $UNFENCED_ROUNDS"
|
|
508
|
+
FAIL=1
|
|
509
|
+
fi
|
|
510
|
+
if [ -f "$DEVLOG_DIR/.awaiting-reply" ]; then
|
|
511
|
+
echo "FAIL: .awaiting-reply should be consumed even when span suppresses the fold"
|
|
512
|
+
FAIL=1
|
|
513
|
+
else
|
|
514
|
+
echo "PASS: .awaiting-reply consumed under an active span"
|
|
515
|
+
fi
|
|
516
|
+
rm -f "$DEVLOG_DIR/.span-open"
|
|
517
|
+
|
|
518
|
+
# --- 15: fold does not increment rounds_since_checkpoint --------------------
|
|
519
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open" "$DEVLOG_DIR/.turn-start"
|
|
520
|
+
cat > "$DEVLOG_DIR/devlog.md" <<'EOF'
|
|
521
|
+
## Round 1 — 2026-09-09T12:00:00+08:00
|
|
522
|
+
|
|
523
|
+
### Summary
|
|
524
|
+
waiting
|
|
525
|
+
|
|
526
|
+
### Reply
|
|
527
|
+
fixture reply.
|
|
528
|
+
|
|
529
|
+
### Handoff
|
|
530
|
+
#### 現況
|
|
531
|
+
waiting on reply
|
|
532
|
+
#### 完成條件
|
|
533
|
+
缺的外部輸入已出現,且可觀察條件達成。
|
|
534
|
+
#### 下一步
|
|
535
|
+
apply the answer
|
|
536
|
+
|
|
537
|
+
### Status
|
|
538
|
+
BLOCKED
|
|
539
|
+
EOF
|
|
540
|
+
printf '%s\n' '{"rounds_since_checkpoint": 3, "max_silent_rounds": 20, "checkpoint_marker_count": 0}' > "$DEVLOG_DIR/.checkpoint-state"
|
|
541
|
+
printf '%s\n' '{"round": 1, "opened_at": "2026-09-09T12:00:00+08:00"}' > "$DEVLOG_DIR/.awaiting-reply"
|
|
542
|
+
printf '%s' '{"prompt":"the reply"}' | bash "$SCRIPT_DIR/round-start.sh"
|
|
543
|
+
CP="$(grep -o '"rounds_since_checkpoint"[[:space:]]*:[[:space:]]*[0-9]\+' "$DEVLOG_DIR/.checkpoint-state" | grep -o '[0-9]\+$')"
|
|
544
|
+
if [ "$CP" = "3" ]; then
|
|
545
|
+
echo "PASS: fold does not increment rounds_since_checkpoint"
|
|
546
|
+
else
|
|
547
|
+
echo "FAIL: expected rounds_since_checkpoint to stay 3, got $CP"
|
|
548
|
+
FAIL=1
|
|
549
|
+
fi
|
|
550
|
+
rm -f "$DEVLOG_DIR/.checkpoint-state"
|
|
551
|
+
|
|
552
|
+
# --- 16: task-notification prompt folds into the last Round, condensed -----
|
|
553
|
+
NOTIF_PROMPT='<task-notification><task-id>t1</task-id><status>completed</status><summary>Agent \"Fix wave\" finished</summary></task-notification>'
|
|
554
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open" "$DEVLOG_DIR/.turn-start" "$DEVLOG_DIR/.checkpoint-state" "$DEVLOG_DIR/.round-current.md"
|
|
555
|
+
cat > "$DEVLOG_DIR/devlog.md" <<'EOF'
|
|
556
|
+
## Round 7 — 2026-09-09T12:00:00+08:00
|
|
557
|
+
|
|
558
|
+
### User Input
|
|
559
|
+
```text
|
|
560
|
+
refactor the widget module
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### Summary
|
|
564
|
+
Refactored the widget module.
|
|
565
|
+
|
|
566
|
+
### Reply
|
|
567
|
+
fixture reply.
|
|
568
|
+
|
|
569
|
+
### Handoff
|
|
570
|
+
#### 現況
|
|
571
|
+
Done, tests pass.
|
|
572
|
+
|
|
573
|
+
### Status
|
|
574
|
+
DONE
|
|
575
|
+
EOF
|
|
576
|
+
printf '%s\n' '{"rounds_since_checkpoint": 0, "max_silent_rounds": 20, "checkpoint_marker_count": 0}' > "$DEVLOG_DIR/.checkpoint-state"
|
|
577
|
+
printf '{"prompt":"%s"}' "$NOTIF_PROMPT" | bash "$SCRIPT_DIR/round-start.sh"
|
|
578
|
+
CUR_BODY="$(cat "$DEVLOG_DIR/.round-current.md" 2>/dev/null || echo '')"
|
|
579
|
+
UNFENCED_ROUNDS="$(awk '/^[ \t]*```/{f=!f} !f && /^## Round /{c++} END{print c+0}' "$DEVLOG_DIR/.round-current.md")"
|
|
580
|
+
if [ "$UNFENCED_ROUNDS" = "1" ]; then
|
|
581
|
+
echo "PASS: task-notification did not open a second Round"
|
|
582
|
+
else
|
|
583
|
+
echo "FAIL: expected 1 unfenced Round heading, got $UNFENCED_ROUNDS"
|
|
584
|
+
FAIL=1
|
|
585
|
+
fi
|
|
586
|
+
assert_contains "task-notification segment heading" "### 段落 1 -" "$CUR_BODY"
|
|
587
|
+
assert_contains "task-notification segment marked as background" "(背景任務通知)" "$CUR_BODY"
|
|
588
|
+
assert_contains "task-notification segment keeps the condensed summary" 'Agent "Fix wave" finished' "$CUR_BODY"
|
|
589
|
+
assert_contains "task-notification segment keeps status" "status=completed" "$CUR_BODY"
|
|
590
|
+
assert_contains "task-notification segment keeps task-id" "task-id=t1" "$CUR_BODY"
|
|
591
|
+
assert_not_contains "task-notification raw tag not recorded" "<task-notification>" "$CUR_BODY"
|
|
592
|
+
SEG_LINE="$(grep -n '### 段落 1' "$DEVLOG_DIR/.round-current.md" | head -1 | cut -d: -f1)"
|
|
593
|
+
SUM_LINE="$(grep -n '^### Summary$' "$DEVLOG_DIR/.round-current.md" | head -1 | cut -d: -f1)"
|
|
594
|
+
if [ "$SEG_LINE" -lt "$SUM_LINE" ]; then
|
|
595
|
+
echo "PASS: task-notification segment inserted before Summary"
|
|
596
|
+
else
|
|
597
|
+
echo "FAIL: task-notification segment landed after Summary (seg=$SEG_LINE summary=$SUM_LINE)"
|
|
598
|
+
FAIL=1
|
|
599
|
+
fi
|
|
600
|
+
OPEN_N="$(grep -o '"round"[[:space:]]*:[[:space:]]*[0-9]\+' "$DEVLOG_DIR/.round-open" | grep -o '[0-9]\+$')"
|
|
601
|
+
if [ "$OPEN_N" = "7" ]; then
|
|
602
|
+
echo "PASS: .round-open reopened to the folded Round (7)"
|
|
603
|
+
else
|
|
604
|
+
echo "FAIL: expected .round-open round=7, got $OPEN_N"
|
|
605
|
+
FAIL=1
|
|
606
|
+
fi
|
|
607
|
+
CP="$(grep -o '"rounds_since_checkpoint"[[:space:]]*:[[:space:]]*[0-9]\+' "$DEVLOG_DIR/.checkpoint-state" | grep -o '[0-9]\+$')"
|
|
608
|
+
if [ "$CP" = "0" ]; then
|
|
609
|
+
echo "PASS: task-notification fold does not increment rounds_since_checkpoint"
|
|
610
|
+
else
|
|
611
|
+
echo "FAIL: expected rounds_since_checkpoint to stay 0, got $CP"
|
|
612
|
+
FAIL=1
|
|
613
|
+
fi
|
|
614
|
+
rm -f "$DEVLOG_DIR/.checkpoint-state"
|
|
615
|
+
|
|
616
|
+
# --- 17: task-notification with no existing Round opens one, still condensed
|
|
617
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open" "$DEVLOG_DIR/.turn-start" "$DEVLOG_DIR/.round-current.md"
|
|
618
|
+
printf '{"prompt":"%s"}' "$NOTIF_PROMPT" | bash "$SCRIPT_DIR/round-start.sh"
|
|
619
|
+
CUR_BODY="$(cat "$DEVLOG_DIR/.round-current.md" 2>/dev/null || echo '')"
|
|
620
|
+
assert_contains "fresh task-notification opens Round 1" "## Round 1 —" "$CUR_BODY"
|
|
621
|
+
assert_contains "fresh task-notification condensed summary" 'Agent "Fix wave" finished' "$CUR_BODY"
|
|
622
|
+
assert_not_contains "fresh task-notification raw tag not recorded" "<task-notification>" "$CUR_BODY"
|
|
623
|
+
|
|
624
|
+
# --- 18: task-notification during an open span is still suppressed ---------
|
|
625
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open" "$DEVLOG_DIR/.turn-start"
|
|
626
|
+
printf '%s\n' '{"round": 1, "opened_at": "2026-09-09T12:00:00+08:00", "ticks_since_checkin": 0, "max_silent_ticks": 5}' > "$DEVLOG_DIR/.span-open"
|
|
627
|
+
printf '{"prompt":"%s"}' "$NOTIF_PROMPT" | bash "$SCRIPT_DIR/round-start.sh"
|
|
628
|
+
if [ -f "$DEVLOG_DIR/devlog.md" ]; then
|
|
629
|
+
echo "FAIL: task-notification during open span should not create devlog.md"
|
|
630
|
+
FAIL=1
|
|
631
|
+
else
|
|
632
|
+
echo "PASS: task-notification during open span created no devlog.md"
|
|
633
|
+
fi
|
|
634
|
+
assert_file_absent "task-notification during open span: no .round-open" "$DEVLOG_DIR/.round-open"
|
|
635
|
+
rm -f "$DEVLOG_DIR/.span-open"
|
|
636
|
+
|
|
637
|
+
# --- workspace claim on next prompt ----------------------------------------
|
|
638
|
+
WS="$TMP_ROOT/ws"
|
|
639
|
+
mkdir -p "$WS/.devlog"
|
|
640
|
+
export CLAUDE_PROJECT_DIR="$WS"
|
|
641
|
+
touch "$WS/.devlog/.enabled"
|
|
642
|
+
git -C "$WS" init -q -b main
|
|
643
|
+
git -C "$WS" config user.email test@example.com
|
|
644
|
+
git -C "$WS" config user.name test
|
|
645
|
+
echo hello > "$WS/a.txt"
|
|
646
|
+
git -C "$WS" add a.txt
|
|
647
|
+
git -C "$WS" commit -q -m init
|
|
648
|
+
LIVE="$(bash "$SCRIPT_DIR/workspace-snapshot.sh" "$WS")"
|
|
649
|
+
cat > "$WS/.devlog/devlog.md" <<EOF
|
|
650
|
+
## Round 1 — 2026-09-11T00:00:00+08:00
|
|
651
|
+
|
|
652
|
+
### Summary
|
|
653
|
+
s
|
|
654
|
+
|
|
655
|
+
### Reply
|
|
656
|
+
fixture reply.
|
|
657
|
+
|
|
658
|
+
### Handoff
|
|
659
|
+
#### 工作區
|
|
660
|
+
${LIVE}
|
|
661
|
+
#### 現況
|
|
662
|
+
going
|
|
663
|
+
#### 完成條件
|
|
664
|
+
`bash hooks/scripts/tests/test-enforce-devlog.sh` 相關情境通過。
|
|
665
|
+
#### 下一步
|
|
666
|
+
do x
|
|
667
|
+
|
|
668
|
+
### Status
|
|
669
|
+
IN_PROGRESS
|
|
670
|
+
EOF
|
|
671
|
+
OUT="$(printf '%s' '{"prompt":"keep going"}' | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
|
|
672
|
+
[ -z "$OUT" ] && echo "PASS: matching 工作區 prints nothing" || { echo "FAIL: match stdout [$OUT]"; FAIL=1; }
|
|
673
|
+
[ ! -f "$WS/.devlog/.workspace-mismatch" ] && echo "PASS: matching 工作區 writes no marker" || { echo "FAIL: marker on match"; FAIL=1; }
|
|
674
|
+
grep -q '^## Round 2' "$WS/.devlog/.round-current.md" && echo "PASS: still opened Round 2" || { echo "FAIL: no round 2"; FAIL=1; }
|
|
675
|
+
|
|
676
|
+
rm -f "$WS/.devlog/.round-open" "$WS/.devlog/.round-current.md"
|
|
677
|
+
cat > "$WS/.devlog/devlog.md" <<EOF
|
|
678
|
+
## Round 1 — 2026-09-11T00:00:00+08:00
|
|
679
|
+
|
|
680
|
+
### Summary
|
|
681
|
+
s
|
|
682
|
+
|
|
683
|
+
### Reply
|
|
684
|
+
fixture reply.
|
|
685
|
+
|
|
686
|
+
### Handoff
|
|
687
|
+
#### 工作區
|
|
688
|
+
main @ deadbeef,工作樹乾淨
|
|
689
|
+
#### 現況
|
|
690
|
+
going
|
|
691
|
+
#### 完成條件
|
|
692
|
+
`bash hooks/scripts/tests/test-enforce-devlog.sh` 相關情境通過。
|
|
693
|
+
#### 下一步
|
|
694
|
+
do x
|
|
695
|
+
|
|
696
|
+
### Status
|
|
697
|
+
IN_PROGRESS
|
|
698
|
+
EOF
|
|
699
|
+
OUT="$(printf '%s' '{"prompt":"keep going"}' | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
|
|
700
|
+
assert_contains "mismatch stdout names 工作區" "#### 工作區" "$OUT"
|
|
701
|
+
assert_contains "mismatch stdout has 宣稱" "宣稱:" "$OUT"
|
|
702
|
+
assert_contains "mismatch stdout has 實際" "實際:" "$OUT"
|
|
703
|
+
assert_contains "mismatch stdout has live snapshot" "$LIVE" "$OUT"
|
|
704
|
+
[ -f "$WS/.devlog/.workspace-mismatch" ] && echo "PASS: mismatch writes marker" || { echo "FAIL: no marker"; FAIL=1; }
|
|
705
|
+
MARKER="$(cat "$WS/.devlog/.workspace-mismatch")"
|
|
706
|
+
[ "$MARKER" = "$LIVE" ] && echo "PASS: marker is live snapshot" || { echo "FAIL: marker [$MARKER]"; FAIL=1; }
|
|
707
|
+
|
|
708
|
+
# --- lessons drift counter: mechanical nudge on repeated mismatch --------
|
|
709
|
+
rm -f "$WS/.devlog/.round-open" "$WS/.devlog/.round-current.md" "$WS/.devlog/.workspace-mismatch"
|
|
710
|
+
touch "$WS/.devlog/.lessons-enabled"
|
|
711
|
+
printf '%s\n' '{"mismatch_count": 0, "threshold": 3}' > "$WS/.devlog/.lessons-drift-state"
|
|
712
|
+
|
|
713
|
+
OUT="$(printf '%s' '{"prompt":"keep going"}' | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
|
|
714
|
+
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; }
|
|
716
|
+
|
|
717
|
+
rm -f "$WS/.devlog/.round-open" "$WS/.devlog/.round-current.md"
|
|
718
|
+
OUT="$(printf '%s' '{"prompt":"keep going"}' | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
|
|
719
|
+
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; }
|
|
721
|
+
|
|
722
|
+
rm -f "$WS/.devlog/.round-open" "$WS/.devlog/.round-current.md"
|
|
723
|
+
OUT="$(printf '%s' '{"prompt":"keep going"}' | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
|
|
724
|
+
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; }
|
|
726
|
+
|
|
727
|
+
# without .lessons-enabled: no counting, no nudge, state file untouched
|
|
728
|
+
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"
|
|
730
|
+
OUT="$(printf '%s' '{"prompt":"keep going"}' | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
|
|
731
|
+
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"
|
|
734
|
+
|
|
735
|
+
# --- lessons drift counter: upgrade path — .lessons-enabled exists (from
|
|
736
|
+
# before this branch) but .lessons-drift-state was never created. round-start.sh
|
|
737
|
+
# must create it with defaults and still act on this same invocation's
|
|
738
|
+
# 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"
|
|
740
|
+
touch "$WS/.devlog/.lessons-enabled"
|
|
741
|
+
cat > "$WS/.devlog/devlog.md" <<EOF
|
|
742
|
+
## Round 1 — 2026-09-11T00:00:00+08:00
|
|
743
|
+
|
|
744
|
+
### Summary
|
|
745
|
+
s
|
|
746
|
+
|
|
747
|
+
### Reply
|
|
748
|
+
fixture reply.
|
|
749
|
+
|
|
750
|
+
### Handoff
|
|
751
|
+
#### 工作區
|
|
752
|
+
main @ deadbeef,工作樹乾淨
|
|
753
|
+
#### 現況
|
|
754
|
+
going
|
|
755
|
+
#### 完成條件
|
|
756
|
+
`bash hooks/scripts/tests/test-enforce-devlog.sh` 相關情境通過。
|
|
757
|
+
#### 下一步
|
|
758
|
+
do x
|
|
759
|
+
|
|
760
|
+
### Status
|
|
761
|
+
IN_PROGRESS
|
|
762
|
+
EOF
|
|
763
|
+
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; }
|
|
767
|
+
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"
|
|
769
|
+
|
|
770
|
+
cat > "$WS/.devlog/devlog.md" <<EOF
|
|
771
|
+
## Round 1 — 2026-09-11T00:00:00+08:00
|
|
772
|
+
|
|
773
|
+
### Summary
|
|
774
|
+
s
|
|
775
|
+
|
|
776
|
+
### Reply
|
|
777
|
+
fixture reply.
|
|
778
|
+
|
|
779
|
+
### Handoff
|
|
780
|
+
#### 工作區
|
|
781
|
+
main @ deadbeef,工作樹乾淨
|
|
782
|
+
|
|
783
|
+
### Status
|
|
784
|
+
DONE
|
|
785
|
+
EOF
|
|
786
|
+
rm -f "$WS/.devlog/.workspace-mismatch"
|
|
787
|
+
OUT="$(printf '%s' '{"prompt":"new topic"}' | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
|
|
788
|
+
[ -z "$OUT" ] && echo "PASS: DONE prints nothing" || { echo "FAIL: DONE stdout [$OUT]"; FAIL=1; }
|
|
789
|
+
[ ! -f "$WS/.devlog/.workspace-mismatch" ] && echo "PASS: DONE writes no marker" || { echo "FAIL: DONE marker"; FAIL=1; }
|
|
790
|
+
|
|
791
|
+
cat > "$WS/.devlog/devlog.md" <<EOF
|
|
792
|
+
## Round 1 — 2026-09-11T00:00:00+08:00
|
|
793
|
+
|
|
794
|
+
### Summary
|
|
795
|
+
s
|
|
796
|
+
|
|
797
|
+
### Reply
|
|
798
|
+
fixture reply.
|
|
799
|
+
|
|
800
|
+
### Handoff
|
|
801
|
+
#### 工作區
|
|
802
|
+
main @ deadbeef,工作樹乾淨
|
|
803
|
+
#### 現況
|
|
804
|
+
going
|
|
805
|
+
#### 完成條件
|
|
806
|
+
`bash hooks/scripts/tests/test-enforce-devlog.sh` 相關情境通過。
|
|
807
|
+
#### 下一步
|
|
808
|
+
do x
|
|
809
|
+
|
|
810
|
+
### Status
|
|
811
|
+
IN_PROGRESS
|
|
812
|
+
EOF
|
|
813
|
+
printf '%s\n' '{"round": 1, "ticks_since_checkin": 0, "max_silent_ticks": 5}' > "$WS/.devlog/.span-open"
|
|
814
|
+
rm -f "$WS/.devlog/.workspace-mismatch"
|
|
815
|
+
printf '%s' '{"prompt":"tick"}' | bash "$SCRIPT_DIR/round-start.sh" >/dev/null
|
|
816
|
+
[ ! -f "$WS/.devlog/.workspace-mismatch" ] && echo "PASS: span skip writes no marker" || { echo "FAIL: span marker"; FAIL=1; }
|
|
817
|
+
rm -f "$WS/.devlog/.span-open"
|
|
818
|
+
|
|
819
|
+
if [ "$FAIL" -eq 0 ]; then
|
|
820
|
+
echo "All checks passed."
|
|
821
|
+
exit 0
|
|
822
|
+
else
|
|
823
|
+
echo "Some checks FAILED."
|
|
824
|
+
exit 1
|
|
825
|
+
fi
|