devlog-tracker 1.0.0 → 1.1.1

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 (56) hide show
  1. package/README.md +7 -5
  2. package/README.zh-TW.md +7 -5
  3. package/codex/hooks/on-interrupt.sh +1 -0
  4. package/codex/hooks/on-pre-tool.sh +9 -7
  5. package/codex/hooks/on-session-end.sh +1 -0
  6. package/codex/hooks/on-session-start.sh +1 -0
  7. package/codex/hooks/on-stop.sh +1 -0
  8. package/codex/hooks/on-subagent-start.sh +1 -0
  9. package/codex/hooks/on-user-prompt-submit.sh +1 -0
  10. package/codex/hooks/test-adapters.sh +29 -19
  11. package/commands/clean.md +2 -2
  12. package/commands/continue.md +1 -1
  13. package/commands/keep-all.md +4 -4
  14. package/commands/keep.md +4 -4
  15. package/commands/migrate.md +2 -2
  16. package/commands/span.md +4 -4
  17. package/commands/status.md +1 -1
  18. package/core/scripts/await-open.sh +4 -4
  19. package/core/scripts/clean-devlog.sh +11 -5
  20. package/core/scripts/close-open-round.sh +0 -4
  21. package/core/scripts/devlog-md.sh +89 -8
  22. package/core/scripts/devlog-path.sh +128 -10
  23. package/core/scripts/enforce-devlog.sh +35 -19
  24. package/core/scripts/keep-all.js +4 -3
  25. package/core/scripts/keep-all.sh +2 -3
  26. package/core/scripts/keep-all.test.js +31 -0
  27. package/core/scripts/keep-move.sh +13 -9
  28. package/core/scripts/migrate-handoff.sh +2 -2
  29. package/core/scripts/on-tool-failure.sh +6 -1
  30. package/core/scripts/pause-devlog.sh +5 -3
  31. package/core/scripts/round-start.sh +31 -20
  32. package/core/scripts/segment-watch-set.sh +12 -8
  33. package/core/scripts/segment-watch.sh +3 -6
  34. package/core/scripts/session-start-devlog.sh +11 -5
  35. package/core/scripts/span-close.sh +7 -3
  36. package/core/scripts/span-open.sh +4 -4
  37. package/core/scripts/status-devlog.sh +6 -6
  38. package/core/scripts/tests/test-await-open.sh +9 -0
  39. package/core/scripts/tests/test-devlog-md.sh +76 -15
  40. package/core/scripts/tests/test-devlog-path.sh +126 -0
  41. package/core/scripts/tests/test-multi-platform.sh +286 -0
  42. package/core/scripts/tests/test-segment-watch-set.sh +10 -0
  43. package/core/scripts/tests/test-session-start-devlog.sh +15 -0
  44. package/cursor/hooks/on-pre-tool.sh +1 -0
  45. package/cursor/hooks/on-session-end.sh +1 -0
  46. package/cursor/hooks/on-session-start.sh +1 -0
  47. package/cursor/hooks/on-stop.sh +1 -0
  48. package/cursor/hooks/on-submit-prompt.sh +1 -0
  49. package/cursor/hooks/on-tool-failure.sh +1 -0
  50. package/cursor/hooks/test-adapters.sh +19 -16
  51. package/package.json +1 -1
  52. package/skills/devlog-tracker/SKILL.md +23 -18
  53. package/skills/devlog-tracker/references/contract.md +2 -2
  54. package/skills/devlog-tracker/references/reply-fold.md +8 -8
  55. package/skills/devlog-tracker/references/round-segments.md +2 -2
  56. package/skills/devlog-tracker/references/span-mode.md +10 -3
