devlog-tracker 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +234 -0
  3. package/bin/devlog-tracker.js +51 -0
  4. package/cli/init.js +55 -0
  5. package/cli/init.test.js +37 -0
  6. package/cli/merge-hooks.js +60 -0
  7. package/cli/merge-hooks.test.js +187 -0
  8. package/cli/platforms/codex.js +13 -0
  9. package/cli/platforms/cursor.js +13 -0
  10. package/cli/status.js +14 -0
  11. package/cli/status.test.js +33 -0
  12. package/cli/vendor.js +37 -0
  13. package/cli/vendor.test.js +84 -0
  14. package/codex/hooks/on-pre-tool.sh +19 -0
  15. package/codex/hooks/on-session-end.sh +31 -0
  16. package/codex/hooks/on-session-start.sh +37 -0
  17. package/codex/hooks/on-stop.sh +18 -0
  18. package/codex/hooks/on-user-prompt-submit.sh +15 -0
  19. package/codex/hooks/project-dir.sh +13 -0
  20. package/codex/hooks/test-adapters.sh +98 -0
  21. package/codex/hooks.json +54 -0
  22. package/commands/checkpoint.md +17 -0
  23. package/commands/clean.md +50 -0
  24. package/commands/compact.md +24 -0
  25. package/commands/continue.md +30 -0
  26. package/commands/keep.md +100 -0
  27. package/commands/lessons-drift.md +19 -0
  28. package/commands/lessons-off.md +16 -0
  29. package/commands/lessons-on.md +20 -0
  30. package/commands/lessons.md +20 -0
  31. package/commands/overview.md +32 -0
  32. package/commands/pause.md +20 -0
  33. package/commands/resume.md +18 -0
  34. package/commands/segment-watch.md +21 -0
  35. package/commands/span.md +13 -0
  36. package/commands/start.md +19 -0
  37. package/commands/status.md +14 -0
  38. package/cursor/hooks/on-pre-tool.sh +24 -0
  39. package/cursor/hooks/on-session-end.sh +11 -0
  40. package/cursor/hooks/on-session-start.sh +15 -0
  41. package/cursor/hooks/on-stop.sh +44 -0
  42. package/cursor/hooks/on-submit-prompt.sh +28 -0
  43. package/cursor/hooks/on-tool-failure.sh +16 -0
  44. package/cursor/hooks/project-dir.sh +10 -0
  45. package/cursor/hooks/test-adapters.sh +201 -0
  46. package/cursor/hooks.json +26 -0
  47. package/hooks/scripts/await-open.sh +23 -0
  48. package/hooks/scripts/checkpoint-set.sh +44 -0
  49. package/hooks/scripts/clean-devlog.sh +69 -0
  50. package/hooks/scripts/close-open-round.sh +186 -0
  51. package/hooks/scripts/compact-devlog.sh +72 -0
  52. package/hooks/scripts/detect-pending-question.sh +31 -0
  53. package/hooks/scripts/devlog-lock.sh +38 -0
  54. package/hooks/scripts/devlog-md.sh +222 -0
  55. package/hooks/scripts/devlog-path.sh +70 -0
  56. package/hooks/scripts/enforce-devlog.sh +629 -0
  57. package/hooks/scripts/files-snapshot.sh +70 -0
  58. package/hooks/scripts/json-field.sh +76 -0
  59. package/hooks/scripts/keep-move.sh +155 -0
  60. package/hooks/scripts/kept-list.sh +40 -0
  61. package/hooks/scripts/lessons-append.sh +93 -0
  62. package/hooks/scripts/lessons-drift-set.sh +42 -0
  63. package/hooks/scripts/lessons-off.sh +15 -0
  64. package/hooks/scripts/lessons-on.sh +21 -0
  65. package/hooks/scripts/lessons-read.sh +60 -0
  66. package/hooks/scripts/on-session-end.sh +14 -0
  67. package/hooks/scripts/on-stop-failure.sh +18 -0
  68. package/hooks/scripts/on-tool-failure.sh +25 -0
  69. package/hooks/scripts/pause-devlog.sh +13 -0
  70. package/hooks/scripts/redact-prompt.sh +19 -0
  71. package/hooks/scripts/resume-devlog.sh +41 -0
  72. package/hooks/scripts/round-start.sh +333 -0
  73. package/hooks/scripts/run-tests.sh +26 -0
  74. package/hooks/scripts/segment-watch-set.sh +47 -0
  75. package/hooks/scripts/segment-watch.sh +197 -0
  76. package/hooks/scripts/session-start-devlog.sh +157 -0
  77. package/hooks/scripts/span-close.sh +9 -0
  78. package/hooks/scripts/span-open.sh +21 -0
  79. package/hooks/scripts/start-devlog.sh +24 -0
  80. package/hooks/scripts/status-devlog.sh +43 -0
  81. package/hooks/scripts/tests/test-await-open.sh +112 -0
  82. package/hooks/scripts/tests/test-branch-scoped-integration.sh +108 -0
  83. package/hooks/scripts/tests/test-checkpoint-set.sh +36 -0
  84. package/hooks/scripts/tests/test-clean-devlog.sh +160 -0
  85. package/hooks/scripts/tests/test-cli-init-e2e.sh +37 -0
  86. package/hooks/scripts/tests/test-close-open-round.sh +325 -0
  87. package/hooks/scripts/tests/test-compact-devlog.sh +62 -0
  88. package/hooks/scripts/tests/test-devlog-lock.sh +37 -0
  89. package/hooks/scripts/tests/test-devlog-md.sh +530 -0
  90. package/hooks/scripts/tests/test-devlog-path.sh +118 -0
  91. package/hooks/scripts/tests/test-enforce-devlog-files.sh +233 -0
  92. package/hooks/scripts/tests/test-enforce-devlog-handoff-order.sh +223 -0
  93. package/hooks/scripts/tests/test-enforce-devlog-workspace.sh +333 -0
  94. package/hooks/scripts/tests/test-enforce-devlog.sh +1290 -0
  95. package/hooks/scripts/tests/test-files-snapshot.sh +142 -0
  96. package/hooks/scripts/tests/test-json-field.sh +57 -0
  97. package/hooks/scripts/tests/test-keep-move.sh +127 -0
  98. package/hooks/scripts/tests/test-kept-list.sh +80 -0
  99. package/hooks/scripts/tests/test-lessons-append.sh +138 -0
  100. package/hooks/scripts/tests/test-lessons-drift-set.sh +36 -0
  101. package/hooks/scripts/tests/test-lessons-on-off.sh +75 -0
  102. package/hooks/scripts/tests/test-lessons-read.sh +97 -0
  103. package/hooks/scripts/tests/test-on-interrupt.sh +186 -0
  104. package/hooks/scripts/tests/test-redact-prompt.sh +27 -0
  105. package/hooks/scripts/tests/test-resume-devlog.sh +35 -0
  106. package/hooks/scripts/tests/test-round-start.sh +825 -0
  107. package/hooks/scripts/tests/test-segment-watch-set.sh +75 -0
  108. package/hooks/scripts/tests/test-segment-watch.sh +468 -0
  109. package/hooks/scripts/tests/test-session-start-devlog.sh +508 -0
  110. package/hooks/scripts/tests/test-start-pause-devlog.sh +84 -0
  111. package/hooks/scripts/tests/test-status-span.sh +50 -0
  112. package/hooks/scripts/tests/test-workspace-snapshot.sh +130 -0
  113. package/hooks/scripts/workspace-snapshot.sh +69 -0
  114. package/package.json +37 -0
  115. package/skills/devlog-tracker/SKILL.md +411 -0
  116. package/skills/devlog-tracker/references/checkpoint-mode.md +42 -0
  117. package/skills/devlog-tracker/references/lessons-mode.md +32 -0
  118. package/skills/devlog-tracker/references/reply-fold.md +129 -0
  119. package/skills/devlog-tracker/references/round-segments.md +55 -0
  120. package/skills/devlog-tracker/references/span-mode.md +65 -0
@@ -0,0 +1,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