devlog-tracker 0.33.5 → 0.34.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/README.md +41 -10
- package/README.zh-TW.md +41 -10
- package/cli/agents-md.js +1 -0
- package/cli/agents-md.test.js +10 -0
- package/cli/platforms/claude.js +1 -0
- package/codex/hooks/on-interrupt.sh +13 -0
- package/codex/hooks/on-pre-tool.sh +69 -1
- package/codex/hooks/on-session-end.sh +5 -5
- package/codex/hooks/on-session-start.sh +5 -6
- package/codex/hooks/on-stop.sh +3 -2
- package/codex/hooks/on-subagent-start.sh +19 -0
- package/codex/hooks/project-dir.sh +16 -1
- package/codex/hooks/test-adapters.sh +127 -9
- package/codex/hooks.json +21 -0
- package/commands/continue.md +3 -3
- package/commands/keep-all.md +1 -1
- package/commands/keep.md +2 -2
- package/commands/lessons-on.md +1 -1
- package/commands/migrate.md +18 -0
- package/commands/pr.md +3 -3
- package/commands/resume.md +1 -1
- package/core/scripts/close-open-round.sh +8 -2
- package/core/scripts/devlog-md.sh +6 -11
- package/core/scripts/enforce-devlog.sh +79 -85
- package/core/scripts/handoff-convert.sh +187 -0
- package/core/scripts/handoff-fields.sh +219 -0
- package/core/scripts/handoff-file.sh +19 -88
- package/core/scripts/lessons-subagent-start.sh +1 -1
- package/core/scripts/migrate-handoff.sh +91 -0
- package/core/scripts/round-start.sh +13 -2
- package/core/scripts/segment-watch.sh +1 -1
- package/core/scripts/tests/lib/xml-fixture.sh +10 -0
- package/core/scripts/tests/test-close-open-round.sh +2 -1
- package/core/scripts/tests/test-devlog-md.sh +21 -0
- package/core/scripts/tests/test-enforce-devlog-files.sh +4 -1
- package/core/scripts/tests/test-enforce-devlog-handoff-order.sh +149 -81
- package/core/scripts/tests/test-enforce-devlog-session-handoff.sh +92 -11
- package/core/scripts/tests/test-enforce-devlog-workspace.sh +42 -26
- package/core/scripts/tests/test-enforce-devlog.sh +70 -0
- package/core/scripts/tests/test-handoff-fields.sh +171 -0
- package/core/scripts/tests/test-handoff-file.sh +67 -45
- package/core/scripts/tests/test-migrate-handoff.sh +278 -0
- package/core/scripts/tests/test-round-start.sh +54 -1
- package/core/scripts/tests/test-session-start-devlog.sh +45 -0
- package/core/scripts/timeline-render.js +26 -2
- package/core/scripts/timeline-render.test.js +23 -1
- package/package.json +3 -3
- package/skills/devlog-tracker/SKILL.md +80 -55
- package/skills/devlog-tracker/references/contract.md +4 -3
- package/skills/devlog-tracker/references/lessons-mode.md +7 -7
- package/skills/devlog-tracker/references/round-segments.md +11 -5
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Self-check for enforce-devlog.sh's Handoff
|
|
3
|
-
#
|
|
2
|
+
# Self-check for enforce-devlog.sh's Handoff format gate: XML tag order/
|
|
3
|
+
# duplicate/unknown-tag checks and legacy-format blocking
|
|
4
|
+
# (docs/design/handoff-xml.md). No framework —
|
|
4
5
|
# plain assert-and-exit, matching this repo's existing style.
|
|
5
6
|
set -uo pipefail
|
|
6
7
|
|
|
@@ -14,11 +15,10 @@ mkdir -p "$DEVLOG_DIR"
|
|
|
14
15
|
touch "$DEVLOG_DIR/.enabled"
|
|
15
16
|
|
|
16
17
|
# A real (if minimal) git repo, only so the "full canonical order" fixture's
|
|
17
|
-
#
|
|
18
|
-
# matches live git — DONE rounds with a non-empty
|
|
19
|
-
# machine-verified too
|
|
20
|
-
#
|
|
21
|
-
# Status DONE with no #### 檔案, so it stays decoupled from this check.
|
|
18
|
+
# <files> + <workspace> pair below can carry a <workspace> that actually
|
|
19
|
+
# matches live git — DONE rounds with a non-empty <files> are
|
|
20
|
+
# machine-verified too. Every other fixture in this file keeps Status DONE
|
|
21
|
+
# with no <files>, so it stays decoupled from this check.
|
|
22
22
|
git -C "$TMP_ROOT" init -q -b main
|
|
23
23
|
git -C "$TMP_ROOT" config user.email test@example.com
|
|
24
24
|
git -C "$TMP_ROOT" config user.name test
|
|
@@ -55,7 +55,7 @@ assert_round_merged() {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
write_round() {
|
|
58
|
-
# $1 = Handoff body (already includes
|
|
58
|
+
# $1 = Handoff body (already includes the <handoff> block). Status DONE
|
|
59
59
|
# throughout so this suite is decoupled from Phase 1's 工作區/下一步 checks.
|
|
60
60
|
# Writes into .round-current.md — the file enforce-devlog.sh now validates
|
|
61
61
|
# (round-start.sh already opened a skeleton there; this overwrites it).
|
|
@@ -78,40 +78,56 @@ write_round() {
|
|
|
78
78
|
|
|
79
79
|
# --- full canonical order -> allowed -----------------------------------------
|
|
80
80
|
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
81
|
-
write_round "
|
|
81
|
+
write_round "<handoff>
|
|
82
|
+
<decisions>
|
|
82
83
|
d
|
|
83
|
-
|
|
84
|
+
</decisions>
|
|
85
|
+
<files>
|
|
84
86
|
尚未 commit:
|
|
85
|
-
|
|
87
|
+
</files>
|
|
88
|
+
<workspace>
|
|
86
89
|
main @ ${HASH},工作樹乾淨
|
|
87
|
-
|
|
90
|
+
</workspace>
|
|
91
|
+
<state>
|
|
88
92
|
c
|
|
89
|
-
|
|
93
|
+
</state>
|
|
94
|
+
<done-when>
|
|
90
95
|
observable done.
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
</done-when>
|
|
97
|
+
<next>
|
|
98
|
+
n
|
|
99
|
+
</next>
|
|
100
|
+
</handoff>"
|
|
93
101
|
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
94
102
|
assert_exit "full canonical order -> allowed" 0 $?
|
|
95
103
|
assert_round_merged "full canonical order"
|
|
96
104
|
|
|
97
105
|
# --- subset in order -> allowed -----------------------------------------------
|
|
98
106
|
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
99
|
-
write_round "
|
|
107
|
+
write_round "<handoff>
|
|
108
|
+
<decisions>
|
|
100
109
|
d
|
|
101
|
-
|
|
102
|
-
|
|
110
|
+
</decisions>
|
|
111
|
+
<state>
|
|
112
|
+
c
|
|
113
|
+
</state>
|
|
114
|
+
</handoff>"
|
|
103
115
|
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
104
116
|
assert_exit "subset in canonical order -> allowed" 0 $?
|
|
105
117
|
assert_round_merged "subset in canonical order"
|
|
106
118
|
|
|
107
119
|
# --- reordered -> blocked -------------------------------------------------------
|
|
108
120
|
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
109
|
-
write_round "
|
|
121
|
+
write_round "<handoff>
|
|
122
|
+
<state>
|
|
110
123
|
c
|
|
111
|
-
|
|
112
|
-
|
|
124
|
+
</state>
|
|
125
|
+
<decisions>
|
|
126
|
+
d
|
|
127
|
+
</decisions>
|
|
128
|
+
</handoff>"
|
|
113
129
|
MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
|
|
114
|
-
assert_exit "
|
|
130
|
+
assert_exit "state before decisions -> blocked" 2 $?
|
|
115
131
|
case "$MSG" in
|
|
116
132
|
*"順序錯了"*) echo "PASS: order-violation message" ;;
|
|
117
133
|
*) echo "FAIL: expected 順序錯了 message, got: $MSG"; FAIL=1 ;;
|
|
@@ -119,32 +135,44 @@ esac
|
|
|
119
135
|
|
|
120
136
|
# --- duplicate -> blocked -------------------------------------------------------
|
|
121
137
|
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
122
|
-
write_round "
|
|
138
|
+
write_round "<handoff>
|
|
139
|
+
<state>
|
|
123
140
|
c1
|
|
124
|
-
|
|
125
|
-
|
|
141
|
+
</state>
|
|
142
|
+
<state>
|
|
143
|
+
c2
|
|
144
|
+
</state>
|
|
145
|
+
</handoff>"
|
|
126
146
|
MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
|
|
127
|
-
assert_exit "duplicate
|
|
147
|
+
assert_exit "duplicate state -> blocked" 2 $?
|
|
128
148
|
case "$MSG" in
|
|
129
|
-
*"出現超過一次"*) echo "PASS: duplicate-
|
|
149
|
+
*"出現超過一次"*) echo "PASS: duplicate-tag message" ;;
|
|
130
150
|
*) echo "FAIL: expected 出現超過一次 message, got: $MSG"; FAIL=1 ;;
|
|
131
151
|
esac
|
|
132
152
|
|
|
133
|
-
# ---
|
|
153
|
+
# --- unknown tag -> blocked (spec rule 6: no silently ignored subsections) ---
|
|
134
154
|
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
135
|
-
write_round "
|
|
155
|
+
write_round "<handoff>
|
|
156
|
+
<decisions>
|
|
136
157
|
d
|
|
137
|
-
|
|
158
|
+
</decisions>
|
|
159
|
+
<notes>
|
|
138
160
|
x
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh"
|
|
142
|
-
assert_exit "
|
|
143
|
-
|
|
161
|
+
</notes>
|
|
162
|
+
</handoff>"
|
|
163
|
+
MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
|
|
164
|
+
assert_exit "unknown tag -> blocked" 2 $?
|
|
165
|
+
case "$MSG" in
|
|
166
|
+
*"不認得的標籤"*) echo "PASS: unknown-tag message" ;;
|
|
167
|
+
*) echo "FAIL: expected 不認得的標籤 message, got: $MSG"; FAIL=1 ;;
|
|
168
|
+
esac
|
|
144
169
|
|
|
145
|
-
# --- fenced example
|
|
170
|
+
# --- fenced legacy example inside a field -> plain content, allowed --------
|
|
171
|
+
# XML fields don't track fences; a ``` block quoting old #### headings is
|
|
172
|
+
# just text in <decisions>.
|
|
146
173
|
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
147
|
-
write_round "
|
|
174
|
+
write_round "<handoff>
|
|
175
|
+
<decisions>
|
|
148
176
|
說明格式時可以貼一段範例:
|
|
149
177
|
\`\`\`markdown
|
|
150
178
|
#### 現況
|
|
@@ -153,67 +181,107 @@ c
|
|
|
153
181
|
d
|
|
154
182
|
\`\`\`
|
|
155
183
|
真正決定
|
|
156
|
-
|
|
157
|
-
|
|
184
|
+
</decisions>
|
|
185
|
+
<state>
|
|
186
|
+
c
|
|
187
|
+
</state>
|
|
188
|
+
</handoff>"
|
|
158
189
|
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
159
|
-
assert_exit "fenced example
|
|
160
|
-
assert_round_merged "fenced example
|
|
190
|
+
assert_exit "fenced #### example inside <decisions> -> allowed" 0 $?
|
|
191
|
+
assert_round_merged "fenced #### example inside <decisions>"
|
|
161
192
|
|
|
162
|
-
# ---
|
|
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.
|
|
193
|
+
# --- unterminated fence inside a field -> fail-open, allowed ----------------
|
|
182
194
|
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
183
|
-
write_round "
|
|
195
|
+
write_round "<handoff>
|
|
196
|
+
<decisions>
|
|
184
197
|
一段沒收尾的範例:
|
|
185
198
|
\`\`\`markdown
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
199
|
+
沒收尾內容
|
|
200
|
+
</decisions>
|
|
201
|
+
<state>
|
|
202
|
+
c
|
|
203
|
+
</state>
|
|
204
|
+
</handoff>"
|
|
189
205
|
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
190
|
-
assert_exit "unterminated fence in
|
|
191
|
-
assert_round_merged "unterminated fence in
|
|
192
|
-
|
|
193
|
-
# ---
|
|
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.)
|
|
206
|
+
assert_exit "unterminated fence in <decisions> -> allowed" 0 $?
|
|
207
|
+
assert_round_merged "unterminated fence in <decisions>"
|
|
208
|
+
|
|
209
|
+
# --- unterminated fence does not hide a real duplicate tag -------------------
|
|
201
210
|
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
202
|
-
write_round "
|
|
211
|
+
write_round "<handoff>
|
|
212
|
+
<decisions>
|
|
203
213
|
一段沒收尾的範例:
|
|
204
214
|
\`\`\`markdown
|
|
205
215
|
沒收尾內容
|
|
206
|
-
|
|
216
|
+
</decisions>
|
|
217
|
+
<state>
|
|
207
218
|
c
|
|
208
|
-
|
|
209
|
-
|
|
219
|
+
</state>
|
|
220
|
+
<decisions>
|
|
221
|
+
d2(應該被判定為重複)
|
|
222
|
+
</decisions>
|
|
223
|
+
</handoff>"
|
|
210
224
|
MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
|
|
211
225
|
assert_exit "unterminated fence no longer hides a real duplicate -> blocked" 2 $?
|
|
212
226
|
case "$MSG" in
|
|
213
|
-
*"出現超過一次"*) echo "PASS: duplicate-
|
|
227
|
+
*"出現超過一次"*) echo "PASS: duplicate-tag message despite unterminated fence" ;;
|
|
214
228
|
*) echo "FAIL: expected 出現超過一次 message, got: $MSG"; FAIL=1 ;;
|
|
215
229
|
esac
|
|
216
230
|
|
|
231
|
+
# --- XML gate (docs/design/handoff-xml.md) ---
|
|
232
|
+
write_handoff_round() { # $1 = Handoff body lines (already formatted)
|
|
233
|
+
{
|
|
234
|
+
echo "## Round 1 — 2026-09-26T00:00:00+08:00"
|
|
235
|
+
echo ""
|
|
236
|
+
echo "### Summary"; echo "s"; echo ""
|
|
237
|
+
echo "### Reply"; echo "r"; echo ""
|
|
238
|
+
echo "### Handoff"
|
|
239
|
+
printf '%s\n' "$1"
|
|
240
|
+
echo ""
|
|
241
|
+
echo "### Status"; echo "DONE"
|
|
242
|
+
} > "$DEVLOG_DIR/.round-current.md"
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
246
|
+
write_handoff_round '#### 現況
|
|
247
|
+
legacy'
|
|
248
|
+
MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
|
|
249
|
+
assert_exit "legacy Handoff blocked" 2 $?
|
|
250
|
+
case "$MSG" in
|
|
251
|
+
*"migrate-handoff.sh"*"<handoff>"*"npx devlog-tracker init"*) echo "PASS: legacy message has migrate, template, init hint" ;;
|
|
252
|
+
*) echo "FAIL: legacy message: $MSG"; FAIL=1 ;;
|
|
253
|
+
esac
|
|
254
|
+
|
|
255
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
256
|
+
write_handoff_round '<handoff>
|
|
257
|
+
<state>
|
|
258
|
+
ok
|
|
259
|
+
</state>
|
|
260
|
+
</handoff>'
|
|
261
|
+
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
262
|
+
assert_exit "XML Handoff DONE passes" 0 $?
|
|
263
|
+
|
|
264
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
265
|
+
write_handoff_round '<handoff>
|
|
266
|
+
<next>
|
|
267
|
+
x
|
|
268
|
+
</next>
|
|
269
|
+
<state>
|
|
270
|
+
y
|
|
271
|
+
</state>
|
|
272
|
+
</handoff>'
|
|
273
|
+
MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
|
|
274
|
+
assert_exit "XML order violation blocked" 2 $?
|
|
275
|
+
case "$MSG" in *"順序"*"### Handoff"*"<handoff>"*) echo "PASS: order message + template" ;; *) echo "FAIL: order msg: $MSG"; FAIL=1 ;; esac
|
|
276
|
+
|
|
277
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
278
|
+
write_handoff_round '<handoff>
|
|
279
|
+
<state>
|
|
280
|
+
y
|
|
281
|
+
</state>'
|
|
282
|
+
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
283
|
+
assert_exit "unclosed handoff blocked" 2 $?
|
|
284
|
+
|
|
217
285
|
if [ "$FAIL" -eq 0 ]; then
|
|
218
286
|
echo "All checks passed."
|
|
219
287
|
exit 0
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
set -uo pipefail
|
|
5
5
|
|
|
6
6
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
7
|
+
# shellcheck source=lib/xml-fixture.sh
|
|
8
|
+
. "$SCRIPT_DIR/tests/lib/xml-fixture.sh"
|
|
7
9
|
TMP_ROOT="$(mktemp -d)"
|
|
8
10
|
trap 'rm -rf "$TMP_ROOT"' EXIT
|
|
9
11
|
|
|
@@ -79,6 +81,7 @@ write_unfinished() {
|
|
|
79
81
|
echo "### Status"
|
|
80
82
|
echo "$status"
|
|
81
83
|
} > "$DEVLOG_DIR/.round-current.md"
|
|
84
|
+
xml_fixture "$DEVLOG_DIR/.round-current.md"
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
# 1) IN_PROGRESS without Session Handoff -> blocked
|
|
@@ -87,8 +90,8 @@ write_unfinished IN_PROGRESS ""
|
|
|
87
90
|
MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
|
|
88
91
|
assert_exit "IN_PROGRESS without Session Handoff -> blocked" 2 $?
|
|
89
92
|
case "$MSG" in
|
|
90
|
-
*"
|
|
91
|
-
*) echo "FAIL: expected
|
|
93
|
+
*"<session-handoff>"*) echo "PASS: message shows <session-handoff> template" ;;
|
|
94
|
+
*) echo "FAIL: expected <session-handoff> in message, got: $MSG"; FAIL=1 ;;
|
|
92
95
|
esac
|
|
93
96
|
|
|
94
97
|
# 2) IN_PROGRESS with Session Handoff -> write handoff.md
|
|
@@ -97,7 +100,7 @@ write_unfinished IN_PROGRESS "$SESSION_OK"
|
|
|
97
100
|
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
98
101
|
assert_exit "IN_PROGRESS with Session Handoff -> allowed" 0 $?
|
|
99
102
|
if [ -f "$DEVLOG_DIR/handoff.md" ] && grep -q 'pick route A' "$DEVLOG_DIR/handoff.md" \
|
|
100
|
-
&& grep -q '
|
|
103
|
+
&& grep -q '^<session-handoff>$' "$DEVLOG_DIR/handoff.md"; then
|
|
101
104
|
echo "PASS: handoff.md written from Session Handoff"
|
|
102
105
|
else
|
|
103
106
|
echo "FAIL: handoff.md missing or wrong"; FAIL=1
|
|
@@ -144,6 +147,7 @@ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
|
144
147
|
echo "### Status"
|
|
145
148
|
echo "DONE"
|
|
146
149
|
} > "$DEVLOG_DIR/.round-current.md"
|
|
150
|
+
xml_fixture "$DEVLOG_DIR/.round-current.md"
|
|
147
151
|
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
148
152
|
assert_exit "DONE -> allowed" 0 $?
|
|
149
153
|
if [ ! -f "$DEVLOG_DIR/handoff.md" ]; then
|
|
@@ -152,27 +156,104 @@ else
|
|
|
152
156
|
echo "FAIL: handoff.md survived DONE"; FAIL=1
|
|
153
157
|
fi
|
|
154
158
|
|
|
155
|
-
# 5) wrong order -> blocked; existing handoff unchanged
|
|
159
|
+
# 5) wrong order -> blocked; existing handoff unchanged. Written as XML by
|
|
160
|
+
# hand: the converter skips a Session Handoff in the wrong order, so a legacy
|
|
161
|
+
# fixture would be stopped by the legacy gate instead of the order check.
|
|
156
162
|
echo "keep-me" > "$DEVLOG_DIR/handoff.md"
|
|
157
163
|
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
158
164
|
write_unfinished IN_PROGRESS "
|
|
159
165
|
### Session Handoff
|
|
160
|
-
|
|
161
|
-
|
|
166
|
+
<session-handoff>
|
|
167
|
+
<open-questions>
|
|
162
168
|
- x
|
|
163
|
-
|
|
164
|
-
|
|
169
|
+
</open-questions>
|
|
170
|
+
<decisions>
|
|
165
171
|
- y
|
|
166
|
-
|
|
167
|
-
|
|
172
|
+
</decisions>
|
|
173
|
+
<failed-attempts>
|
|
168
174
|
- z
|
|
175
|
+
</failed-attempts>
|
|
176
|
+
</session-handoff>
|
|
169
177
|
"
|
|
170
|
-
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh"
|
|
178
|
+
MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
|
|
171
179
|
assert_exit "wrong Session Handoff order -> blocked" 2 $?
|
|
180
|
+
case "$MSG" in
|
|
181
|
+
*"順序"*) echo "PASS: wrong order message mentions 順序" ;;
|
|
182
|
+
*) echo "FAIL: expected 順序 in message, got: $MSG"; FAIL=1 ;;
|
|
183
|
+
esac
|
|
172
184
|
if [ -f "$DEVLOG_DIR/handoff.md" ] && grep -q 'keep-me' "$DEVLOG_DIR/handoff.md"; then
|
|
173
185
|
echo "PASS: handoff.md unchanged on validation failure"
|
|
174
186
|
else
|
|
175
187
|
echo "FAIL: handoff.md should be untouched on validation failure"; FAIL=1
|
|
176
188
|
fi
|
|
177
189
|
|
|
190
|
+
# 6) legacy Session Handoff under an XML Handoff -> blocked with migrate hint
|
|
191
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
192
|
+
write_unfinished IN_PROGRESS "$SESSION_OK"
|
|
193
|
+
xml_fixture "$DEVLOG_DIR/.round-current.md"
|
|
194
|
+
# re-inject a legacy Session Handoff after conversion
|
|
195
|
+
awk '/^### Session Handoff/{skip=1; print "### Session Handoff\n#### 決策\n- x\n#### 待解問題\n- y\n#### 失敗嘗試\n- z\n"; next} skip && /^### /{skip=0} !skip' \
|
|
196
|
+
"$DEVLOG_DIR/.round-current.md" > "$DEVLOG_DIR/.rc" && mv "$DEVLOG_DIR/.rc" "$DEVLOG_DIR/.round-current.md"
|
|
197
|
+
MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
|
|
198
|
+
assert_exit "legacy Session Handoff blocked" 2 $?
|
|
199
|
+
case "$MSG" in *"migrate-handoff.sh"*) echo "PASS: legacy session message" ;; *) echo "FAIL: $MSG"; FAIL=1 ;; esac
|
|
200
|
+
|
|
201
|
+
# 7) DONE round quoting a fenced ### Session Handoff example in its Reply, no
|
|
202
|
+
# real Session Handoff -> allowed (presence check must ignore fences)
|
|
203
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
204
|
+
cat > "$DEVLOG_DIR/.round-current.md" <<'EOF2'
|
|
205
|
+
## Round 1 — 2026-09-22T02:00:00+08:00
|
|
206
|
+
|
|
207
|
+
### Summary
|
|
208
|
+
done
|
|
209
|
+
|
|
210
|
+
### Reply
|
|
211
|
+
範例格式:
|
|
212
|
+
```markdown
|
|
213
|
+
### Session Handoff
|
|
214
|
+
<session-handoff>
|
|
215
|
+
<decisions>
|
|
216
|
+
- x
|
|
217
|
+
</decisions>
|
|
218
|
+
</session-handoff>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Handoff
|
|
222
|
+
<handoff>
|
|
223
|
+
<state>
|
|
224
|
+
finished
|
|
225
|
+
</state>
|
|
226
|
+
</handoff>
|
|
227
|
+
|
|
228
|
+
### Status
|
|
229
|
+
DONE
|
|
230
|
+
EOF2
|
|
231
|
+
MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
|
|
232
|
+
assert_exit "DONE with fenced Session Handoff example -> allowed" 0 $?
|
|
233
|
+
[ -n "$MSG" ] && echo "note: stderr was: $MSG"
|
|
234
|
+
|
|
235
|
+
# 8) IN_PROGRESS with only a look-alike heading (### Session Handoff(補充))
|
|
236
|
+
# -> blocked as missing: the writer only accepts the exact heading
|
|
237
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
238
|
+
write_unfinished IN_PROGRESS "
|
|
239
|
+
### Session Handoff(補充)
|
|
240
|
+
<session-handoff>
|
|
241
|
+
<decisions>
|
|
242
|
+
- x
|
|
243
|
+
</decisions>
|
|
244
|
+
<open-questions>
|
|
245
|
+
- y
|
|
246
|
+
</open-questions>
|
|
247
|
+
<failed-attempts>
|
|
248
|
+
- z
|
|
249
|
+
</failed-attempts>
|
|
250
|
+
</session-handoff>
|
|
251
|
+
"
|
|
252
|
+
MSG="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
|
|
253
|
+
assert_exit "look-alike Session Handoff heading -> blocked" 2 $?
|
|
254
|
+
case "$MSG" in
|
|
255
|
+
*"缺少 ### Session Handoff"*) echo "PASS: look-alike heading reported as missing" ;;
|
|
256
|
+
*) echo "FAIL: expected missing Session Handoff message, got: $MSG"; FAIL=1 ;;
|
|
257
|
+
esac
|
|
258
|
+
|
|
178
259
|
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Self-check for enforce-devlog.sh's
|
|
2
|
+
# Self-check for enforce-devlog.sh's <workspace> machine-verify (Phase 1,
|
|
3
3
|
# docs/design/devlog-as-ssot-assessment.md). Separate file from
|
|
4
4
|
# test-enforce-devlog.sh to avoid growing that suite further; still
|
|
5
5
|
# auto-discovered by run-tests.sh's tests/test-*.sh glob.
|
|
6
6
|
set -uo pipefail
|
|
7
7
|
|
|
8
8
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
9
|
+
# shellcheck source=lib/xml-fixture.sh
|
|
10
|
+
. "$SCRIPT_DIR/tests/lib/xml-fixture.sh"
|
|
9
11
|
TMP_ROOT="$(mktemp -d)"
|
|
10
12
|
trap 'rm -rf "$TMP_ROOT"' EXIT
|
|
11
13
|
|
|
@@ -89,6 +91,7 @@ write_round() {
|
|
|
89
91
|
echo "### Status"
|
|
90
92
|
echo "IN_PROGRESS"
|
|
91
93
|
} > "$DEVLOG_DIR/.round-current.md"
|
|
94
|
+
xml_fixture "$DEVLOG_DIR/.round-current.md"
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
# --- exact match -> allowed --------------------------------------------------
|
|
@@ -150,6 +153,7 @@ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
|
150
153
|
echo "### Status"
|
|
151
154
|
echo "BLOCKED"
|
|
152
155
|
} > "$DEVLOG_DIR/.round-current.md"
|
|
156
|
+
xml_fixture "$DEVLOG_DIR/.round-current.md"
|
|
153
157
|
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
154
158
|
assert_exit "BLOCKED with missing workspace -> blocked" 2 $?
|
|
155
159
|
|
|
@@ -171,12 +175,15 @@ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
|
171
175
|
echo "### Status"
|
|
172
176
|
echo "DONE"
|
|
173
177
|
} > "$DEVLOG_DIR/.round-current.md"
|
|
178
|
+
xml_fixture "$DEVLOG_DIR/.round-current.md"
|
|
174
179
|
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
175
180
|
assert_exit "DONE with no workspace section -> allowed (check does not apply)" 0 $?
|
|
176
181
|
assert_round_merged "DONE with no workspace section"
|
|
177
182
|
|
|
178
|
-
# --- fenced example quoting
|
|
179
|
-
#
|
|
183
|
+
# --- fenced example quoting a legacy `#### 工作區` inside <decisions> before
|
|
184
|
+
# the real <workspace> -> the quoted text stays plain content of <decisions>
|
|
185
|
+
# after xml_fixture, and the workspace check still reads the real <workspace>
|
|
186
|
+
# body -------------------------------------------------------------------
|
|
180
187
|
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
181
188
|
{
|
|
182
189
|
echo "## Round 4 — 2026-09-10T00:15:00+08:00"
|
|
@@ -219,17 +226,15 @@ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
|
219
226
|
echo "### Status"
|
|
220
227
|
echo "IN_PROGRESS"
|
|
221
228
|
} > "$DEVLOG_DIR/.round-current.md"
|
|
229
|
+
xml_fixture "$DEVLOG_DIR/.round-current.md"
|
|
222
230
|
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
223
231
|
assert_exit "fenced example quoting #### 工作區 before the real section -> allowed" 0 $?
|
|
224
232
|
assert_round_merged "fenced example quoting #### 工作區 before the real section"
|
|
225
233
|
|
|
226
|
-
# --- unterminated fence in an earlier
|
|
227
|
-
#
|
|
228
|
-
#
|
|
229
|
-
#
|
|
230
|
-
# `fence` stuck at 1 for the rest of the round, so handoff_subsection_body()
|
|
231
|
-
# could no longer even find the "#### 下一步" heading — even though #### 工作區
|
|
232
|
-
# above it is exactly correct and #### 下一步 below it has real content.
|
|
234
|
+
# --- unterminated fence in an earlier field -> not a false block. A ```
|
|
235
|
+
# fence that never closes must never make a present, filled-in <next>
|
|
236
|
+
# register as missing: XML fields don't track fences at all. Written as XML
|
|
237
|
+
# directly — the legacy converter leaves odd-fence rounds untouched.
|
|
233
238
|
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
234
239
|
{
|
|
235
240
|
echo "## Round 5 — 2026-09-10T00:20:00+08:00"
|
|
@@ -241,28 +246,36 @@ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
|
241
246
|
echo "fixture reply."
|
|
242
247
|
echo ""
|
|
243
248
|
echo "### Handoff"
|
|
244
|
-
echo "
|
|
249
|
+
echo "<handoff>"
|
|
250
|
+
echo "<workspace>"
|
|
245
251
|
echo "main @ ${HASH},工作樹乾淨"
|
|
246
|
-
echo "
|
|
252
|
+
echo "</workspace>"
|
|
253
|
+
echo "<state>"
|
|
247
254
|
echo "一段沒收尾的範例:"
|
|
248
255
|
echo '```markdown'
|
|
249
256
|
echo "沒收尾內容,一路吃到這個 Round 結尾"
|
|
250
|
-
echo "
|
|
257
|
+
echo "</state>"
|
|
258
|
+
echo "<done-when>"
|
|
251
259
|
echo "observable done via test."
|
|
252
|
-
echo "
|
|
260
|
+
echo "</done-when>"
|
|
261
|
+
echo "<next>"
|
|
253
262
|
echo "edit hooks/scripts/enforce-devlog.sh"
|
|
263
|
+
echo "</next>"
|
|
264
|
+
echo "</handoff>"
|
|
254
265
|
echo ""
|
|
255
266
|
echo ""
|
|
256
267
|
echo "### Session Handoff"
|
|
257
|
-
echo ""
|
|
258
|
-
echo "
|
|
268
|
+
echo "<session-handoff>"
|
|
269
|
+
echo "<decisions>"
|
|
259
270
|
echo "- (無)"
|
|
260
|
-
echo ""
|
|
261
|
-
echo "
|
|
271
|
+
echo "</decisions>"
|
|
272
|
+
echo "<open-questions>"
|
|
262
273
|
echo "- fixture open"
|
|
263
|
-
echo ""
|
|
264
|
-
echo "
|
|
274
|
+
echo "</open-questions>"
|
|
275
|
+
echo "<failed-attempts>"
|
|
265
276
|
echo "- (無)"
|
|
277
|
+
echo "</failed-attempts>"
|
|
278
|
+
echo "</session-handoff>"
|
|
266
279
|
echo ""
|
|
267
280
|
echo "### Status"
|
|
268
281
|
echo "IN_PROGRESS"
|
|
@@ -271,17 +284,17 @@ echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
|
271
284
|
assert_exit "unterminated fence in 現況 -> fail-open, 下一步 still recognized" 0 $?
|
|
272
285
|
assert_round_merged "unterminated fence in 現況"
|
|
273
286
|
|
|
274
|
-
# --- DONE with a non-empty
|
|
287
|
+
# --- DONE with a non-empty <files> (claims files were touched/committed)
|
|
275
288
|
# IS now checked: a DONE round that reports file changes but has no/wrong
|
|
276
|
-
#
|
|
289
|
+
# <workspace> is exactly the "已 commit 完成" false-claim case
|
|
277
290
|
# devlog-as-ssot-assessment.md flags as uncaught — machine-verify it the
|
|
278
291
|
# same way IN_PROGRESS/BLOCKED already are. A trivial DONE round with no
|
|
279
|
-
#
|
|
292
|
+
# <files> stays exempt (previous test above), matching SKILL.md's
|
|
280
293
|
# "瑣碎輪只留現況一句(沒有工作區)" convention untouched.
|
|
281
294
|
#
|
|
282
|
-
# The
|
|
283
|
-
# this suite only cares about triggering the
|
|
284
|
-
#
|
|
295
|
+
# The <files> body below is just "尚未 commit:" with no claimed paths —
|
|
296
|
+
# this suite only cares about triggering the <workspace> check via a non-empty
|
|
297
|
+
# <files>, not about <files> content itself (that's
|
|
285
298
|
# test-enforce-devlog-files.sh's job, Phase 4). Any grammar-valid,
|
|
286
299
|
# always-passing body works here. ------------------------------------------
|
|
287
300
|
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
@@ -303,6 +316,7 @@ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
|
303
316
|
echo "### Status"
|
|
304
317
|
echo "DONE"
|
|
305
318
|
} > "$DEVLOG_DIR/.round-current.md"
|
|
319
|
+
xml_fixture "$DEVLOG_DIR/.round-current.md"
|
|
306
320
|
MSG_DONE="$(echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" 2>&1)"
|
|
307
321
|
assert_exit "DONE with #### 檔案 but no #### 工作區 -> blocked" 2 $?
|
|
308
322
|
case "$MSG_DONE" in
|
|
@@ -331,6 +345,7 @@ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
|
331
345
|
echo "### Status"
|
|
332
346
|
echo "DONE"
|
|
333
347
|
} > "$DEVLOG_DIR/.round-current.md"
|
|
348
|
+
xml_fixture "$DEVLOG_DIR/.round-current.md"
|
|
334
349
|
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
335
350
|
assert_exit "DONE with #### 檔案 and stale 工作區 hash -> blocked" 2 $?
|
|
336
351
|
|
|
@@ -355,6 +370,7 @@ bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
|
355
370
|
echo "### Status"
|
|
356
371
|
echo "DONE"
|
|
357
372
|
} > "$DEVLOG_DIR/.round-current.md"
|
|
373
|
+
xml_fixture "$DEVLOG_DIR/.round-current.md"
|
|
358
374
|
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
359
375
|
assert_exit "DONE with #### 檔案 and matching 工作區 -> allowed" 0 $?
|
|
360
376
|
assert_round_merged "DONE with #### 檔案 and matching 工作區"
|