@@ -29,12 +29,7 @@ HOOKS_DIR="$(cd "${_src%/*}" && pwd)"
29
29
  PROJECT_DIR="${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}"
30
30
  [ -f "$PROJECT_DIR/.devlog/.enabled" ] || exit 0
31
31
  devlog_resolve_paths "$PROJECT_DIR"
32
- ROUND_CURRENT="$DEVLOG_DIR/.round-current.md"
33
- SPAN_FILE="$DEVLOG_DIR/.span-open"
34
32
  CHECKPOINT_FILE="$DEVLOG_DIR/.checkpoint-state"
35
- SEGMENT_FILE="$DEVLOG_DIR/.segment-state"
36
- ROUND_OPEN="$DEVLOG_DIR/.round-open"
37
- AWAITING_FILE="$DEVLOG_DIR/.awaiting-reply"
38
33
  ADVISORY_FILE="$DEVLOG_DIR/.lessons-advisory-state"
39
34
 
40
35
  devlog_lock_acquire
@@ -59,11 +54,9 @@ if [ -f "$AWAITING_FILE" ]; then
59
54
  case "$AWAIT_ROUND" in
60
55
  ''|*[!0-9]*) AWAIT_ROUND='' ;;
61
56
  esac
62
- if [ -n "$AWAIT_ROUND" ] && [ -f "$DEVLOG_FILE" ]; then
63
- CURRENT_LAST="$(devlog_list_round_starts "$DEVLOG_FILE" | awk 'END { print $2 }')"
64
- if [ "$CURRENT_LAST" = "$AWAIT_ROUND" ]; then
65
- FOLD_ROUND="$AWAIT_ROUND"
66
- fi
57
+ if [ -n "$AWAIT_ROUND" ] && [ -f "$DEVLOG_FILE" ] \
58
+ && [ -n "$(devlog_fold_round_start "$DEVLOG_FILE" "$AWAIT_ROUND" "$DEVLOG_PLATFORM")" ]; then
59
+ FOLD_ROUND="$AWAIT_ROUND"
67
60
  fi
68
61
  fi
69
62
 
@@ -155,7 +148,7 @@ if [ "$SPAN_SKIP" -eq 1 ]; then
155
148
  fi
156
149
 
157
150
  if [ "$TASK_NOTIF" -eq 1 ] && [ -z "$FOLD_ROUND" ] && [ "$SPAN_SKIP" -eq 0 ] && [ -f "$DEVLOG_FILE" ]; then
158
- TASK_NOTIF_LAST="$(devlog_list_round_starts "$DEVLOG_FILE" | awk 'END { print $2 }')"
151
+ TASK_NOTIF_LAST="$(devlog_list_round_starts_of "$DEVLOG_FILE" "$DEVLOG_PLATFORM" | awk 'END { print $2 }')"
159
152
  case "$TASK_NOTIF_LAST" in
160
153
  ''|*[!0-9]*) : ;;
161
154
  *)
@@ -165,18 +158,17 @@ if [ "$TASK_NOTIF" -eq 1 ] && [ -z "$FOLD_ROUND" ] && [ "$SPAN_SKIP" -eq 0 ] &&
165
158
  esac
166
159
  fi
167
160
 
168
- MISMATCH_FILE="$DEVLOG_DIR/.workspace-mismatch"
169
161
  rm -f "$MISMATCH_FILE" 2>/dev/null || true
170
162
  if [ "$SPAN_SKIP" -eq 0 ] && [ "$TASK_NOTIF" -eq 0 ] && [ -f "$DEVLOG_FILE" ]; then
171
- CLAIM_ST="$(workspace_claim_state "$PROJECT_DIR" "$DEVLOG_FILE" 2>/dev/null || echo NO_CLAIM)"
163
+ CLAIM_ST="$(workspace_claim_state "$PROJECT_DIR" "$DEVLOG_FILE" "$DEVLOG_PLATFORM" 2>/dev/null || echo NO_CLAIM)"
172
164
  if [ "$CLAIM_ST" = "MISMATCH" ]; then
173
- LAST_START="$(devlog_list_round_starts "$DEVLOG_FILE" | awk 'END { print $1 }')"
165
+ LAST_START="$(devlog_list_round_starts_of "$DEVLOG_FILE" "$DEVLOG_PLATFORM" | awk 'END { print $1 }')"
174
166
  LAST_END="$(devlog_block_end "$DEVLOG_FILE" "$LAST_START")"
175
167
  CLAIMED_WS="$(devlog_round_workspace_body "$DEVLOG_FILE" "$LAST_START" "$LAST_END")"
176
168
  LIVE_WS="$(workspace_snapshot "$PROJECT_DIR")"
177
169
  if [ -n "$LIVE_WS" ]; then
178
170
  printf '%s\n' "$LIVE_WS" > "$MISMATCH_FILE" 2>/dev/null || true
179
- printf '%s\n' "上一輪 Handoff 的工作區(\`<workspace>\`;舊格式是 \`#### 工作區\`)跟目前 git 不符。先在這一輪追加 ### 段落,寫宣稱 vs 實際(實際用下面「實際」逐字內容),再依實際工作樹行動,不要照上一輪「現況/下一步」的字面。"
171
+ printf '%s\n' "上一輪 Handoff 的工作區(\`<workspace>\`;舊格式是 \`#### 工作區\`)跟目前 git 不符。先在這一輪追加 ### 段落,寫宣稱 vs 實際(實際用下面「實際」逐字內容),再依實際工作樹行動,不要照上一輪「現況/下一步」的字面。(若同一工作樹還有其他平台在跑,差異可能來自它。)"
180
172
  printf '\n宣稱:\n%s\n\n實際:\n%s\n' "$CLAIMED_WS" "$LIVE_WS"
181
173
 
182
174
  if [ -f "$DEVLOG_DIR/.lessons-enabled" ]; then
@@ -188,7 +180,7 @@ if [ "$SPAN_SKIP" -eq 0 ] && [ "$TASK_NOTIF" -eq 0 ] && [ -f "$DEVLOG_FILE" ]; t
188
180
  fi
189
181
 
190
182
  if [ "$SPAN_SKIP" -eq 0 ] && [ "$TASK_NOTIF" -eq 0 ] && [ -f "$DEVLOG_FILE" ] && [ -f "$DEVLOG_DIR/.lessons-enabled" ]; then
191
- BLOCKED_ROUND_STARTS="$(devlog_list_round_starts "$DEVLOG_FILE")"
183
+ BLOCKED_ROUND_STARTS="$(devlog_list_round_starts_of "$DEVLOG_FILE" "$DEVLOG_PLATFORM")"
192
184
  BLOCKED_ROUND_COUNT="$(printf '%s\n' "$BLOCKED_ROUND_STARTS" | grep -c '.' || true)"
193
185
  BLOCKED_LAST_LINE="$(printf '%s\n' "$BLOCKED_ROUND_STARTS" | awk 'END { print }')"
194
186
  BLOCKED_LAST_START="$(printf '%s\n' "$BLOCKED_LAST_LINE" | awk '{ print $1 }')"
@@ -214,7 +206,7 @@ if [ "$SPAN_SKIP" -eq 0 ] && [ "$TASK_NOTIF" -eq 0 ] && [ -f "$DEVLOG_FILE" ] &&
214
206
  fi
215
207
 
216
208
  if [ -n "$FOLD_ROUND" ]; then
217
- devlog_reopen_last_round "$DEVLOG_FILE" "$ROUND_CURRENT" || : > "$ROUND_CURRENT"
209
+ devlog_reopen_round "$DEVLOG_FILE" "$ROUND_CURRENT" "$FOLD_ROUND" "$DEVLOG_PLATFORM" || : > "$ROUND_CURRENT"
218
210
  if [ -z "$PROMPT" ]; then
219
211
  PROMPT="(無 prompt)"
220
212
  fi
@@ -251,6 +243,7 @@ if [ -n "$FOLD_ROUND" ]; then
251
243
  fi
252
244
 
253
245
  printf '{"round": %s, "opened_at": "%s", "file": "%s"}\n' "$FOLD_ROUND" "$FULL_TS" "${DEVLOG_FILE##*/}" > "$ROUND_OPEN" 2>/dev/null || true
246
+ [ "$DEVLOG_PLATFORM" = claude ] || printf '這一輪寫在 .devlog/%s。\n' "${ROUND_CURRENT##*/}"
254
247
  elif [ "$SPAN_SKIP" -eq 0 ]; then
255
248
  if [ -z "$PROMPT" ]; then
256
249
  PROMPT="(無 prompt)"
@@ -285,11 +278,20 @@ elif [ "$SPAN_SKIP" -eq 0 ]; then
285
278
  ' "$DEVLOG_FILE" 2>/dev/null || echo 0)"
286
279
  case "$LAST_N" in ''|*[!0-9]*) LAST_N=0 ;; esac
287
280
  fi
281
+ # Another platform's still-open round already owns its number
282
+ # (docs/design/multi-platform-concurrency.md "Round numbering").
283
+ RESERVED_N="$(devlog_open_rounds | awk -F '\t' -v f="${DEVLOG_FILE##*/}" '
284
+ ($3 == f || $3 == "") && $2 ~ /^[0-9]+$/ && $2 + 0 > m { m = $2 + 0 }
285
+ END { print m + 0 }')"
286
+ case "$RESERVED_N" in ''|*[!0-9]*) RESERVED_N=0 ;; esac
287
+ [ "$RESERVED_N" -gt "$LAST_N" ] && LAST_N="$RESERVED_N"
288
288
  NEXT_N=$((LAST_N + 1))
