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,233 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Self-check for enforce-devlog.sh's #### 檔案 machine-verify (Phase 4,
|
|
3
|
+
# docs/design/files-verify.md). Separate file from test-enforce-devlog.sh
|
|
4
|
+
# and test-enforce-devlog-workspace.sh; still auto-discovered by
|
|
5
|
+
# run-tests.sh's tests/test-*.sh glob.
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
9
|
+
TMP_ROOT="$(mktemp -d)"
|
|
10
|
+
trap 'rm -rf "$TMP_ROOT"' EXIT
|
|
11
|
+
|
|
12
|
+
export CLAUDE_PROJECT_DIR="$TMP_ROOT"
|
|
13
|
+
DEVLOG_DIR="$TMP_ROOT/.devlog"
|
|
14
|
+
mkdir -p "$DEVLOG_DIR"
|
|
15
|
+
touch "$DEVLOG_DIR/.enabled"
|
|
16
|
+
|
|
17
|
+
git -C "$TMP_ROOT" init -q -b main
|
|
18
|
+
git -C "$TMP_ROOT" config user.email test@example.com
|
|
19
|
+
git -C "$TMP_ROOT" config user.name test
|
|
20
|
+
echo '.devlog/' > "$TMP_ROOT/.gitignore"
|
|
21
|
+
echo one > "$TMP_ROOT/a.txt"
|
|
22
|
+
git -C "$TMP_ROOT" add .gitignore a.txt
|
|
23
|
+
git -C "$TMP_ROOT" commit -q -m init
|
|
24
|
+
|
|
25
|
+
FAIL=0
|
|
26
|
+
assert_exit() {
|
|
27
|
+
local desc="$1" expected="$2" actual="$3"
|
|
28
|
+
if [ "$actual" -eq "$expected" ]; then
|
|
29
|
+
echo "PASS: $desc"
|
|
30
|
+
else
|
|
31
|
+
echo "FAIL: $desc (expected exit $expected, got $actual)"
|
|
32
|
+
FAIL=1
|
|
33
|
+
fi
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Round-current split: enforce-devlog.sh validates .round-current.md now
|
|
37
|
+
# (not devlog.md), and merges it into devlog.md on success. One assertion
|
|
38
|
+
# per "should succeed" case exercises that merge.
|
|
39
|
+
assert_round_merged() {
|
|
40
|
+
local desc="$1" marker="${2:-fixture}"
|
|
41
|
+
if [ -f "$DEVLOG_DIR/.round-current.md" ]; then
|
|
42
|
+
echo "FAIL: $desc (.round-current.md should be merged away)"
|
|
43
|
+
FAIL=1
|
|
44
|
+
elif ! grep -q "$marker" "$DEVLOG_DIR/devlog.md" 2>/dev/null; then
|
|
45
|
+
echo "FAIL: $desc (devlog.md missing merged content)"
|
|
46
|
+
FAIL=1
|
|
47
|
+
else
|
|
48
|
+
echo "PASS: $desc (round-current merged into devlog.md)"
|
|
49
|
+
fi
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# write_round: $1 = workspace body, $2 = files body (both may be empty to
|
|
53
|
+
# omit the section). Status is always IN_PROGRESS unless $3 overrides it.
|
|
54
|
+
# Writes into .round-current.md — the file enforce-devlog.sh now validates
|
|
55
|
+
# (round-start.sh already opened a skeleton there; this overwrites it).
|
|
56
|
+
write_round() {
|
|
57
|
+
local ws="$1" files="$2" status="${3:-IN_PROGRESS}"
|
|
58
|
+
{
|
|
59
|
+
echo "## Round 1 — 2026-09-11T00:00:00+08:00"
|
|
60
|
+
echo ""
|
|
61
|
+
echo "### Summary"
|
|
62
|
+
echo "fixture"
|
|
63
|
+
echo ""
|
|
64
|
+
echo "### Reply"
|
|
65
|
+
echo "fixture reply."
|
|
66
|
+
echo ""
|
|
67
|
+
echo "### Handoff"
|
|
68
|
+
if [ -n "$files" ]; then
|
|
69
|
+
echo "#### 檔案"
|
|
70
|
+
printf '%s\n' "$files"
|
|
71
|
+
fi
|
|
72
|
+
if [ -n "$ws" ]; then
|
|
73
|
+
echo "#### 工作區"
|
|
74
|
+
printf '%s\n' "$ws"
|
|
75
|
+
fi
|
|
76
|
+
echo "#### 現況"
|
|
77
|
+
echo "fixture"
|
|
78
|
+
if [ "$status" != "DONE" ]; then
|
|
79
|
+
echo "#### 完成條件"
|
|
80
|
+
echo "observable done via test."
|
|
81
|
+
echo "#### 下一步"
|
|
82
|
+
echo "edit hooks/scripts/enforce-devlog.sh"
|
|
83
|
+
fi
|
|
84
|
+
echo ""
|
|
85
|
+
echo "### Status"
|
|
86
|
+
echo "$status"
|
|
87
|
+
} > "$DEVLOG_DIR/.round-current.md"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
ws_clean() {
|
|
91
|
+
local hash
|
|
92
|
+
hash="$(git -C "$TMP_ROOT" rev-parse --short HEAD)"
|
|
93
|
+
echo "main @ ${hash},工作樹乾淨"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# --- committed block, exact match -> allowed ---------------------------------
|
|
97
|
+
echo two > "$TMP_ROOT/b.txt"
|
|
98
|
+
git -C "$TMP_ROOT" add b.txt
|
|
99
|
+
git -C "$TMP_ROOT" commit -q -m "add b"
|
|
100
|
+
HASH="$(git -C "$TMP_ROOT" rev-parse --short HEAD)"
|
|
101
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
102
|
+
write_round "$(ws_clean)" "commit ${HASH}:
|
|
103
|
+
新增:b.txt" "DONE"
|
|
104
|
+
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
105
|
+
assert_exit "commit block matches diff-tree -> allowed" 0 $?
|
|
106
|
+
assert_round_merged "commit block matches diff-tree"
|
|
107
|
+
|
|
108
|
+
# --- committed block, wrong category -> blocked, message shows expected -----
|
|
109
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
110
|
+
write_round "$(ws_clean)" "commit ${HASH}:
|
|
111
|
+
修改:b.txt" "DONE"
|
|
112
|
+
MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
|
|
113
|
+
assert_exit "commit block wrong category -> blocked" 2 $?
|
|
114
|
+
case "$MSG" in
|
|
115
|
+
*"新增:b.txt"*) echo "PASS: message includes the correct category line" ;;
|
|
116
|
+
*) echo "FAIL: message should include '新增:b.txt', got: $MSG"; FAIL=1 ;;
|
|
117
|
+
esac
|
|
118
|
+
|
|
119
|
+
# --- unresolvable commit hash -> fail-open, allowed --------------------------
|
|
120
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
121
|
+
write_round "$(ws_clean)" "commit 0000000:
|
|
122
|
+
新增:nonexistent.txt" "DONE"
|
|
123
|
+
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
124
|
+
assert_exit "unresolvable commit hash -> fail-open, allowed" 0 $?
|
|
125
|
+
assert_round_merged "unresolvable commit hash"
|
|
126
|
+
|
|
127
|
+
# --- 尚未 commit, claimed path really dirty -> allowed ------------------------
|
|
128
|
+
echo change >> "$TMP_ROOT/a.txt"
|
|
129
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
130
|
+
write_round "main @ ${HASH}
|
|
131
|
+
未提交:a.txt" "尚未 commit:
|
|
132
|
+
修改:a.txt" "IN_PROGRESS"
|
|
133
|
+
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
134
|
+
assert_exit "uncommitted claim matches actual dirty file -> allowed" 0 $?
|
|
135
|
+
assert_round_merged "uncommitted claim matches actual dirty file"
|
|
136
|
+
|
|
137
|
+
# --- 尚未 commit, fabricated path -> blocked ----------------------------------
|
|
138
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
139
|
+
write_round "main @ ${HASH}
|
|
140
|
+
未提交:a.txt" "尚未 commit:
|
|
141
|
+
修改:nope.txt" "IN_PROGRESS"
|
|
142
|
+
MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
|
|
143
|
+
assert_exit "uncommitted claim on a path that isn't dirty -> blocked" 2 $?
|
|
144
|
+
case "$MSG" in
|
|
145
|
+
*"nope.txt"*) echo "PASS: message names the fabricated path" ;;
|
|
146
|
+
*) echo "FAIL: message should name nope.txt, got: $MSG"; FAIL=1 ;;
|
|
147
|
+
esac
|
|
148
|
+
|
|
149
|
+
# --- 尚未 commit, extra unclaimed dirty residue -> still allowed (one-directional) --
|
|
150
|
+
echo untouched-by-this-round > "$TMP_ROOT/residue.txt"
|
|
151
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
152
|
+
write_round "main @ ${HASH}
|
|
153
|
+
未提交:a.txt, residue.txt" "尚未 commit:
|
|
154
|
+
修改:a.txt" "IN_PROGRESS"
|
|
155
|
+
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
156
|
+
assert_exit "unclaimed extra dirty file (residue) -> still allowed" 0 $?
|
|
157
|
+
assert_round_merged "unclaimed extra dirty file (residue)"
|
|
158
|
+
rm -f "$TMP_ROOT/residue.txt"
|
|
159
|
+
git -C "$TMP_ROOT" checkout -q -- a.txt
|
|
160
|
+
|
|
161
|
+
# --- 尚未 commit, fabricated path against a genuinely clean tree -> blocked, not
|
|
162
|
+
# a crash. Regression for Finding 1: path_in_list used to build a bash array
|
|
163
|
+
# via `IFS=',' read -ra arr <<< "$haystack"`; on bash 3.2 (this suite's floor)
|
|
164
|
+
# an empty haystack (clean tree) makes that a genuinely empty array, and
|
|
165
|
+
# "${arr[@]}" on it is an unbound-variable error under `set -uo pipefail` —
|
|
166
|
+
# the script died with exit 1 and a raw traceback instead of the intended
|
|
167
|
+
# exit 2 block. A clean tree is exactly when ACTUAL_JOINED is empty, so this
|
|
168
|
+
# is also the headline case the feature exists to catch (a false "still
|
|
169
|
+
# uncommitted" claim).
|
|
170
|
+
git -C "$TMP_ROOT" status --short --no-renames | grep -q . && {
|
|
171
|
+
echo "FIXTURE BUG: tree should be clean before the Finding-1 regression test" >&2
|
|
172
|
+
FAIL=1
|
|
173
|
+
}
|
|
174
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
175
|
+
write_round "$(ws_clean)" "尚未 commit:
|
|
176
|
+
修改:ghost.txt" "DONE"
|
|
177
|
+
MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
|
|
178
|
+
assert_exit "uncommitted claim against a clean tree -> blocked, not a crash" 2 $?
|
|
179
|
+
case "$MSG" in
|
|
180
|
+
*"ghost.txt"*) echo "PASS: message names the fabricated path" ;;
|
|
181
|
+
*) echo "FAIL: message should name ghost.txt, got: $MSG"; FAIL=1 ;;
|
|
182
|
+
esac
|
|
183
|
+
case "$MSG" in
|
|
184
|
+
*"工作樹乾淨"*) echo "PASS: message shows the actual-dirty fallback text" ;;
|
|
185
|
+
*) echo "FAIL: message should show the clean-tree fallback text, got: $MSG"; FAIL=1 ;;
|
|
186
|
+
esac
|
|
187
|
+
|
|
188
|
+
# --- commit resolves but its outside-.devlog/ diff is empty -> must still be
|
|
189
|
+
# compared (not silently fail-open); a fabricated claim against it is blocked.
|
|
190
|
+
# Regression for Finding 2: files_snapshot returns empty stdout both when a
|
|
191
|
+
# hash doesn't resolve and when it resolves but has nothing to report outside
|
|
192
|
+
# .devlog/ — check_commit_block must tell these apart via its own rev-parse,
|
|
193
|
+
# not infer "unresolvable" from empty output. Placed after every test above
|
|
194
|
+
# that hardcodes `$HASH` against the current HEAD, since this test advances
|
|
195
|
+
# HEAD with a new commit.
|
|
196
|
+
echo devlog-only-change > "$TMP_ROOT/.devlog/marker.txt"
|
|
197
|
+
git -C "$TMP_ROOT" add -f .devlog/marker.txt
|
|
198
|
+
git -C "$TMP_ROOT" commit -q -m "devlog-only change"
|
|
199
|
+
DEVLOG_ONLY_HASH="$(git -C "$TMP_ROOT" rev-parse --short HEAD)"
|
|
200
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
201
|
+
write_round "$(ws_clean)" "commit ${DEVLOG_ONLY_HASH}:
|
|
202
|
+
新增:fabricated.ts" "DONE"
|
|
203
|
+
MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
|
|
204
|
+
assert_exit "commit with empty outside-.devlog diff + fabricated claim -> blocked" 2 $?
|
|
205
|
+
case "$MSG" in
|
|
206
|
+
*"跟宣稱不符"*) echo "PASS: message reports the mismatch instead of silently passing" ;;
|
|
207
|
+
*) echo "FAIL: message should report a mismatch, got: $MSG"; FAIL=1 ;;
|
|
208
|
+
esac
|
|
209
|
+
|
|
210
|
+
# --- malformed 檔案 body -> blocked, format-violation message ------------------
|
|
211
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
212
|
+
write_round "$(ws_clean)" "新增了一些東西,忘了寫成規定格式" "DONE"
|
|
213
|
+
MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
|
|
214
|
+
assert_exit "unparseable 檔案 body -> blocked" 2 $?
|
|
215
|
+
case "$MSG" in
|
|
216
|
+
*"格式不對"*) echo "PASS: message explains the format violation" ;;
|
|
217
|
+
*) echo "FAIL: message should explain the format violation, got: $MSG"; FAIL=1 ;;
|
|
218
|
+
esac
|
|
219
|
+
|
|
220
|
+
# --- empty 檔案 section -> unaffected (existing 瑣碎輪 DONE case) ----------------
|
|
221
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
222
|
+
write_round "" "" "DONE"
|
|
223
|
+
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
224
|
+
assert_exit "empty 檔案 section -> unaffected, allowed" 0 $?
|
|
225
|
+
assert_round_merged "empty 檔案 section"
|
|
226
|
+
|
|
227
|
+
if [ "$FAIL" -eq 0 ]; then
|
|
228
|
+
echo "All checks passed."
|
|
229
|
+
exit 0
|
|
230
|
+
else
|
|
231
|
+
echo "Some checks FAILED."
|
|
232
|
+
exit 1
|
|
233
|
+
fi
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Self-check for enforce-devlog.sh's Handoff subsection order/duplicate
|
|
3
|
+
# check (Phase 2, docs/design/devlog-as-ssot-assessment.md). No framework —
|
|
4
|
+
# plain assert-and-exit, matching this repo's existing style.
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
8
|
+
TMP_ROOT="$(mktemp -d)"
|
|
9
|
+
trap 'rm -rf "$TMP_ROOT"' EXIT
|
|
10
|
+
|
|
11
|
+
export CLAUDE_PROJECT_DIR="$TMP_ROOT"
|
|
12
|
+
DEVLOG_DIR="$TMP_ROOT/.devlog"
|
|
13
|
+
mkdir -p "$DEVLOG_DIR"
|
|
14
|
+
touch "$DEVLOG_DIR/.enabled"
|
|
15
|
+
|
|
16
|
+
# A real (if minimal) git repo, only so the "full canonical order" fixture's
|
|
17
|
+
# #### 檔案 + #### 工作區 pair below can carry a #### 工作區 that actually
|
|
18
|
+
# matches live git — DONE rounds with a non-empty #### 檔案 are now
|
|
19
|
+
# machine-verified too (enforce-devlog.sh's workspace check, extended past
|
|
20
|
+
# Phase 1's IN_PROGRESS/BLOCKED). Every other fixture in this file keeps
|
|
21
|
+
# Status DONE with no #### 檔案, so it stays decoupled from this check.
|
|
22
|
+
git -C "$TMP_ROOT" init -q -b main
|
|
23
|
+
git -C "$TMP_ROOT" config user.email test@example.com
|
|
24
|
+
git -C "$TMP_ROOT" config user.name test
|
|
25
|
+
echo '.devlog/' > "$TMP_ROOT/.gitignore"
|
|
26
|
+
git -C "$TMP_ROOT" add .gitignore
|
|
27
|
+
git -C "$TMP_ROOT" commit -q -m init
|
|
28
|
+
HASH="$(git -C "$TMP_ROOT" rev-parse --short HEAD)"
|
|
29
|
+
|
|
30
|
+
FAIL=0
|
|
31
|
+
assert_exit() {
|
|
32
|
+
local desc="$1" expected="$2" actual="$3"
|
|
33
|
+
if [ "$actual" -eq "$expected" ]; then
|
|
34
|
+
echo "PASS: $desc"
|
|
35
|
+
else
|
|
36
|
+
echo "FAIL: $desc (expected exit $expected, got $actual)"
|
|
37
|
+
FAIL=1
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Round-current split: enforce-devlog.sh validates .round-current.md now
|
|
42
|
+
# (not devlog.md), and merges it into devlog.md on success. One assertion
|
|
43
|
+
# per "should succeed" case exercises that merge.
|
|
44
|
+
assert_round_merged() {
|
|
45
|
+
local desc="$1" marker="${2:-fixture}"
|
|
46
|
+
if [ -f "$DEVLOG_DIR/.round-current.md" ]; then
|
|
47
|
+
echo "FAIL: $desc (.round-current.md should be merged away)"
|
|
48
|
+
FAIL=1
|
|
49
|
+
elif ! grep -q "$marker" "$DEVLOG_DIR/devlog.md" 2>/dev/null; then
|
|
50
|
+
echo "FAIL: $desc (devlog.md missing merged content)"
|
|
51
|
+
FAIL=1
|
|
52
|
+
else
|
|
53
|
+
echo "PASS: $desc (round-current merged into devlog.md)"
|
|
54
|
+
fi
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
write_round() {
|
|
58
|
+
# $1 = Handoff body (already includes #### subsection lines). Status DONE
|
|
59
|
+
# throughout so this suite is decoupled from Phase 1's 工作區/下一步 checks.
|
|
60
|
+
# Writes into .round-current.md — the file enforce-devlog.sh now validates
|
|
61
|
+
# (round-start.sh already opened a skeleton there; this overwrites it).
|
|
62
|
+
{
|
|
63
|
+
echo "## Round 1 — 2026-09-10T00:00:00+08:00"
|
|
64
|
+
echo ""
|
|
65
|
+
echo "### Summary"
|
|
66
|
+
echo "fixture"
|
|
67
|
+
echo ""
|
|
68
|
+
echo "### Reply"
|
|
69
|
+
echo "fixture reply."
|
|
70
|
+
echo ""
|
|
71
|
+
echo "### Handoff"
|
|
72
|
+
printf '%s\n' "$1"
|
|
73
|
+
echo ""
|
|
74
|
+
echo "### Status"
|
|
75
|
+
echo "DONE"
|
|
76
|
+
} > "$DEVLOG_DIR/.round-current.md"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# --- full canonical order -> allowed -----------------------------------------
|
|
80
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
81
|
+
write_round "#### 決策
|
|
82
|
+
d
|
|
83
|
+
#### 檔案
|
|
84
|
+
尚未 commit:
|
|
85
|
+
#### 工作區
|
|
86
|
+
main @ ${HASH},工作樹乾淨
|
|
87
|
+
#### 現況
|
|
88
|
+
c
|
|
89
|
+
#### 完成條件
|
|
90
|
+
observable done.
|
|
91
|
+
#### 下一步
|
|
92
|
+
n"
|
|
93
|
+
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
94
|
+
assert_exit "full canonical order -> allowed" 0 $?
|
|
95
|
+
assert_round_merged "full canonical order"
|
|
96
|
+
|
|
97
|
+
# --- subset in order -> allowed -----------------------------------------------
|
|
98
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
99
|
+
write_round "#### 決策
|
|
100
|
+
d
|
|
101
|
+
#### 現況
|
|
102
|
+
c"
|
|
103
|
+
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
104
|
+
assert_exit "subset in canonical order -> allowed" 0 $?
|
|
105
|
+
assert_round_merged "subset in canonical order"
|
|
106
|
+
|
|
107
|
+
# --- reordered -> blocked -------------------------------------------------------
|
|
108
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
109
|
+
write_round "#### 現況
|
|
110
|
+
c
|
|
111
|
+
#### 決策
|
|
112
|
+
d"
|
|
113
|
+
MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
|
|
114
|
+
assert_exit "現況 before 決策 -> blocked" 2 $?
|
|
115
|
+
case "$MSG" in
|
|
116
|
+
*"順序錯了"*) echo "PASS: order-violation message" ;;
|
|
117
|
+
*) echo "FAIL: expected 順序錯了 message, got: $MSG"; FAIL=1 ;;
|
|
118
|
+
esac
|
|
119
|
+
|
|
120
|
+
# --- duplicate -> blocked -------------------------------------------------------
|
|
121
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
122
|
+
write_round "#### 現況
|
|
123
|
+
c1
|
|
124
|
+
#### 現況
|
|
125
|
+
c2"
|
|
126
|
+
MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
|
|
127
|
+
assert_exit "duplicate 現況 -> blocked" 2 $?
|
|
128
|
+
case "$MSG" in
|
|
129
|
+
*"出現超過一次"*) echo "PASS: duplicate-subsection message" ;;
|
|
130
|
+
*) echo "FAIL: expected 出現超過一次 message, got: $MSG"; FAIL=1 ;;
|
|
131
|
+
esac
|
|
132
|
+
|
|
133
|
+
# --- unrecognized heading interleaved -> ignored, allowed ------------------------
|
|
134
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
135
|
+
write_round "#### 決策
|
|
136
|
+
d
|
|
137
|
+
#### 其他備註
|
|
138
|
+
x
|
|
139
|
+
#### 現況
|
|
140
|
+
c"
|
|
141
|
+
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
142
|
+
assert_exit "unrecognized #### heading interleaved -> ignored, allowed" 0 $?
|
|
143
|
+
assert_round_merged "unrecognized #### heading interleaved"
|
|
144
|
+
|
|
145
|
+
# --- fenced example reordering subsection headings -> ignored, allowed -----
|
|
146
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
147
|
+
write_round "#### 決策
|
|
148
|
+
說明格式時可以貼一段範例:
|
|
149
|
+
\`\`\`markdown
|
|
150
|
+
#### 現況
|
|
151
|
+
c
|
|
152
|
+
#### 決策
|
|
153
|
+
d
|
|
154
|
+
\`\`\`
|
|
155
|
+
真正決定
|
|
156
|
+
#### 現況
|
|
157
|
+
c"
|
|
158
|
+
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
159
|
+
assert_exit "fenced example reordering subsection headings -> ignored, allowed" 0 $?
|
|
160
|
+
assert_round_merged "fenced example reordering subsection headings"
|
|
161
|
+
|
|
162
|
+
# --- fenced example duplicating a heading already used -> ignored, allowed -
|
|
163
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
164
|
+
write_round "#### 決策
|
|
165
|
+
真正決策
|
|
166
|
+
說明格式時再貼一次範例:
|
|
167
|
+
\`\`\`markdown
|
|
168
|
+
#### 決策
|
|
169
|
+
d2
|
|
170
|
+
\`\`\`
|
|
171
|
+
#### 現況
|
|
172
|
+
c"
|
|
173
|
+
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
174
|
+
assert_exit "fenced example duplicating a used heading -> ignored, allowed" 0 $?
|
|
175
|
+
assert_round_merged "fenced example duplicating a used heading"
|
|
176
|
+
|
|
177
|
+
# --- unterminated fence inside #### 決策 -> fail-open, not a false block
|
|
178
|
+
# (final-review Fix 1). A ``` fence that never closes (odd fence-marker
|
|
179
|
+
# count) must never make a present #### 現況 register as missing/malformed.
|
|
180
|
+
# Status DONE so 下一步/工作區 (Phase 1) stay out of the picture — this only
|
|
181
|
+
# exercises section_body()/ORDER_ERR's own fence handling.
|
|
182
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
183
|
+
write_round "#### 決策
|
|
184
|
+
一段沒收尾的範例:
|
|
185
|
+
\`\`\`markdown
|
|
186
|
+
沒收尾內容,一路吃到 Handoff 結尾
|
|
187
|
+
#### 現況
|
|
188
|
+
c"
|
|
189
|
+
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
190
|
+
assert_exit "unterminated fence in 決策, real 現況 after it -> fail-open, allowed" 0 $?
|
|
191
|
+
assert_round_merged "unterminated fence in 決策, real 現況 after it"
|
|
192
|
+
|
|
193
|
+
# --- same unterminated fence, but hiding a real duplicate below it --------
|
|
194
|
+
# ORDER_ERR re-scans the extracted Handoff body with its own fence tracking;
|
|
195
|
+
# pre-fix the same stuck-fence bug hid every #### heading after the broken
|
|
196
|
+
# fence from ORDER_ERR too — including a genuine duplicate "#### 決策" that
|
|
197
|
+
# should be rejected. NOFENCE degrades ORDER_ERR back to a plain scan for
|
|
198
|
+
# this round, so the duplicate check reaches it again. (This is the mirror
|
|
199
|
+
# image of the false-block case above: here the pre-fix bug wrongly *allowed*
|
|
200
|
+
# something; the point is the same root cause, fixed the same way.)
|
|
201
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
202
|
+
write_round "#### 決策
|
|
203
|
+
一段沒收尾的範例:
|
|
204
|
+
\`\`\`markdown
|
|
205
|
+
沒收尾內容
|
|
206
|
+
#### 現況
|
|
207
|
+
c
|
|
208
|
+
#### 決策
|
|
209
|
+
d2(應該被判定為重複)"
|
|
210
|
+
MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
|
|
211
|
+
assert_exit "unterminated fence no longer hides a real duplicate -> blocked" 2 $?
|
|
212
|
+
case "$MSG" in
|
|
213
|
+
*"出現超過一次"*) echo "PASS: duplicate-subsection message despite unterminated fence" ;;
|
|
214
|
+
*) echo "FAIL: expected 出現超過一次 message, got: $MSG"; FAIL=1 ;;
|
|
215
|
+
esac
|
|
216
|
+
|
|
217
|
+
if [ "$FAIL" -eq 0 ]; then
|
|
218
|
+
echo "All checks passed."
|
|
219
|
+
exit 0
|
|
220
|
+
else
|
|
221
|
+
echo "Some checks FAILED."
|
|
222
|
+
exit 1
|
|
223
|
+
fi
|