devlog-tracker 0.33.4 → 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/tests/test-workspace-snapshot.sh +32 -0
- package/core/scripts/timeline-render.js +26 -2
- package/core/scripts/timeline-render.test.js +23 -1
- package/core/scripts/workspace-snapshot.sh +15 -2
- package/package.json +3 -3
- package/skills/devlog-tracker/SKILL.md +81 -56
- 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
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Self-check for handoff-convert.sh + migrate-handoff.sh (docs/design/handoff-xml.md).
|
|
3
|
+
set -uo pipefail
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
# shellcheck source=../handoff-convert.sh
|
|
6
|
+
. "$SCRIPT_DIR/handoff-convert.sh"
|
|
7
|
+
TMP="$(mktemp -d)"
|
|
8
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
9
|
+
FAIL=0
|
|
10
|
+
eq() {
|
|
11
|
+
if [ "$2" = "$3" ]; then echo "PASS: $1"; else printf 'FAIL: %s\n--- expected\n%s\n--- actual\n%s\n' "$1" "$2" "$3"; FAIL=1; fi
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
cat > "$TMP/in.md" <<'EOF'
|
|
15
|
+
# Project
|
|
16
|
+
|
|
17
|
+
## Round 1 — t1
|
|
18
|
+
|
|
19
|
+
### Summary
|
|
20
|
+
s1
|
|
21
|
+
|
|
22
|
+
### Handoff
|
|
23
|
+
#### 決策
|
|
24
|
+
pick A
|
|
25
|
+
|
|
26
|
+
#### 工作區
|
|
27
|
+
main @ abc1234,工作樹乾淨
|
|
28
|
+
#### 下一步
|
|
29
|
+
run `x`
|
|
30
|
+
```
|
|
31
|
+
#### 下一步
|
|
32
|
+
fenced, not a heading
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Session Handoff
|
|
36
|
+
|
|
37
|
+
#### 決策
|
|
38
|
+
- (無)
|
|
39
|
+
|
|
40
|
+
#### 待解問題
|
|
41
|
+
- q
|
|
42
|
+
|
|
43
|
+
#### 失敗嘗試
|
|
44
|
+
- (無)
|
|
45
|
+
|
|
46
|
+
### Status
|
|
47
|
+
IN_PROGRESS
|
|
48
|
+
|
|
49
|
+
## Checkpoint(Round 1-1 摘要)
|
|
50
|
+
### 決策
|
|
51
|
+
untouched
|
|
52
|
+
|
|
53
|
+
## Round 2 — t2
|
|
54
|
+
|
|
55
|
+
### Handoff
|
|
56
|
+
#### 自訂小節
|
|
57
|
+
x
|
|
58
|
+
|
|
59
|
+
### Status
|
|
60
|
+
DONE
|
|
61
|
+
|
|
62
|
+
## Round 3 — t3
|
|
63
|
+
|
|
64
|
+
### Handoff
|
|
65
|
+
<handoff>
|
|
66
|
+
<state>
|
|
67
|
+
already xml
|
|
68
|
+
</state>
|
|
69
|
+
</handoff>
|
|
70
|
+
|
|
71
|
+
### Status
|
|
72
|
+
DONE
|
|
73
|
+
EOF
|
|
74
|
+
|
|
75
|
+
cat > "$TMP/want.md" <<'EOF'
|
|
76
|
+
# Project
|
|
77
|
+
|
|
78
|
+
## Round 1 — t1
|
|
79
|
+
|
|
80
|
+
### Summary
|
|
81
|
+
s1
|
|
82
|
+
|
|
83
|
+
### Handoff
|
|
84
|
+
<handoff>
|
|
85
|
+
<decisions>
|
|
86
|
+
pick A
|
|
87
|
+
</decisions>
|
|
88
|
+
<workspace>
|
|
89
|
+
main @ abc1234,工作樹乾淨
|
|
90
|
+
</workspace>
|
|
91
|
+
<next>
|
|
92
|
+
run `x`
|
|
93
|
+
```
|
|
94
|
+
#### 下一步
|
|
95
|
+
fenced, not a heading
|
|
96
|
+
```
|
|
97
|
+
</next>
|
|
98
|
+
</handoff>
|
|
99
|
+
|
|
100
|
+
### Session Handoff
|
|
101
|
+
<session-handoff>
|
|
102
|
+
<decisions>
|
|
103
|
+
- (無)
|
|
104
|
+
</decisions>
|
|
105
|
+
<open-questions>
|
|
106
|
+
- q
|
|
107
|
+
</open-questions>
|
|
108
|
+
<failed-attempts>
|
|
109
|
+
- (無)
|
|
110
|
+
</failed-attempts>
|
|
111
|
+
</session-handoff>
|
|
112
|
+
|
|
113
|
+
### Status
|
|
114
|
+
IN_PROGRESS
|
|
115
|
+
|
|
116
|
+
## Checkpoint(Round 1-1 摘要)
|
|
117
|
+
### 決策
|
|
118
|
+
untouched
|
|
119
|
+
|
|
120
|
+
## Round 2 — t2
|
|
121
|
+
|
|
122
|
+
### Handoff
|
|
123
|
+
#### 自訂小節
|
|
124
|
+
x
|
|
125
|
+
|
|
126
|
+
### Status
|
|
127
|
+
DONE
|
|
128
|
+
|
|
129
|
+
## Round 3 — t3
|
|
130
|
+
|
|
131
|
+
### Handoff
|
|
132
|
+
<handoff>
|
|
133
|
+
<state>
|
|
134
|
+
already xml
|
|
135
|
+
</state>
|
|
136
|
+
</handoff>
|
|
137
|
+
|
|
138
|
+
### Status
|
|
139
|
+
DONE
|
|
140
|
+
EOF
|
|
141
|
+
|
|
142
|
+
handoff_convert_file "$TMP/in.md" "$TMP/out.md" "$TMP/report"
|
|
143
|
+
eq "convert output" "$(cat "$TMP/want.md")" "$(cat "$TMP/out.md")"
|
|
144
|
+
eq "report" "MIGRATED 1
|
|
145
|
+
SKIP 2 不認得的小節「#### 自訂小節」" "$(cat "$TMP/report")"
|
|
146
|
+
|
|
147
|
+
handoff_convert_file "$TMP/out.md" "$TMP/out2.md" "$TMP/report2"
|
|
148
|
+
eq "idempotent output" "$(cat "$TMP/out.md")" "$(cat "$TMP/out2.md")"
|
|
149
|
+
eq "idempotent report only repeats skip" "SKIP 2 不認得的小節「#### 自訂小節」" "$(cat "$TMP/report2")"
|
|
150
|
+
|
|
151
|
+
printf '## Round 4 — t\n\n### Handoff\n#### 現況\na\n#### 決策\nb\n' > "$TMP/order.md"
|
|
152
|
+
handoff_convert_file "$TMP/order.md" "$TMP/order.out" "$TMP/order.rep"
|
|
153
|
+
eq "bad order untouched" "$(cat "$TMP/order.md")" "$(cat "$TMP/order.out")"
|
|
154
|
+
case "$(cat "$TMP/order.rep")" in SKIP\ 4\ *) echo "PASS: bad order reported" ;; *) echo "FAIL: bad order report"; FAIL=1 ;; esac
|
|
155
|
+
|
|
156
|
+
printf '## Round 5 — t\n\n### Handoff\n#### 現況\n```\nunclosed\n' > "$TMP/fence.md"
|
|
157
|
+
handoff_convert_file "$TMP/fence.md" "$TMP/fence.out" "$TMP/fence.rep"
|
|
158
|
+
eq "odd fence untouched" "$(cat "$TMP/fence.md")" "$(cat "$TMP/fence.out")"
|
|
159
|
+
|
|
160
|
+
printf '## Session Handoff\n\n### 決策\n- a\n\n### 待解問題\n- b\n\n### 失敗嘗試\n- (無)\n' > "$TMP/h.md"
|
|
161
|
+
handoff_convert_snapshot "$TMP/h.md" "$TMP/h.out"; RC=$?
|
|
162
|
+
eq "snapshot rc" 0 "$RC"
|
|
163
|
+
eq "snapshot xml" "<session-handoff>
|
|
164
|
+
<decisions>
|
|
165
|
+
- a
|
|
166
|
+
</decisions>
|
|
167
|
+
<open-questions>
|
|
168
|
+
- b
|
|
169
|
+
</open-questions>
|
|
170
|
+
<failed-attempts>
|
|
171
|
+
- (無)
|
|
172
|
+
</failed-attempts>
|
|
173
|
+
</session-handoff>" "$(cat "$TMP/h.out")"
|
|
174
|
+
handoff_convert_snapshot "$TMP/h.out" "$TMP/h.out2"; RC=$?
|
|
175
|
+
eq "snapshot already xml rc" 2 "$RC"
|
|
176
|
+
|
|
177
|
+
# ---- migrate-handoff.sh (Task 3 appends below this line) ----
|
|
178
|
+
|
|
179
|
+
P="$TMP/proj"; D="$P/.devlog"; mkdir -p "$D"
|
|
180
|
+
cp "$TMP/in.md" "$D/devlog.md"
|
|
181
|
+
{ printf '<!-- devlog-origin: branch=feat/x -->\n\n'; cat "$TMP/in.md"; } > "$D/devlog.feat-x.md"
|
|
182
|
+
cp "$TMP/in.md" "$D/devlog.archive.md"
|
|
183
|
+
cp "$TMP/in.md" "$D/devlog.mytopic.md"
|
|
184
|
+
cp "$TMP/h.md" "$D/handoff.md"
|
|
185
|
+
OUT="$(DEVLOG_PROJECT_DIR="$P" bash "$SCRIPT_DIR/migrate-handoff.sh")"; RC=$?
|
|
186
|
+
eq "migrate rc" 0 "$RC"
|
|
187
|
+
case "$OUT" in *"MIGRATED=2"*) echo "PASS: migrated count (main + branch)" ;; *) echo "FAIL: migrated count: $OUT"; FAIL=1 ;; esac
|
|
188
|
+
case "$OUT" in *"SKIP devlog.md Round 2: "*) echo "PASS: skip line" ;; *) echo "FAIL: skip line: $OUT"; FAIL=1 ;; esac
|
|
189
|
+
eq "main converted" "$(cat "$TMP/want.md")" "$(cat "$D/devlog.md")"
|
|
190
|
+
eq "backup kept original" "$(cat "$TMP/in.md")" "$(cat "$D/devlog.md.pre-migrate")"
|
|
191
|
+
grep -q '^<handoff>$' "$D/devlog.feat-x.md" && echo "PASS: branch file converted" || { echo "FAIL: branch file"; FAIL=1; }
|
|
192
|
+
eq "archive untouched" "$(cat "$TMP/in.md")" "$(cat "$D/devlog.archive.md")"
|
|
193
|
+
eq "keep file untouched" "$(cat "$TMP/in.md")" "$(cat "$D/devlog.mytopic.md")"
|
|
194
|
+
grep -q '^<session-handoff>$' "$D/handoff.md" && echo "PASS: handoff.md converted" || { echo "FAIL: handoff.md"; FAIL=1; }
|
|
195
|
+
OUT2="$(DEVLOG_PROJECT_DIR="$P" bash "$SCRIPT_DIR/migrate-handoff.sh")"
|
|
196
|
+
case "$OUT2" in *"MIGRATED=0"*) echo "PASS: second run migrates nothing" ;; *) echo "FAIL: second run: $OUT2"; FAIL=1 ;; esac
|
|
197
|
+
[ ! -d "$D/.lock" ] && echo "PASS: lock released" || { echo "FAIL: lock left behind"; FAIL=1; }
|
|
198
|
+
OUT3="$(DEVLOG_PROJECT_DIR="$TMP/nope" bash "$SCRIPT_DIR/migrate-handoff.sh")"; RC=$?
|
|
199
|
+
eq "no devlog rc" 1 "$RC"
|
|
200
|
+
eq "no devlog out" "NO_DEVLOG" "$OUT3"
|
|
201
|
+
|
|
202
|
+
# Coverage gap 1: a duplicated `#### 現況` inside one Handoff leaves that
|
|
203
|
+
# round byte-for-byte untouched and is reported as SKIP mentioning 重複.
|
|
204
|
+
printf '## Round 10 — dup\n\n### Handoff\n#### 現況\na\n#### 現況\nb\n\n### Status\nIN_PROGRESS\n' > "$TMP/dup.md"
|
|
205
|
+
handoff_convert_file "$TMP/dup.md" "$TMP/dup.out" "$TMP/dup.rep"
|
|
206
|
+
eq "duplicate section untouched" "$(cat "$TMP/dup.md")" "$(cat "$TMP/dup.out")"
|
|
207
|
+
case "$(cat "$TMP/dup.rep")" in SKIP\ 10\ *重複*) echo "PASS: duplicate section reported" ;; *) echo "FAIL: duplicate section report: $(cat "$TMP/dup.rep")"; FAIL=1 ;; esac
|
|
208
|
+
|
|
209
|
+
# Coverage gap 2: Handoff is valid legacy but Session Handoff has an unknown
|
|
210
|
+
# `#### 其他` subsection — the WHOLE round (including the convertible
|
|
211
|
+
# Handoff) is left byte-for-byte untouched and reported SKIP.
|
|
212
|
+
printf '## Round 11 — mixed\n\n### Handoff\n#### 決策\npick\n\n### Session Handoff\n#### 其他\nx\n\n### Status\nDONE\n' > "$TMP/mixed.md"
|
|
213
|
+
handoff_convert_file "$TMP/mixed.md" "$TMP/mixed.out" "$TMP/mixed.rep"
|
|
214
|
+
eq "unknown session-handoff subsection leaves whole round untouched" "$(cat "$TMP/mixed.md")" "$(cat "$TMP/mixed.out")"
|
|
215
|
+
case "$(cat "$TMP/mixed.rep")" in SKIP\ 11\ *) echo "PASS: mixed round skipped" ;; *) echo "FAIL: mixed round report: $(cat "$TMP/mixed.rep")"; FAIL=1 ;; esac
|
|
216
|
+
|
|
217
|
+
# Odd fence (a): the open round's User Input pasted an unbalanced fence (3
|
|
218
|
+
# ``` lines). Stop's NOFENCE gate still sees the legacy Handoff, so migrate
|
|
219
|
+
# must report it SKIP instead of silently finding nothing.
|
|
220
|
+
P2="$TMP/proj-odd"; D2="$P2/.devlog"; mkdir -p "$D2"
|
|
221
|
+
printf '## Round 7 — t\n\n### User Input\npaste:\n```\ncode\n```\n```\nunclosed\n\n### Summary\ns\n\n### Reply\nr\n\n### Handoff\n#### 現況\na\n\n#### 下一步\nb\n' > "$D2/.round-current.md"
|
|
222
|
+
cp "$D2/.round-current.md" "$TMP/odd-a.orig"
|
|
223
|
+
OUT4="$(DEVLOG_PROJECT_DIR="$P2" bash "$SCRIPT_DIR/migrate-handoff.sh")"
|
|
224
|
+
case "$OUT4" in *"SKIP .round-current.md Round 7: fence 沒有成對"*) echo "PASS: odd fence in open round reported SKIP" ;; *) echo "FAIL: odd fence open round: $OUT4"; FAIL=1 ;; esac
|
|
225
|
+
case "$OUT4" in *"SKIPPED=1"*) echo "PASS: odd fence open round counted" ;; *) echo "FAIL: odd fence open round count: $OUT4"; FAIL=1 ;; esac
|
|
226
|
+
eq "odd fence open round untouched" "$(cat "$TMP/odd-a.orig")" "$(cat "$D2/.round-current.md")"
|
|
227
|
+
|
|
228
|
+
# Odd fence (b): an unbalanced fence in an old round's Summary must not
|
|
229
|
+
# swallow every later `## Round` heading.
|
|
230
|
+
printf '## Round 1 — old\n\n### Summary\n```\nunclosed\n\n### Handoff\n#### 現況\na\n\n### Status\nDONE\n\n## Round 2 — new\n\n### Handoff\n#### 現況\nb\n\n### Status\nDONE\n' > "$TMP/odd-b.md"
|
|
231
|
+
handoff_convert_file "$TMP/odd-b.md" "$TMP/odd-b.out" "$TMP/odd-b.rep"
|
|
232
|
+
eq "odd fence (b) report" "SKIP 1 fence 沒有成對
|
|
233
|
+
MIGRATED 2" "$(cat "$TMP/odd-b.rep")"
|
|
234
|
+
eq "odd fence (b) output" "## Round 1 — old
|
|
235
|
+
|
|
236
|
+
### Summary
|
|
237
|
+
\`\`\`
|
|
238
|
+
unclosed
|
|
239
|
+
|
|
240
|
+
### Handoff
|
|
241
|
+
#### 現況
|
|
242
|
+
a
|
|
243
|
+
|
|
244
|
+
### Status
|
|
245
|
+
DONE
|
|
246
|
+
|
|
247
|
+
## Round 2 — new
|
|
248
|
+
|
|
249
|
+
### Handoff
|
|
250
|
+
<handoff>
|
|
251
|
+
<state>
|
|
252
|
+
b
|
|
253
|
+
</state>
|
|
254
|
+
</handoff>
|
|
255
|
+
|
|
256
|
+
### Status
|
|
257
|
+
DONE" "$(cat "$TMP/odd-b.out")"
|
|
258
|
+
|
|
259
|
+
# migrate_snapshot_file rc=1: an unrecognised handoff.md is reported, untouched.
|
|
260
|
+
P3="$TMP/proj-snap"; D3="$P3/.devlog"; mkdir -p "$D3"
|
|
261
|
+
printf 'random notes\n' > "$D3/handoff.md"
|
|
262
|
+
OUT5="$(DEVLOG_PROJECT_DIR="$P3" bash "$SCRIPT_DIR/migrate-handoff.sh")"
|
|
263
|
+
case "$OUT5" in *"SKIP handoff.md: "*) echo "PASS: unknown snapshot reported" ;; *) echo "FAIL: unknown snapshot: $OUT5"; FAIL=1 ;; esac
|
|
264
|
+
eq "unknown snapshot untouched" "random notes" "$(cat "$D3/handoff.md")"
|
|
265
|
+
|
|
266
|
+
# Branch file from before the devlog-origin marker existed: still migrated
|
|
267
|
+
# when that branch is checked out (resolved via devlog_resolve_paths).
|
|
268
|
+
P4="$TMP/proj-git"; D4="$P4/.devlog"; mkdir -p "$D4"
|
|
269
|
+
git -C "$P4" init -q
|
|
270
|
+
git -C "$P4" -c user.email=t@t -c user.name=t commit -q --allow-empty -m init
|
|
271
|
+
git -C "$P4" checkout -q -b feat/y
|
|
272
|
+
cp "$TMP/in.md" "$D4/devlog.feat-y.md"
|
|
273
|
+
OUT6="$(DEVLOG_PROJECT_DIR="$P4" bash "$SCRIPT_DIR/migrate-handoff.sh")"
|
|
274
|
+
eq "markerless current branch file converted" "$(cat "$TMP/want.md")" "$(cat "$D4/devlog.feat-y.md")"
|
|
275
|
+
case "$OUT6" in *"MIGRATED=1"*) echo "PASS: markerless branch file converted once" ;; *) echo "FAIL: markerless branch count: $OUT6"; FAIL=1 ;; esac
|
|
276
|
+
|
|
277
|
+
[ "$FAIL" -eq 0 ] || exit 1
|
|
278
|
+
echo "All migrate-handoff checks passed."
|
|
@@ -6,6 +6,8 @@ set -uo pipefail
|
|
|
6
6
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
7
7
|
# shellcheck source=../json-field.sh
|
|
8
8
|
. "$SCRIPT_DIR/json-field.sh"
|
|
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
|
|
|
@@ -664,6 +666,16 @@ else
|
|
|
664
666
|
fi
|
|
665
667
|
rm -f "$DEVLOG_DIR/.checkpoint-state"
|
|
666
668
|
|
|
669
|
+
# Codex invokes project skills through $devlog-<name> in the prompt.
|
|
670
|
+
for ADMIN_PROMPT in '$devlog-status' '$devlog-report --json'; do
|
|
671
|
+
printf '{"prompt":"%s"}' "$ADMIN_PROMPT" | bash "$SCRIPT_DIR/round-start.sh"
|
|
672
|
+
assert_file_absent "Codex admin $ADMIN_PROMPT: no .round-current.md" "$DEVLOG_DIR/.round-current.md"
|
|
673
|
+
done
|
|
674
|
+
printf '{"prompt":"%s"}' '$devlog-continue' | bash "$SCRIPT_DIR/round-start.sh"
|
|
675
|
+
CUR_BODY="$(cat "$DEVLOG_DIR/.round-current.md" 2>/dev/null || echo '')"
|
|
676
|
+
assert_contains "Codex continue still opens a Round" "## Round 1 —" "$CUR_BODY"
|
|
677
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open" "$DEVLOG_DIR/.turn-start" "$DEVLOG_DIR/.round-current.md"
|
|
678
|
+
|
|
667
679
|
# --- 19b: report / timeline / keep-all are admin commands too -------------
|
|
668
680
|
for ADMIN_CMD in report timeline keep-all; do
|
|
669
681
|
rm -f "$DEVLOG_DIR/.round-current.md" "$DEVLOG_DIR/.round-open"
|
|
@@ -772,7 +784,7 @@ do x
|
|
|
772
784
|
IN_PROGRESS
|
|
773
785
|
EOF
|
|
774
786
|
OUT="$(printf '%s' '{"prompt":"keep going"}' | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
|
|
775
|
-
assert_contains "mismatch stdout names
|
|
787
|
+
assert_contains "mismatch stdout names 工作區(<workspace>)" "工作區(\`<workspace>\`" "$OUT"
|
|
776
788
|
assert_contains "mismatch stdout has 宣稱" "宣稱:" "$OUT"
|
|
777
789
|
assert_contains "mismatch stdout has 實際" "實際:" "$OUT"
|
|
778
790
|
assert_contains "mismatch stdout has live snapshot" "$LIVE" "$OUT"
|
|
@@ -780,6 +792,47 @@ assert_contains "mismatch stdout has live snapshot" "$LIVE" "$OUT"
|
|
|
780
792
|
MARKER="$(cat "$WS/.devlog/.workspace-mismatch")"
|
|
781
793
|
[ "$MARKER" = "$LIVE" ] && echo "PASS: marker is live snapshot" || { echo "FAIL: marker [$MARKER]"; FAIL=1; }
|
|
782
794
|
|
|
795
|
+
# --- same MISMATCH injection, Handoff in XML form (docs/design/handoff-xml.md) --
|
|
796
|
+
WS2="$TMP_ROOT/ws-xml"
|
|
797
|
+
mkdir -p "$WS2/.devlog"
|
|
798
|
+
touch "$WS2/.devlog/.enabled"
|
|
799
|
+
git -C "$WS2" init -q -b main
|
|
800
|
+
git -C "$WS2" config user.email test@example.com
|
|
801
|
+
git -C "$WS2" config user.name test
|
|
802
|
+
echo hello > "$WS2/a.txt"
|
|
803
|
+
git -C "$WS2" add a.txt
|
|
804
|
+
git -C "$WS2" commit -q -m init
|
|
805
|
+
cat > "$WS2/.devlog/devlog.md" <<EOF
|
|
806
|
+
## Round 1 — 2026-09-11T00:00:00+08:00
|
|
807
|
+
|
|
808
|
+
### Summary
|
|
809
|
+
s
|
|
810
|
+
|
|
811
|
+
### Reply
|
|
812
|
+
fixture reply.
|
|
813
|
+
|
|
814
|
+
### Handoff
|
|
815
|
+
#### 工作區
|
|
816
|
+
main @ deadbeef,工作樹乾淨
|
|
817
|
+
#### 現況
|
|
818
|
+
going
|
|
819
|
+
#### 完成條件
|
|
820
|
+
`bash hooks/scripts/tests/test-enforce-devlog.sh` 相關情境通過。
|
|
821
|
+
#### 下一步
|
|
822
|
+
do x
|
|
823
|
+
|
|
824
|
+
### Status
|
|
825
|
+
IN_PROGRESS
|
|
826
|
+
EOF
|
|
827
|
+
xml_fixture "$WS2/.devlog/devlog.md"
|
|
828
|
+
export CLAUDE_PROJECT_DIR="$WS2"
|
|
829
|
+
XML_OUT="$(printf '%s' '{"prompt":"keep going"}' | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
|
|
830
|
+
assert_contains "XML Handoff mismatch stdout names 工作區(<workspace>)" "工作區(\`<workspace>\`" "$XML_OUT"
|
|
831
|
+
assert_contains "XML Handoff mismatch stdout has 宣稱" "宣稱:" "$XML_OUT"
|
|
832
|
+
assert_contains "XML Handoff mismatch stdout has 實際" "實際:" "$XML_OUT"
|
|
833
|
+
[ -f "$WS2/.devlog/.workspace-mismatch" ] && echo "PASS: XML Handoff mismatch writes marker" || { echo "FAIL: XML Handoff mismatch: no marker"; FAIL=1; }
|
|
834
|
+
export CLAUDE_PROJECT_DIR="$WS"
|
|
835
|
+
|
|
783
836
|
# --- lessons drift counter: mechanical nudge on repeated mismatch --------
|
|
784
837
|
rm -f "$WS/.devlog/.round-open" "$WS/.devlog/.round-current.md" "$WS/.devlog/.workspace-mismatch"
|
|
785
838
|
touch "$WS/.devlog/.lessons-enabled"
|
|
@@ -539,6 +539,51 @@ else
|
|
|
539
539
|
FAIL=1
|
|
540
540
|
fi
|
|
541
541
|
|
|
542
|
+
# --- XML Handoff + XML handoff.md: both injected --------------------------
|
|
543
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/handoff.md"
|
|
544
|
+
cat > "$DEVLOG_DIR/devlog.md" <<'EOF'
|
|
545
|
+
## Round 1 — 2026-09-22T00:00:00+08:00
|
|
546
|
+
|
|
547
|
+
### Summary
|
|
548
|
+
summary 1
|
|
549
|
+
|
|
550
|
+
### Reply
|
|
551
|
+
fixture reply.
|
|
552
|
+
|
|
553
|
+
### Handoff
|
|
554
|
+
<handoff>
|
|
555
|
+
<state>
|
|
556
|
+
xml state line
|
|
557
|
+
</state>
|
|
558
|
+
<done-when>
|
|
559
|
+
tests pass
|
|
560
|
+
</done-when>
|
|
561
|
+
<next>
|
|
562
|
+
edit xml-next-target.sh
|
|
563
|
+
</next>
|
|
564
|
+
</handoff>
|
|
565
|
+
|
|
566
|
+
### Status
|
|
567
|
+
IN_PROGRESS
|
|
568
|
+
EOF
|
|
569
|
+
cat > "$DEVLOG_DIR/handoff.md" <<'EOF'
|
|
570
|
+
<session-handoff>
|
|
571
|
+
<decisions>
|
|
572
|
+
- keep route A
|
|
573
|
+
</decisions>
|
|
574
|
+
<open-questions>
|
|
575
|
+
- xml open Q
|
|
576
|
+
</open-questions>
|
|
577
|
+
<failed-attempts>
|
|
578
|
+
- (無)
|
|
579
|
+
</failed-attempts>
|
|
580
|
+
</session-handoff>
|
|
581
|
+
EOF
|
|
582
|
+
OUTPUT="$(echo '{"source":"startup"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
|
|
583
|
+
assert_contains "XML handoff <next> content injected" "edit xml-next-target.sh" "$OUTPUT"
|
|
584
|
+
assert_contains "XML handoff.md block injected" "<session-handoff>" "$OUTPUT"
|
|
585
|
+
assert_contains "XML handoff.md body injected" "xml open Q" "$OUTPUT"
|
|
586
|
+
|
|
542
587
|
# --- no handoff.md: excerpt only -----------------------------------------
|
|
543
588
|
rm -f "$DEVLOG_DIR/handoff.md"
|
|
544
589
|
OUTPUT="$(echo '{"source":"startup"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
|
|
@@ -83,6 +83,38 @@ CLI_UNBORN="$(bash "$SCRIPT_DIR/workspace-snapshot.sh" "$UNBORN")"
|
|
|
83
83
|
FUNC_UNBORN="$(workspace_snapshot "$UNBORN")"
|
|
84
84
|
assert_eq "cli matches function (unborn dirty)" "$FUNC_UNBORN" "$CLI_UNBORN"
|
|
85
85
|
|
|
86
|
+
# --- .devlog/ changes are ignored (same rule as files-snapshot.sh) ----------
|
|
87
|
+
# The devlog is rewritten every round, so counting it would make every
|
|
88
|
+
# snapshot dirty and every Handoff 工作區 drift on its own write.
|
|
89
|
+
DL="$TMP_ROOT/devlog-ignored"
|
|
90
|
+
mkdir -p "$DL"
|
|
91
|
+
git -C "$DL" init -q -b main
|
|
92
|
+
git -C "$DL" config user.email test@example.com
|
|
93
|
+
git -C "$DL" config user.name test
|
|
94
|
+
echo hello > "$DL/a.txt"
|
|
95
|
+
mkdir -p "$DL/.devlog"
|
|
96
|
+
echo tracked > "$DL/.devlog/devlog.md"
|
|
97
|
+
git -C "$DL" add a.txt .devlog/devlog.md
|
|
98
|
+
git -C "$DL" commit -q -m init
|
|
99
|
+
DL_HASH="$(git -C "$DL" rev-parse --short HEAD)"
|
|
100
|
+
echo more >> "$DL/.devlog/devlog.md"
|
|
101
|
+
echo round > "$DL/.devlog/.round-current.md"
|
|
102
|
+
OUT="$(workspace_snapshot "$DL")"
|
|
103
|
+
assert_eq ".devlog-only changes count as clean" "main @ ${DL_HASH},工作樹乾淨" "$OUT"
|
|
104
|
+
|
|
105
|
+
echo change >> "$DL/a.txt"
|
|
106
|
+
OUT="$(workspace_snapshot "$DL")"
|
|
107
|
+
EXPECTED="main @ ${DL_HASH}
|
|
108
|
+
未提交:a.txt"
|
|
109
|
+
assert_eq ".devlog changes omitted from 未提交" "$EXPECTED" "$OUT"
|
|
110
|
+
|
|
111
|
+
DL_UNBORN="$TMP_ROOT/devlog-unborn"
|
|
112
|
+
mkdir -p "$DL_UNBORN/.devlog"
|
|
113
|
+
git -C "$DL_UNBORN" init -q -b main
|
|
114
|
+
echo x > "$DL_UNBORN/.devlog/devlog.md"
|
|
115
|
+
OUT="$(workspace_snapshot "$DL_UNBORN")"
|
|
116
|
+
assert_eq "unborn with untracked .devlog/ only is clean" "main @ (尚無 commit),工作樹乾淨" "$OUT"
|
|
117
|
+
|
|
86
118
|
# --- executable entry matches the sourced function --------------------------
|
|
87
119
|
CLI_NONGIT="$(bash "$SCRIPT_DIR/workspace-snapshot.sh" "$NONGIT")"
|
|
88
120
|
assert_eq "cli non-git directory" "非 git 工作區" "$CLI_NONGIT"
|
|
@@ -136,6 +136,30 @@ function renderMarkdown(md) {
|
|
|
136
136
|
return out.join('\n');
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
const HANDOFF_FIELDS = {
|
|
140
|
+
decisions: '決策', files: '檔案', workspace: '工作區', state: '現況',
|
|
141
|
+
'done-when': '完成條件', next: '下一步', 'open-questions': '待解問題', 'failed-attempts': '失敗嘗試',
|
|
142
|
+
};
|
|
143
|
+
const HANDOFF_BLOCK_TAG = /^\s*<\/?(handoff|session-handoff)>\s*$/;
|
|
144
|
+
const HANDOFF_TAG = /^\s*<(\/?)([a-z-]+)>\s*$/;
|
|
145
|
+
|
|
146
|
+
// Line-based XML Handoff (docs/design/handoff-xml.md) → `#### 中文名`
|
|
147
|
+
// headings so renderMarkdown shows it like the legacy form. Legacy text
|
|
148
|
+
// passes through unchanged.
|
|
149
|
+
function handoffToMarkdown(text) {
|
|
150
|
+
const out = [];
|
|
151
|
+
for (const line of String(text).split('\n')) {
|
|
152
|
+
if (HANDOFF_BLOCK_TAG.test(line)) continue;
|
|
153
|
+
const m = HANDOFF_TAG.exec(line);
|
|
154
|
+
if (m && HANDOFF_FIELDS[m[2]]) {
|
|
155
|
+
if (!m[1]) out.push(`#### ${HANDOFF_FIELDS[m[2]]}`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
out.push(line);
|
|
159
|
+
}
|
|
160
|
+
return out.join('\n').trim();
|
|
161
|
+
}
|
|
162
|
+
|
|
139
163
|
function statusOf(s) {
|
|
140
164
|
return STATUSES.includes(s) ? s : 'NONE';
|
|
141
165
|
}
|
|
@@ -145,7 +169,7 @@ function roundCard(r) {
|
|
|
145
169
|
const parts = [];
|
|
146
170
|
if (r.input) parts.push(`<h4>User Input</h4><pre><code>${escapeHtml(r.input)}</code></pre>`);
|
|
147
171
|
if (r.reply) parts.push(`<h4>Reply</h4>${renderMarkdown(r.reply)}`);
|
|
148
|
-
if (r.handoff) parts.push(`<h4>Handoff</h4>${renderMarkdown(r.handoff)}`);
|
|
172
|
+
if (r.handoff) parts.push(`<h4>Handoff</h4>${renderMarkdown(handoffToMarkdown(r.handoff))}`);
|
|
149
173
|
for (const seg of r.segments || []) parts.push(`<div class="seg">${renderMarkdown('#### ' + seg)}</div>`);
|
|
150
174
|
const details = parts.length ? `<details><summary>詳細</summary>${parts.join('\n')}</details>` : '';
|
|
151
175
|
return `<article class="card round" data-status="${st}" data-branch="${escapeHtml(r.branch)}">
|
|
@@ -244,4 +268,4 @@ if (require.main === module) {
|
|
|
244
268
|
});
|
|
245
269
|
}
|
|
246
270
|
|
|
247
|
-
module.exports = { render, renderMarkdown, inline, escapeHtml, safeHref, timelineItems };
|
|
271
|
+
module.exports = { render, renderMarkdown, inline, escapeHtml, safeHref, timelineItems, handoffToMarkdown, HANDOFF_FIELDS };
|
|
@@ -3,7 +3,7 @@ const test = require('node:test');
|
|
|
3
3
|
const assert = require('node:assert/strict');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { spawnSync } = require('child_process');
|
|
6
|
-
const { render, renderMarkdown, safeHref, escapeHtml } = require('./timeline-render');
|
|
6
|
+
const { render, renderMarkdown, safeHref, escapeHtml, handoffToMarkdown, HANDOFF_FIELDS } = require('./timeline-render');
|
|
7
7
|
|
|
8
8
|
function round(extra) {
|
|
9
9
|
return { branch: 'main', file: 'devlog.md', line: 1, n: 1, at: '2026-09-01T10:00:00+0800',
|
|
@@ -90,6 +90,28 @@ test('render: empty data shows an empty-state message', () => {
|
|
|
90
90
|
assert.ok(render({ started: true, rounds: [], checkpoint_blocks: [] }).includes('沒有 Round 可顯示'));
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
+
test('handoffToMarkdown turns XML fields into headings and drops block tags', () => {
|
|
94
|
+
const md = handoffToMarkdown('<handoff>\n<state>\nok\n</state>\n<next>\nrun `x`\n</next>\n</handoff>');
|
|
95
|
+
assert.equal(md, '#### 現況\nok\n#### 下一步\nrun `x`');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('handoffToMarkdown leaves legacy Handoff untouched', () => {
|
|
99
|
+
assert.equal(handoffToMarkdown('#### 現況\nok'), '#### 現況\nok');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('XML Handoff renders as headings, never as raw tags', () => {
|
|
103
|
+
const html = render({ rounds: [round({ handoff: '<handoff>\n<state>\nok\n</state>\n</handoff>' })], checkpoint_blocks: [] });
|
|
104
|
+
assert.ok(!html.includes('<state>'));
|
|
105
|
+
assert.ok(html.includes('現況'));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('HANDOFF_FIELDS matches handoff-fields.sh', () => {
|
|
109
|
+
const script = path.join(__dirname, 'handoff-fields.sh');
|
|
110
|
+
const r = spawnSync('bash', ['-c', `. "${script}"; for k in $HANDOFF_KEYS $SESSION_HANDOFF_KEYS; do printf '%s=%s\\n' "$k" "$(handoff_key_heading "$k")"; done`], { encoding: 'utf8' });
|
|
111
|
+
const fromBash = Object.fromEntries(r.stdout.trim().split('\n').map((l) => l.split('=')));
|
|
112
|
+
assert.deepEqual(fromBash, HANDOFF_FIELDS);
|
|
113
|
+
});
|
|
114
|
+
|
|
93
115
|
test('CLI: stdin JSON -> stdout HTML; invalid JSON -> exit 1', () => {
|
|
94
116
|
const script = path.join(__dirname, 'timeline-render.js');
|
|
95
117
|
const ok = spawnSync(process.execPath, [script], { input: JSON.stringify({ rounds: [round({})] }), encoding: 'utf8' });
|
|
@@ -15,6 +15,19 @@
|
|
|
15
15
|
# Known limitation: a dirty file path containing a space is not exactly
|
|
16
16
|
# reversible from `git status --short`'s last-field extraction below — same
|
|
17
17
|
# limitation the hand-written 工作區 convention already has.
|
|
18
|
+
#
|
|
19
|
+
# Paths under .devlog/ are left out (same rule as files-snapshot.sh): the
|
|
20
|
+
# devlog is rewritten every round, so counting it would make the snapshot
|
|
21
|
+
# dirty by its own write. A tree whose only changes are under .devlog/
|
|
22
|
+
# reports as clean.
|
|
23
|
+
|
|
24
|
+
# `git status --short` minus .devlog/ entries. Lines are "<XY> <path>".
|
|
25
|
+
_ws_status_short() {
|
|
26
|
+
git -C "$1" status --short 2>/dev/null \
|
|
27
|
+
| awk '{ p = substr($0, 4) } p == ".devlog" || p ~ /^\.devlog\// { next } { print }' \
|
|
28
|
+
|| true
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
workspace_snapshot() {
|
|
19
32
|
local dir="${1:-.}"
|
|
20
33
|
if ! git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
@@ -37,7 +50,7 @@ workspace_snapshot() {
|
|
|
37
50
|
printf '非 git 工作區\n'
|
|
38
51
|
return 0
|
|
39
52
|
fi
|
|
40
|
-
status_out="$(
|
|
53
|
+
status_out="$(_ws_status_short "$dir")"
|
|
41
54
|
if [ -z "$status_out" ]; then
|
|
42
55
|
printf '%s @ (尚無 commit),工作樹乾淨\n' "$unborn_branch"
|
|
43
56
|
else
|
|
@@ -51,7 +64,7 @@ workspace_snapshot() {
|
|
|
51
64
|
else
|
|
52
65
|
label="$branch"
|
|
53
66
|
fi
|
|
54
|
-
status_out="$(
|
|
67
|
+
status_out="$(_ws_status_short "$dir")"
|
|
55
68
|
if [ -z "$status_out" ]; then
|
|
56
69
|
if [ "$label" = "HEAD detached" ]; then
|
|
57
70
|
printf '%s @ %s\n' "$label" "$hash"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devlog-tracker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "npx installer for devlog-tracker's Claude Code, Cursor and Codex adapters.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"devlog-tracker": "bin/devlog-tracker.js"
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"node": ">=18"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
|
-
"test": "node --test cli/*.test.js cli/platforms/*.test.js scripts/*.test.js core/scripts/*.test.js",
|
|
25
|
-
"version": "node scripts/sync-version.js && git add .claude-plugin/plugin.json .claude-plugin/marketplace.json README.md README.zh-TW.md"
|
|
24
|
+
"test": "node scripts/sync-codex-plugin.js --check && node --test cli/*.test.js cli/platforms/*.test.js scripts/*.test.js core/scripts/*.test.js",
|
|
25
|
+
"version": "node scripts/sync-version.js && git add .claude-plugin/plugin.json .codex-plugin/plugin.json .claude-plugin/marketplace.json README.md README.zh-TW.md"
|
|
26
26
|
},
|
|
27
27
|
"license": "Apache-2.0",
|
|
28
28
|
"author": {
|