289
289
  TS="$(date +%Y-%m-%dT%H:%M:%S%z 2>/dev/null || echo unknown)"
290
290
 
291
+ HEADING_SOURCE=""
292
+ [ "$DEVLOG_PLATFORM" = claude ] || HEADING_SOURCE=" · $DEVLOG_PLATFORM"
291
293
  {
292
- printf '## Round %s — %s\n\n' "$NEXT_N" "$TS"
294
+ printf '## Round %s — %s%s\n\n' "$NEXT_N" "$TS" "$HEADING_SOURCE"
293
295
  printf '### User Input\n'
294
296
  printf '```text\n'
295
297
  printf '%s\n' "$PROMPT"
@@ -302,13 +304,14 @@ elif [ "$SPAN_SKIP" -eq 0 ]; then
302
304
 
303
305
  if [ -f "$ROUND_CURRENT" ]; then
304
306
  printf '{"round": %s, "opened_at": "%s", "file": "%s"}\n' "$NEXT_N" "$TS" "${DEVLOG_FILE##*/}" > "$ROUND_OPEN" 2>/dev/null || true
307
+ [ "$DEVLOG_PLATFORM" = claude ] || printf '這一輪寫在 .devlog/%s。\n' "${ROUND_CURRENT##*/}"
305
308
  fi
306
309
  fi
307
310
 
308
311
  if [ -f "$ROUND_CURRENT" ]; then
309
- cksum < "$ROUND_CURRENT" > "$DEVLOG_DIR/.turn-start" 2>/dev/null || true
312
+ cksum < "$ROUND_CURRENT" > "$TURN_MARKER" 2>/dev/null || true
310
313
  else
311
- echo "MISSING" > "$DEVLOG_DIR/.turn-start" 2>/dev/null || true
314
+ echo "MISSING" > "$TURN_MARKER" 2>/dev/null || true
312
315
  fi
313
316
 
314
317
  SPAN_WILL_PASS_THROUGH=0
@@ -337,6 +340,14 @@ if [ "$SPAN_WILL_PASS_THROUGH" -eq 0 ] && [ -z "$FOLD_ROUND" ] && [ -f "$CHECKPO
337
340
  esac
338
341
  fi
339
342
 
343
+ CLAUDE_SEGMENT="$DEVLOG_DIR/.segment-state"
344
+ if [ "$SEGMENT_FILE" != "$CLAUDE_SEGMENT" ] && [ ! -f "$SEGMENT_FILE" ] && [ -f "$CLAUDE_SEGMENT" ]; then
345
+ SEED_MAX="$(json_int_get "$CLAUDE_SEGMENT" max_silent_seconds)"
346
+ case "$SEED_MAX" in ''|*[!0-9]*) SEED_MAX=600 ;; esac
347
+ printf '{"last_change_epoch": 0, "last_seen_cksum": "", "max_silent_seconds": %s, "session_id": ""}\n' \
348
+ "$SEED_MAX" > "$SEGMENT_FILE" 2>/dev/null || true
349
+ fi
350
+
340
351
  if [ -f "$SEGMENT_FILE" ]; then
341
352
  SEG_EPOCH="$(json_int_get "$SEGMENT_FILE" last_change_epoch)"
342
353
  SEG_MAX="$(json_int_get "$SEGMENT_FILE" max_silent_seconds)"
@@ -24,21 +24,25 @@ case "$SECONDS_ARG" in
24
24
  ;;
25
25
  esac
26
26
 
27
- if [ -f "$DEVLOG_DIR/.segment-state" ]; then
28
- json_int_set "$DEVLOG_DIR/.segment-state" max_silent_seconds "$SECONDS_ARG"
29
- APPLIED="$(json_int_get "$DEVLOG_DIR/.segment-state" max_silent_seconds)"
27
+ APPLIED_ANY=0
28
+ for SEG in "$DEVLOG_DIR"/.segment-state "$DEVLOG_DIR"/.segment-state@*; do
29
+ [ -f "$SEG" ] || continue
30
+ APPLIED_ANY=1
31
+ json_int_set "$SEG" max_silent_seconds "$SECONDS_ARG"
32
+ APPLIED="$(json_int_get "$SEG" max_silent_seconds)"
30
33
  if [ "$APPLIED" != "$SECONDS_ARG" ]; then
31
34
  # Existing file didn't have a matching max_silent_seconds key to patch
32
35
  # (malformed / hand-edited) -- rebuild it fresh rather than leave the
33
36
  # requested value silently unapplied.
34
- LAST_EPOCH="$(json_int_get "$DEVLOG_DIR/.segment-state" last_change_epoch)"
35
- LAST_SUM="$(json_str_get "$DEVLOG_DIR/.segment-state" last_seen_cksum)"
36
- SESSION="$(json_str_get "$DEVLOG_DIR/.segment-state" session_id)"
37
+ LAST_EPOCH="$(json_int_get "$SEG" last_change_epoch)"
38
+ LAST_SUM="$(json_str_get "$SEG" last_seen_cksum)"
39
+ SESSION="$(json_str_get "$SEG" session_id)"
37
40
  case "$LAST_EPOCH" in ''|*[!0-9]*) LAST_EPOCH=0 ;; esac
38
41
  printf '%s\n' "{\"last_change_epoch\": ${LAST_EPOCH}, \"last_seen_cksum\": \"${LAST_SUM}\", \"max_silent_seconds\": ${SECONDS_ARG}, \"session_id\": \"${SESSION}\"}" \
39
- > "$DEVLOG_DIR/.segment-state" || exit 1
42
+ > "$SEG" || exit 1
40
43
  fi
41
- else
44
+ done
45
+ if [ "$APPLIED_ANY" -eq 0 ]; then
42
46
  printf '%s\n' "{\"last_change_epoch\": 0, \"last_seen_cksum\": \"\", \"max_silent_seconds\": ${SECONDS_ARG}, \"session_id\": \"\"}" \
43
47
  > "$DEVLOG_DIR/.segment-state" || exit 1
44
48
  fi
@@ -16,8 +16,6 @@ SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
16
16
  PROJECT_DIR="${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}"
17
17
  [ -f "$PROJECT_DIR/.devlog/.enabled" ] || exit 0
18
18
  devlog_resolve_paths "$PROJECT_DIR"
19
- ROUND_CURRENT="$DEVLOG_DIR/.round-current.md"
20
- SEGMENT_FILE="$DEVLOG_DIR/.segment-state"
21
19
 
22
20
  INPUT="$(cat 2>/dev/null || true)"
23
21
  [ -n "$INPUT" ] || exit 0
@@ -66,7 +64,7 @@ is_devlog_tool_allowed() {
66
64
  case "$1" in
67
65
  Write|Edit|StrReplace|Read|Grep)
68
66
  case "$2" in
69
- .devlog/.round-current.md|*/.devlog/.round-current.md) return 0 ;;
67
+ ".devlog/${ROUND_CURRENT##*/}"|*"/.devlog/${ROUND_CURRENT##*/}") return 0 ;;
70
68
  esac
71
69
  ;;
72
70
  Bash)
@@ -82,7 +80,6 @@ if [ -n "$AGENT_ID" ]; then
82
80
  exit 0
83
81
  fi
84
82
 
85
- MISMATCH_FILE="$DEVLOG_DIR/.workspace-mismatch"
86
83
  if [ -f "$MISMATCH_FILE" ]; then
87
84
  LIVE_MARK="$(cat "$MISMATCH_FILE" 2>/dev/null || true)"
88
85
  if [ -z "$LIVE_MARK" ]; then
@@ -103,7 +100,7 @@ if [ -f "$MISMATCH_FILE" ]; then
103
100
  *"$mark_flat"*) rm -f "$MISMATCH_FILE" 2>/dev/null || true ;;
104
101
  *)
105
102
  if ! is_devlog_tool_allowed "$TOOL_NAME" "$FILE_PATH" "$COMMAND"; then
106
- echo "上一輪 Handoff 的工作區(\`<workspace>\`;舊格式是 \`#### 工作區\`)跟目前 git 不符。請先 Read .devlog/.round-current.md,再用 Edit/StrReplace 在這一輪追加 ### 段落,把下面「實際」逐字貼進段落(宣稱 vs 實際)。寫完再呼叫其他工具。不要照上一輪 Handoff「現況/下一步」的字面行動。(唯讀的 git status/diff/log/show/rev-parse 仍可執行,方便自行核對。)" >&2
103
+ echo "上一輪 Handoff 的工作區(\`<workspace>\`;舊格式是 \`#### 工作區\`)跟目前 git 不符。請先 Read .devlog/${ROUND_CURRENT##*/},再用 Edit/StrReplace 在這一輪追加 ### 段落,把下面「實際」逐字貼進段落(宣稱 vs 實際)。寫完再呼叫其他工具。不要照上一輪 Handoff「現況/下一步」的字面行動。(唯讀的 git status/diff/log/show/rev-parse 仍可執行,方便自行核對。)" >&2
107
104
  echo "" >&2
108
105
  echo "實際:" >&2
109
106
  printf '%s\n' "$LIVE_MARK" >&2
@@ -190,7 +187,7 @@ fi
190
187
 
191
188
  ELAPSED=$((NOW - SEG_EPOCH))
192
189
  if [ "$ELAPSED" -ge "$SEG_MAX" ]; then
193
- echo "這一輪已經 ${ELAPSED} 秒沒有更新 .devlog/.round-current.md(門檻 ${SEG_MAX} 秒)。請先 Read .devlog/.round-current.md,再用 Edit 或 StrReplace **追加**一段「### 段落」(一行也可以);禁止用 Write 覆寫整份檔。寫完再繼續呼叫其他工具。(唯讀的 git status/diff/log/show/rev-parse 仍可執行。)" >&2
190
+ echo "這一輪已經 ${ELAPSED} 秒沒有更新 .devlog/${ROUND_CURRENT##*/}(門檻 ${SEG_MAX} 秒)。請先 Read .devlog/${ROUND_CURRENT##*/},再用 Edit 或 StrReplace **追加**一段「### 段落」(一行也可以);禁止用 Write 覆寫整份檔。寫完再繼續呼叫其他工具。(唯讀的 git status/diff/log/show/rev-parse 仍可執行。)" >&2
194
191
  exit 2
195
192
  fi
196
193
 
@@ -21,6 +21,9 @@
21
21
  # 非空就整份印出來、清楚標示「尚未收尾」,跟上面的歷史摘要分開。startup /
22
22
  # resume / fork 這幾個 source 因為上面已經先 heal 過,這裡通常是空的、印出
23
23
  # 來是 no-op,不特別排除,寫法比較單純。讀不到就跳過,fail-open。
24
+ #
25
+ # 其他平台的 Session Handoff:只注入本平台的 handoff 檔;其他平台非空的
26
+ # handoff 只印一行路徑提醒,不注入內容。
24
27
 
25
28
  set -uo pipefail
26
29
 
@@ -35,15 +38,13 @@ HOOKS_DIR="$(cd "${_src%/*}" && pwd)"
35
38
 
36
39
  PROJECT_DIR="${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}"
37
40
  devlog_resolve_paths "$PROJECT_DIR"
38
- ROUND_CURRENT="$DEVLOG_DIR/.round-current.md"
39
- SPAN_FILE="$DEVLOG_DIR/.span-open"
40
41
 
41
42
  INPUT="$(cat 2>/dev/null || true)"
42
43
  SOURCE="$(json_str_field "$INPUT" source)"
43
44
 
44
45
  case "$SOURCE" in
45
46
  startup|resume|clear|fork)
46
- if [ -f "$DEVLOG_DIR/.round-open" ]; then
47
+ if [ -f "$ROUND_OPEN" ]; then
47
48
  DANGLING_DETAIL=""
48
49
  if [ -n "$(detect_pending_question "$(json_str_field "$INPUT" transcript_path)")" ]; then
49
50
  DANGLING_DETAIL="awaiting_question"
@@ -64,7 +65,7 @@ if [ -f "$SPAN_FILE" ]; then
64
65
  if [ -n "$SPAN_ROUND" ] && [ -n "$SPAN_OPENED_AT" ]; then
65
66
  echo "⚠️ 有一個開啟中的 span:Round ${SPAN_ROUND},從 ${SPAN_OPENED_AT} 開始,"
66
67
  echo "還沒有正式結束。請先確認要繼續這個自動化任務,還是要明確關閉它"
67
- echo "(刪除 .devlog/.span-open 並補寫收尾的 Round)。"
68
+ echo "(刪除 .devlog/${SPAN_FILE##*/} 並補寫收尾的 Round)。"
68
69
  echo ""
69
70
  fi
70
71
  fi
@@ -76,6 +77,11 @@ if [ -s "${HANDOFF_FILE:-}" ]; then
76
77
  echo ""
77
78
  fi
78
79
 
80
+ devlog_other_handoffs | while IFS="$(printf '\t')" read -r OTHER_PLATFORM OTHER_FILE; do
81
+ echo "另一個平台(${OTHER_PLATFORM})有未完成的交接:.devlog/${OTHER_FILE##*/}"
82
+ echo ""
83
+ done
84
+
79
85
  if [ -f "$DEVLOG_FILE" ]; then
80
86
  echo "以下是本專案 .devlog/${DEVLOG_FILE##*/} 的接手摘要(不是全文;完整紀錄請自行讀取原檔):"
81
87
  echo ""
@@ -155,7 +161,7 @@ fi
155
161
  # heal) with its real content sitting only in .round-current.md. Surface it
156
162
  # separately from the devlog.md excerpt above so it isn't silently dropped.
157
163
  if [ -s "$ROUND_CURRENT" ]; then
158
- echo "以下是目前還沒收尾、仍在 .devlog/.round-current.md 裡的這一輪內容(跟上面的歷史摘要分開,尚未併入 devlog.md):"
164
+ echo "以下是目前還沒收尾、仍在 .devlog/${ROUND_CURRENT##*/} 裡的這一輪內容(跟上面的歷史摘要分開,尚未併入 devlog.md):"
159
165
  echo ""
160
166
  cat "$ROUND_CURRENT" 2>/dev/null || true
161
167
  echo ""
@@ -1,9 +1,13 @@
1
1
  #!/usr/bin/env bash
2
2
  set -uo pipefail
3
- DEVLOG_DIR="${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}/.devlog"
4
- if [ ! -e "$DEVLOG_DIR/.span-open" ]; then
3
+ _src="${BASH_SOURCE[0]}"
4
+ SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
5
+ # shellcheck source=devlog-path.sh
6
+ . "$SCRIPT_DIR/devlog-path.sh"
7
+ devlog_resolve_paths "${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}"
8
+ if [ ! -e "$SPAN_FILE" ]; then
5
9
  echo "NOT_OPEN"
6
10
  exit 0
7
11
  fi
8
- rm -f "$DEVLOG_DIR/.span-open" || exit 1
12
+ rm -f "$SPAN_FILE" || exit 1
9
13
  echo "CLOSED"
@@ -8,14 +8,14 @@ SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
8
8
 
9
9
  devlog_resolve_paths "${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}"
10
10
  [ -f "$DEVLOG_DIR/.enabled" ] || { echo "NOT_ENABLED" >&2; exit 1; }
11
- [ ! -e "$DEVLOG_DIR/.span-open" ] || { echo "ALREADY_OPEN" >&2; exit 1; }
11
+ [ ! -e "$SPAN_FILE" ] || { echo "ALREADY_OPEN" >&2; exit 1; }
12
12
  ROUND=""
13
- [ ! -f "$DEVLOG_DIR/.round-open" ] || ROUND="$(json_int_get "$DEVLOG_DIR/.round-open" round)"
13
+ [ ! -f "$ROUND_OPEN" ] || ROUND="$(json_int_get "$ROUND_OPEN" round)"
14
14
  if [ -z "$ROUND" ] && [ -f "$DEVLOG_FILE" ]; then
15
- ROUND="$(devlog_list_round_starts "$DEVLOG_FILE" | awk 'END { print $2 }')"
15
+ ROUND="$(devlog_list_round_starts_of "$DEVLOG_FILE" "$DEVLOG_PLATFORM" | awk 'END { print $2 }')"
16
16
  fi
17
17
  [ -n "$ROUND" ] || { echo "NO_ROUND" >&2; exit 1; }
18
18
  OPENED_AT="$(date -Iseconds 2>/dev/null || date '+%Y-%m-%dT%H:%M:%S%z')"
19
19
  printf '{"round": %s, "opened_at": "%s", "ticks_since_checkin": 0, "max_silent_ticks": 5}\n' \
20
- "$ROUND" "$OPENED_AT" > "$DEVLOG_DIR/.span-open" || exit 1
20
+ "$ROUND" "$OPENED_AT" > "$SPAN_FILE" || exit 1
21
21
  printf 'OPENED=%s\n' "$ROUND"
@@ -16,11 +16,11 @@ if [ -f "$DEVLOG_DIR/.lessons-advisory-state" ]; then
16
16
  advisory_max="$(json_int_get "$DEVLOG_DIR/.lessons-advisory-state" threshold)"
17
17
  printf 'LESSONS_ADVISORY=%s/%s\n' "${advisory_count:-0}" "${advisory_max:-3}"
18
18
  fi
19
- if [ -f "$DEVLOG_DIR/.span-open" ]; then
20
- r="$(json_int_get "$DEVLOG_DIR/.span-open" round)"
21
- opened="$(json_str_get "$DEVLOG_DIR/.span-open" opened_at)"
22
- ticks="$(json_int_get "$DEVLOG_DIR/.span-open" ticks_since_checkin)"
23
- max="$(json_int_get "$DEVLOG_DIR/.span-open" max_silent_ticks)"
19
+ if [ -f "$SPAN_FILE" ]; then
20
+ r="$(json_int_get "$SPAN_FILE" round)"
21
+ opened="$(json_str_get "$SPAN_FILE" opened_at)"
22
+ ticks="$(json_int_get "$SPAN_FILE" ticks_since_checkin)"
23
+ max="$(json_int_get "$SPAN_FILE" max_silent_ticks)"
24
24
  printf 'SPAN=%s,%s,%s,%s\n' "${r:-?}" "${opened:-?}" "${ticks:-?}" "${max:-5}"
25
25
  else
26
26
  echo "SPAN=closed"
@@ -28,7 +28,7 @@ fi
28
28
  rounds="$(json_int_get "$DEVLOG_DIR/.checkpoint-state" rounds_since_checkpoint)"
29
29
  checkpoint_max="$(json_int_get "$DEVLOG_DIR/.checkpoint-state" max_silent_rounds)"
30
30
  printf 'CHECKPOINT=%s/%s\n' "${rounds:-0}" "${checkpoint_max:-20}"
31
- seconds="$(json_int_get "$DEVLOG_DIR/.segment-state" max_silent_seconds)"
31
+ seconds="$(json_int_get "$SEGMENT_FILE" max_silent_seconds)"
32
32
  printf 'SEGMENT=max_silent_seconds=%s\n' "${seconds:-600}"
33
33
 
34
34
  status="none"
@@ -103,6 +103,15 @@ grep -q 'sentinel' "$DEVLOG_DIR/.awaiting-reply" \
103
103
  && echo "PASS: existing marker left untouched" || { echo "FAIL: existing marker was overwritten"; FAIL=1; }
104
104
  rm -f "$DEVLOG_DIR/.awaiting-reply" "$DEVLOG_DIR/devlog.md"
105
105
 
106
+ # --- platform: codex falls back to its own last round ----------------------
107
+ rm -f "$DEVLOG_DIR/.round-open" "$DEVLOG_DIR/.awaiting-reply" "$DEVLOG_DIR/.awaiting-reply@codex"
108
+ printf '## Round 7 — t · codex\n\n### Status\nDONE\n\n## Round 8 — t\n\n### Status\nDONE\n' > "$DEVLOG_DIR/devlog.md"
109
+ touch "$DEVLOG_DIR/.platform-claimed"
110
+ OUT="$(DEVLOG_PLATFORM=codex bash "$SCRIPT_DIR/await-open.sh")"
111
+ assert_contains "codex await-open uses its own last round" "OPENED=7" "$OUT"
112
+ [ -f "$DEVLOG_DIR/.awaiting-reply@codex" ] && [ ! -f "$DEVLOG_DIR/.awaiting-reply" ] \
113
+ && echo "PASS: codex awaiting file" || { echo "FAIL: codex awaiting file"; FAIL=1; }
114
+
106
115
  if [ "$FAIL" -eq 0 ]; then
107
116
  echo "All checks passed."
108
117
  exit 0
@@ -475,6 +475,36 @@ NONGIT_DIR="$TMP_ROOT/nongit"
475
475
  mkdir -p "$NONGIT_DIR"
476
476
  assert_eq "unclosed fence still yields a claim (MISMATCH, not NO_CLAIM)" "MISMATCH" "$(workspace_claim_state "$NONGIT_DIR" "$TMP_ROOT/unclosed-fence.md")"
477
477
 
478
+ WS_FILE="$TMP_ROOT/per-platform-claim.md"
479
+ cat > "$WS_FILE" <<'EOF'
480
+ ## Round 1 — 2026-09-27T10:00:00+0800
481
+
482
+ ### Handoff
483
+ <handoff>
484
+ <workspace>
485
+ 非 git 工作區
486
+ </workspace>
487
+ </handoff>
488
+
489
+ ### Status
490
+ IN_PROGRESS
491
+
492
+ ## Round 2 — 2026-09-27T10:05:00+0800 · codex
493
+
494
+ ### Handoff
495
+ <handoff>
496
+ <workspace>
497
+ bogus
498
+ </workspace>
499
+ </handoff>
500
+
501
+ ### Status
502
+ IN_PROGRESS
503
+ EOF
504
+ assert_eq "claude claim ignores codex's later round" "MATCH" "$(workspace_claim_state "$NONGIT_DIR" "$WS_FILE" claude)"
505
+ assert_eq "claude is the default platform" "MATCH" "$(workspace_claim_state "$NONGIT_DIR" "$WS_FILE")"
506
+ assert_eq "codex claim sees its own round" "MISMATCH" "$(workspace_claim_state "$NONGIT_DIR" "$WS_FILE" codex)"
507
+
478
508
  cat > "$TMP_ROOT/unclosed-fence-segments.md" <<'EOF'
479
509
  ## Round 1 — 2026-09-11T00:00:00+08:00
480
510
 
@@ -515,30 +545,61 @@ devlog_merge_round_current "$MERGE_MAIN" "$MERGE_CUR"
515
545
  AFTER="$(cat "$MERGE_MAIN")"
516
546
  if [ "$BEFORE" = "$AFTER" ]; then echo "PASS: merge no-op when round-current absent"; else echo "FAIL: merge no-op when round-current absent"; FAIL=1; fi
517
547
 
518
- # --- devlog_reopen_last_round ------------------------------------------
519
- REOPEN_MAIN="$TMP_ROOT/.devlog/reopen-main.md"
520
- REOPEN_CUR="$TMP_ROOT/.devlog/reopen-current.md"
521
- rm -f "$REOPEN_CUR"
548
+ # --- per-platform round lookup --------------------------------------------
549
+ MP="$TMP_ROOT/mp.md"
550
+ cat > "$MP" <<'EOF'
551
+ # 專案摘要
552
+
553
+ ## Round 1 — 2026-09-27T10:00:00+0800
554
+
555
+ ### Status
556
+ DONE
522
557
 
523
- printf '## Round 1 — 2026-09-17T09:00:00+0800\n\n### Summary\ns1\n\n### Handoff\n#### 現況\nc1\n\n### Status\nDONE\n\n## Round 2 — 2026-09-17T09:10:00+0800\n\n### User Input\n```text\n問題?\n```\n\n### Summary\ns2\n\n### Handoff\n#### 現況\nBLOCKED 等答案\n\n### Status\nBLOCKED\n' > "$REOPEN_MAIN"
558
+ ## Round 3 — 2026-09-27T10:05:00+0800 · codex
524
559
 
525
- devlog_reopen_last_round "$REOPEN_MAIN" "$REOPEN_CUR"
526
- RC=$?
527
- assert_exit "reopen: returns 0 when a round exists" 0 "$RC"
560
+ ### Status
561
+ IN_PROGRESS
528
562
 
529
- REOPENED="$(cat "$REOPEN_CUR" 2>/dev/null || echo '')"
530
- assert_contains "reopen: round 2 moved into round-current" "## Round 2" "$REOPENED"
531
- assert_not_contains "reopen: round 1 not pulled along" "## Round 1" "$REOPENED"
563
+ ## Round 2 — 2026-09-27T10:01:00+0800
532
564
 
533
- REMAINING="$(cat "$REOPEN_MAIN")"
534
- assert_contains "reopen: round 1 stays in main file" "## Round 1" "$REMAINING"
535
- assert_not_contains "reopen: round 2 removed from main file" "## Round 2" "$REMAINING"
565
+ ```text
566
+ ## Round 9 — fenced · codex
567
+ ```
568
+
569
+ ### Status
570
+ DONE
571
+
572
+ ## Round 4 — 2026-09-27T10:09:00+0800 · cursor
573
+
574
+ ### Status
575
+ DONE
576
+ EOF
577
+ assert_eq "claude rounds" "3 1
578
+ 13 2" "$(devlog_list_round_starts_of "$MP" claude)"
579
+ assert_eq "codex rounds (fenced ignored)" "8 3" "$(devlog_list_round_starts_of "$MP" codex)"
580
+ assert_eq "cursor rounds" "22 4" "$(devlog_list_round_starts_of "$MP" cursor)"
581
+ assert_eq "by number: middle round" "8" "$(devlog_round_start_by_number "$MP" 3)"
582
+ assert_eq "by number: missing" "" "$(devlog_round_start_by_number "$MP" 8)"
583
+
584
+ # --- devlog_reopen_round ----------------------------------------------------
585
+ REOPEN_MAIN="$TMP_ROOT/reopen.md"
586
+ REOPEN_CUR="$TMP_ROOT/reopen-cur.md"
587
+ cp "$MP" "$REOPEN_MAIN"
588
+ devlog_reopen_round "$REOPEN_MAIN" "$REOPEN_CUR" 3
589
+ assert_eq "reopen middle: returns 0" "0" "$?"
590
+ assert_contains "reopen middle: current has round 3" "## Round 3 — 2026-09-27T10:05:00+0800 · codex" "$(cat "$REOPEN_CUR")"
591
+ assert_not_contains "reopen middle: devlog lost round 3" "## Round 3 " "$(cat "$REOPEN_MAIN")"
592
+ assert_eq "reopen middle: others stay in order" "1 2 4 " "$(devlog_list_round_starts "$REOPEN_MAIN" | awk '{printf "%s ", $2}')"
593
+ devlog_reopen_round "$REOPEN_MAIN" "$TMP_ROOT/should-not-exist.md" 3
594
+ assert_eq "reopen missing: returns 1" "1" "$?"
595
+ assert_file_absent "reopen missing: no current file" "$TMP_ROOT/should-not-exist.md"
536
596
 
537
597
  # reopen on a file with no round at all fails without touching either file
598
+ mkdir -p "$TMP_ROOT/.devlog"
538
599
  EMPTY_MAIN="$TMP_ROOT/.devlog/empty-main.md"
539
600
  : > "$EMPTY_MAIN"
540
601
  rm -f "$TMP_ROOT/.devlog/should-not-exist.md"
541
- devlog_reopen_last_round "$EMPTY_MAIN" "$TMP_ROOT/.devlog/should-not-exist.md"
602
+ devlog_reopen_round "$EMPTY_MAIN" "$TMP_ROOT/.devlog/should-not-exist.md" 1
542
603
  assert_exit "reopen: returns 1 when no round exists" 1 $?
543
604
  assert_file_absent "reopen: no round-current created on failure" "$TMP_ROOT/.devlog/should-not-exist.md"
544
605
 
@@ -288,5 +288,131 @@ devlog_resolve_paths "$DETACHEDREPO"
288
288
  && echo "PASS: detached HANDOFF_FILE uses worktree dirname" \
289
289
  || { echo "FAIL: detached HANDOFF_FILE=$HANDOFF_FILE"; FAIL=1; }
290
290
 
291
+ # --- platform identity ---------------------------------------------------
292
+ assert_path() {
293
+ if [ "$2" = "$3" ]; then echo "PASS: $1"; else echo "FAIL: $1 (expected [$2], got [$3])"; FAIL=1; fi
294
+ }
295
+ assert_path "unset platform is claude" claude "$(unset DEVLOG_PLATFORM; devlog_platform)"
296
+ assert_path "codex platform" codex "$(DEVLOG_PLATFORM=codex devlog_platform)"
297
+ assert_path "unknown platform falls back to claude" claude "$(DEVLOG_PLATFORM='x;rm' devlog_platform)"
298
+ assert_path "claude keeps plain name" .round-current.md "$(devlog_platform_file .round-current .md claude)"
299
+ assert_path "codex gets @ suffix" .round-current@codex.md "$(devlog_platform_file .round-current .md codex)"
300
+ assert_path "extensionless @ suffix" .round-open@cursor "$(devlog_platform_file .round-open '' cursor)"
301
+
302
+ # --- per-platform state paths --------------------------------------------
303
+ PLATREPO="$TMP/platrepo"
304
+ mkdir -p "$PLATREPO"
305
+ git_setup "$PLATREPO"
306
+ git -C "$PLATREPO" branch -M main
307
+ DEVLOG_PLATFORM=codex devlog_resolve_paths "$PLATREPO"
308
+ assert_path "codex ROUND_CURRENT" "$PLATREPO/.devlog/.round-current@codex.md" "$ROUND_CURRENT"
309
+ assert_path "codex ROUND_OPEN" "$PLATREPO/.devlog/.round-open@codex" "$ROUND_OPEN"
310
+ assert_path "codex TURN_MARKER" "$PLATREPO/.devlog/.turn-start@codex" "$TURN_MARKER"
311
+ assert_path "codex SEGMENT_FILE" "$PLATREPO/.devlog/.segment-state@codex" "$SEGMENT_FILE"
312
+ assert_path "codex AWAITING_FILE" "$PLATREPO/.devlog/.awaiting-reply@codex" "$AWAITING_FILE"
313
+ assert_path "codex INTERRUPTED_FLAG" "$PLATREPO/.devlog/.interrupted@codex" "$INTERRUPTED_FLAG"
314
+ assert_path "codex SPAN_FILE" "$PLATREPO/.devlog/.span-open@codex" "$SPAN_FILE"
315
+ assert_path "codex MISMATCH_FILE" "$PLATREPO/.devlog/.workspace-mismatch@codex" "$MISMATCH_FILE"
316
+ assert_path "codex main HANDOFF_FILE" "$PLATREPO/.devlog/handoff@codex.md" "$HANDOFF_FILE"
317
+ assert_path "codex DEVLOG_FILE shared" "$PLATREPO/.devlog/devlog.md" "$DEVLOG_FILE"
318
+ unset DEVLOG_PLATFORM
319
+ devlog_resolve_paths "$PLATREPO"
320
+ assert_path "claude ROUND_CURRENT unchanged" "$PLATREPO/.devlog/.round-current.md" "$ROUND_CURRENT"
321
+ assert_path "claude ROUND_OPEN unchanged" "$PLATREPO/.devlog/.round-open" "$ROUND_OPEN"
322
+ assert_path "claude HANDOFF_FILE unchanged" "$PLATREPO/.devlog/handoff.md" "$HANDOFF_FILE"
323
+ git -C "$PLATREPO" checkout -q -b codex
324
+ DEVLOG_PLATFORM=cursor devlog_resolve_paths "$PLATREPO"
325
+ assert_path "branch named codex + cursor platform" "$PLATREPO/.devlog/handoff.codex@cursor.md" "$HANDOFF_FILE"
326
+ assert_path "HANDOFF_STEM" "$PLATREPO/.devlog/handoff.codex" "$HANDOFF_STEM"
327
+ unset DEVLOG_PLATFORM
328
+
329
+ # --- legacy claim ----------------------------------------------------------
330
+ CLAIMREPO="$TMP/claimrepo"
331
+ mkdir -p "$CLAIMREPO/.devlog"
332
+ git_setup "$CLAIMREPO"
333
+ git -C "$CLAIMREPO" branch -M main
334
+ touch "$CLAIMREPO/.devlog/.enabled"
335
+ echo "open round" > "$CLAIMREPO/.devlog/.round-current.md"
336
+ printf '{"round": 3, "opened_at": "t", "file": "devlog.md"}\n' > "$CLAIMREPO/.devlog/.round-open"
337
+ echo "legacy handoff" > "$CLAIMREPO/.devlog/handoff.md"
338
+ echo "segment cfg" > "$CLAIMREPO/.devlog/.segment-state"
339
+ DEVLOG_PLATFORM=codex devlog_resolve_paths "$CLAIMREPO"
340
+ if [ -f "$CLAIMREPO/.devlog/.round-current@codex.md" ] && [ ! -f "$CLAIMREPO/.devlog/.round-current.md" ] \
341
+ && [ -f "$CLAIMREPO/.devlog/.round-open@codex" ] \
342
+ && grep -q "legacy handoff" "$CLAIMREPO/.devlog/handoff@codex.md" \
343
+ && [ -f "$CLAIMREPO/.devlog/.segment-state" ] \
344
+ && [ -f "$CLAIMREPO/.devlog/.platform-claimed" ]; then
345
+ echo "PASS: first non-claude platform claims legacy state (segment config stays)"
346
+ else
347
+ echo "FAIL: legacy claim"; ls -a "$CLAIMREPO/.devlog"; FAIL=1
348
+ fi
349
+ echo "later claude round" > "$CLAIMREPO/.devlog/.round-current.md"
350
+ DEVLOG_PLATFORM=cursor devlog_resolve_paths "$CLAIMREPO"
351
+ if [ -f "$CLAIMREPO/.devlog/.round-current.md" ] && [ ! -f "$CLAIMREPO/.devlog/.round-current@cursor.md" ]; then
352
+ echo "PASS: marker stops later claims"
353
+ else
354
+ echo "FAIL: second claim happened"; FAIL=1
355
+ fi
356
+ unset DEVLOG_PLATFORM
357
+
358
+ CLAUDEFIRST="$TMP/claudefirst"
359
+ mkdir -p "$CLAUDEFIRST/.devlog"
360
+ git_setup "$CLAUDEFIRST"
361
+ git -C "$CLAUDEFIRST" branch -M main
362
+ touch "$CLAUDEFIRST/.devlog/.enabled"
363
+ echo "open round" > "$CLAUDEFIRST/.devlog/.round-current.md"
364
+ devlog_resolve_paths "$CLAUDEFIRST"
365
+ if [ -f "$CLAUDEFIRST/.devlog/.round-current.md" ] && [ -f "$CLAUDEFIRST/.devlog/.platform-claimed" ]; then
366
+ echo "PASS: claude-first claims nothing, writes marker"
367
+ else
368
+ echo "FAIL: claude-first claim"; FAIL=1
369
+ fi
370
+
371
+ NOTENABLED="$TMP/notenabled"
372
+ mkdir -p "$NOTENABLED/.devlog"
373
+ echo "x" > "$NOTENABLED/.devlog/.round-current.md"
374
+ DEVLOG_PLATFORM=codex devlog_resolve_paths "$NOTENABLED"
375
+ if [ -f "$NOTENABLED/.devlog/.round-current.md" ] && [ ! -e "$NOTENABLED/.devlog/.platform-claimed" ]; then
376
+ echo "PASS: no claim without .enabled"
377
+ else
378
+ echo "FAIL: claimed while disabled"; FAIL=1
379
+ fi
380
+ unset DEVLOG_PLATFORM
381
+
382
+ # --- open rounds / other handoffs -----------------------------------------
383
+ LISTREPO="$TMP/listrepo"
384
+ mkdir -p "$LISTREPO/.devlog"
385
+ git_setup "$LISTREPO"
386
+ git -C "$LISTREPO" branch -M main
387
+ touch "$LISTREPO/.devlog/.enabled" "$LISTREPO/.devlog/.platform-claimed"
388
+ printf '{"round": 5, "opened_at": "t", "file": "devlog.md"}\n' > "$LISTREPO/.devlog/.round-open"
389
+ printf '{"round": 6, "opened_at": "t", "file": "devlog.md"}\n' > "$LISTREPO/.devlog/.round-open@codex"
390
+ echo "claude h" > "$LISTREPO/.devlog/handoff.md"
391
+ echo "cursor h" > "$LISTREPO/.devlog/handoff@cursor.md"
392
+ : > "$LISTREPO/.devlog/handoff@codex.md"
393
+ DEVLOG_PLATFORM=codex devlog_resolve_paths "$LISTREPO"
394
+ assert_path "open rounds listing" "$(printf 'claude\t5\tdevlog.md\ncodex\t6\tdevlog.md')" "$(devlog_open_rounds)"
395
+ assert_path "other handoffs (non-empty, not own)" \
396
+ "$(printf 'claude\t%s\ncursor\t%s' "$LISTREPO/.devlog/handoff.md" "$LISTREPO/.devlog/handoff@cursor.md")" \
397
+ "$(DEVLOG_PLATFORM=codex devlog_other_handoffs)"
398
+ unset DEVLOG_PLATFORM
399
+
400
+ # --- branch tail migration moves every platform's handoff -----------------
401
+ PTAIL="$TMP/ptail"
402
+ mig_repo "$PTAIL"
403
+ write_main_devlog "$PTAIL/.devlog/devlog.md" 1:DONE 2:IN_PROGRESS
404
+ touch "$PTAIL/.devlog/.platform-claimed"
405
+ echo "claude handoff" > "$PTAIL/.devlog/handoff.md"
406
+ echo "codex handoff" > "$PTAIL/.devlog/handoff@codex.md"
407
+ git -C "$PTAIL" checkout -q -b feature-p
408
+ devlog_resolve_paths "$PTAIL"
409
+ if grep -q "claude handoff" "$PTAIL/.devlog/handoff.feature-p.md" \
410
+ && grep -q "codex handoff" "$PTAIL/.devlog/handoff.feature-p@codex.md" \
411
+ && [ ! -f "$PTAIL/.devlog/handoff@codex.md" ]; then
412
+ echo "PASS: tail migration moves every platform's handoff"
413
+ else
414
+ echo "FAIL: platform handoff tail migration"; ls -a "$PTAIL/.devlog"; FAIL=1
415
+ fi
416
+
291
417
  if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
292
418
  else echo "Some checks FAILED."; exit 1; fi