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
@@ -0,0 +1,286 @@
1
+ #!/usr/bin/env bash
2
+ # Two platforms in one worktree (docs/design/multi-platform-concurrency.md).
3
+ # Run: bash core/scripts/tests/test-multi-platform.sh
4
+ set -uo pipefail
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
+ # shellcheck source=../workspace-snapshot.sh
7
+ . "$SCRIPT_DIR/workspace-snapshot.sh"
8
+ TMP_ROOT="$(mktemp -d)"
9
+ trap 'rm -rf "$TMP_ROOT"' EXIT
10
+ FAIL=0
11
+ pass() { echo "PASS: $1"; }
12
+ fail() { echo "FAIL: $1"; FAIL=1; }
13
+ check() { if eval "$2"; then pass "$1"; else fail "$1"; fi; }
14
+
15
+ new_project() {
16
+ P="$TMP_ROOT/$1"
17
+ mkdir -p "$P/.devlog"
18
+ git -C "$P" init -q
19
+ git -C "$P" -c user.email=t@t.t -c user.name=t commit -q --allow-empty -m init
20
+ git -C "$P" branch -M main
21
+ echo '.devlog/' > "$P/.gitignore"
22
+ git -C "$P" add .gitignore
23
+ git -C "$P" -c user.email=t@t.t -c user.name=t commit -q -m ignore
24
+ touch "$P/.devlog/.enabled" "$P/.devlog/.platform-claimed"
25
+ # shellcheck disable=SC2034 # read inside check's eval strings
26
+ D="$P/.devlog"
27
+ }
28
+ submit() { # platform prompt
29
+ printf '{"prompt":"%s","session_id":"%s-1"}' "$2" "$1" \
30
+ | DEVLOG_PLATFORM="$1" DEVLOG_PROJECT_DIR="$P" bash "$SCRIPT_DIR/round-start.sh"
31
+ }
32
+
33
+ # --- Task 3: codex writes only @codex files ---------------------------------
34
+ new_project refactor
35
+ submit codex "hello from codex" >/dev/null
36
+ check "codex round file" '[ -f "$D/.round-current@codex.md" ] && grep -q "hello from codex" "$D/.round-current@codex.md"'
37
+ check "codex marker" '[ -f "$D/.round-open@codex" ] && [ -f "$D/.turn-start@codex" ]'
38
+ check "claude files untouched" '[ ! -e "$D/.round-current.md" ] && [ ! -e "$D/.round-open" ] && [ ! -e "$D/.turn-start" ]'
39
+
40
+ # --- Task 4: the other platform's open round survives, numbers are unique -
41
+ new_project interleave
42
+ printf '%s\n' '{"last_change_epoch": 0, "last_seen_cksum": "", "max_silent_seconds": 600, "session_id": ""}' > "$D/.segment-state"
43
+ submit claude "claude task" >/dev/null
44
+ # shellcheck disable=SC2034 # read inside check's eval strings
45
+ OUT="$(submit codex "codex task")"
46
+ check "claude round still open" '[ -f "$D/.round-open" ] && grep -q "claude task" "$D/.round-current.md"'
47
+ check "claude round not INTERRUPTED" '! grep -q INTERRUPTED "$D/.round-current.md" && [ ! -s "$D/devlog.md" ]'
48
+ check "claude heading has no suffix" 'head -1 "$D/.round-current.md" | grep -q "^## Round 1 — " && ! head -1 "$D/.round-current.md" | grep -q " · "'
49
+ check "codex reserved round 2 with suffix" 'grep -q "^## Round 2 — .* · codex$" "$D/.round-current@codex.md"'
50
+ check "codex told its file" 'case "$OUT" in *".devlog/.round-current@codex.md"*) true ;; *) false ;; esac'
51
+ submit cursor "cursor task" >/dev/null
52
+ check "cursor reserved round 3" 'grep -q "^## Round 3 — .* · cursor$" "$D/.round-current@cursor.md"'
53
+
54
+ # Codex's own next prompt closes only codex's round.
55
+ submit codex "codex second" >/dev/null
56
+ check "codex dangling round merged as INTERRUPTED" 'grep -q "^## Round 2 — .* · codex$" "$D/devlog.md" && grep -q INTERRUPTED "$D/devlog.md"'
57
+ check "claude still open after codex re-submit" 'grep -q "claude task" "$D/.round-current.md"'
58
+ check "codex second is round 4" 'grep -q "^## Round 4 — .* · codex$" "$D/.round-current@codex.md"'
59
+ check "non-claude segment state seeded" '[ ! -f "$D/.segment-state" ] || [ -f "$D/.segment-state@codex" ]'
60
+
61
+ # Reply Fold finds codex's round by number even when it is not last.
62
+ new_project fold
63
+ cat > "$D/devlog.md" <<'EOF'
64
+ ## Round 1 — 2026-09-27T10:00:00+0800 · codex
65
+
66
+ ### Summary
67
+ asked a question
68
+
69
+ ### Reply
70
+ q?
71
+
72
+ ### Handoff
73
+ <handoff>
74
+ <state>
75
+ waiting
76
+ </state>
77
+ </handoff>
78
+
79
+ ### Status
80
+ DONE
81
+
82
+ ## Round 2 — 2026-09-27T10:02:00+0800
83
+
84
+ ### Summary
85
+ claude work
86
+
87
+ ### Reply
88
+ ok
89
+
90
+ ### Handoff
91
+ <handoff>
92
+ <state>
93
+ done
94
+ </state>
95
+ </handoff>
96
+
97
+ ### Status
98
+ DONE
99
+ EOF
100
+ printf '{"round": 1, "opened_at": "t"}\n' > "$D/.awaiting-reply@codex"
101
+ submit codex "the answer" >/dev/null
102
+ check "fold reopened codex round 1" 'grep -q "^## Round 1 — .* · codex$" "$D/.round-current@codex.md" && grep -q "the answer" "$D/.round-current@codex.md"'
103
+ check "claude round 2 stays in devlog" 'grep -q "^## Round 2 " "$D/devlog.md" && ! grep -q "^## Round 1 " "$D/devlog.md"'
104
+
105
+ # --- Task 5: both Stop in either order, handoffs coexist -------------------
106
+ close_round() { # platform status file
107
+ local f="$3"
108
+ cat >> "$f" <<EOF
109
+
110
+ ### Summary
111
+ closed by $1
112
+
113
+ ### Reply
114
+ ok
115
+
116
+ ### Handoff
117
+ <handoff>
118
+ <workspace>
119
+ $(workspace_snapshot "$P")
120
+ </workspace>
121
+ <state>
122
+ $1 state
123
+ </state>
124
+ <done-when>
125
+ tests pass
126
+ </done-when>
127
+ <next>
128
+ run \`bash core/scripts/run-tests.sh\`
129
+ </next>
130
+ </handoff>
131
+
132
+ ### Session Handoff
133
+ <session-handoff>
134
+ <decisions>
135
+ - $1 decision
136
+ </decisions>
137
+ <open-questions>
138
+ - (無)
139
+ </open-questions>
140
+ <failed-attempts>
141
+ - (無)
142
+ </failed-attempts>
143
+ </session-handoff>
144
+ EOF
145
+ # Replace the skeleton's Status with the requested one.
146
+ awk -v st="$2" '/^### Status$/ { print; getline; print st; next } { print }' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
147
+ printf '{}' | DEVLOG_PLATFORM="$1" DEVLOG_PROJECT_DIR="$P" bash "$SCRIPT_DIR/enforce-devlog.sh"
148
+ }
149
+ new_project stops_claude_first
150
+ submit claude "claude task" >/dev/null
151
+ submit codex "codex task" >/dev/null
152
+ close_round claude IN_PROGRESS "$D/.round-current.md" 2>"$TMP_ROOT/err0"; rc=$?
153
+ check "claude-first: claude Stop passes" '[ "$rc" -eq 0 ] || { cat "$TMP_ROOT/err0"; false; }'
154
+ check "claude-first: claude merged, codex still open" 'grep -q "closed by claude" "$D/devlog.md" && [ -f "$D/.round-open@codex" ] && [ ! -e "$D/.round-open" ]'
155
+ close_round codex IN_PROGRESS "$D/.round-current@codex.md" 2>"$TMP_ROOT/err0"; rc=$?
156
+ check "claude-first: codex Stop passes" '[ "$rc" -eq 0 ] || { cat "$TMP_ROOT/err0"; false; }'
157
+ check "claude-first: numbers unique" '[ "$(grep -o "^## Round [0-9]*" "$D/devlog.md" | tr "\n" ",")" = "## Round 1,## Round 2," ]'
158
+ check "claude-first: neither INTERRUPTED" '! grep -q INTERRUPTED "$D/devlog.md"'
159
+
160
+ new_project stops
161
+ submit claude "claude task" >/dev/null
162
+ submit codex "codex task" >/dev/null
163
+ close_round codex IN_PROGRESS "$D/.round-current@codex.md" 2>"$TMP_ROOT/err1"; rc=$?
164
+ check "codex Stop passes" '[ "$rc" -eq 0 ] || { cat "$TMP_ROOT/err1"; false; }'
165
+ check "codex merged, claude still open" 'grep -q "closed by codex" "$D/devlog.md" && [ -f "$D/.round-open" ] && [ ! -e "$D/.round-open@codex" ]'
166
+ check "codex handoff written" 'grep -q "codex decision" "$D/handoff@codex.md"'
167
+ close_round claude IN_PROGRESS "$D/.round-current.md" 2>"$TMP_ROOT/err2"; rc=$?
168
+ check "claude Stop passes" '[ "$rc" -eq 0 ] || { cat "$TMP_ROOT/err2"; false; }'
169
+ check "file order is close order, numbers unique" '[ "$(grep -o "^## Round [0-9]*" "$D/devlog.md" | tr "\n" ",")" = "## Round 2,## Round 1," ]'
170
+ check "handoffs coexist" 'grep -q "claude decision" "$D/handoff.md" && grep -q "codex decision" "$D/handoff@codex.md"'
171
+ check "neither INTERRUPTED" '! grep -q INTERRUPTED "$D/devlog.md"'
172
+
173
+ OUT="$(printf '{"source":"startup"}' | DEVLOG_PLATFORM=codex DEVLOG_PROJECT_DIR="$P" bash "$SCRIPT_DIR/session-start-devlog.sh")"
174
+ check "codex SessionStart injects own handoff" 'case "$OUT" in *"codex decision"*) true ;; *) false ;; esac'
175
+ check "codex SessionStart does not inject claude handoff" 'case "$OUT" in *"claude decision"*) false ;; *) true ;; esac'
176
+ check "codex SessionStart notes claude handoff" 'case "$OUT" in *"另一個平台(claude)有未完成的交接:.devlog/handoff.md"*) true ;; *) false ;; esac'
177
+
178
+ # Interrupt of one platform does not touch the other.
179
+ new_project interrupt
180
+ submit claude "claude task" >/dev/null
181
+ submit codex "codex task" >/dev/null
182
+ DEVLOG_PLATFORM=codex DEVLOG_PROJECT_DIR="$P" bash "$SCRIPT_DIR/close-open-round.sh" "Interrupt:cancelled"
183
+ check "codex interrupted round merged" 'grep -q "^## Round 2 — .* · codex$" "$D/devlog.md" && grep -q "Interrupt:cancelled" "$D/devlog.md"'
184
+ check "claude untouched by codex interrupt" '[ -f "$D/.round-open" ] && grep -q "claude task" "$D/.round-current.md"'
185
+
186
+ # span-open falls back to the platform's own last round.
187
+ new_project span
188
+ printf '## Round 7 — t · codex\n\n### Status\nDONE\n\n## Round 8 — t\n\n### Status\nDONE\n' > "$D/devlog.md"
189
+ # shellcheck disable=SC2034 # read inside check's eval strings
190
+ OUT="$(DEVLOG_PLATFORM=codex DEVLOG_PROJECT_DIR="$P" bash "$SCRIPT_DIR/span-open.sh")"
191
+ check "codex span-open uses its own last round" '[ "$OUT" = "OPENED=7" ] && grep -q "\"round\": 7" "$D/.span-open@codex" && [ ! -e "$D/.span-open" ]'
192
+
193
+ # --- Task 6: whole-devlog commands --------------------------------------
194
+ new_project cmds
195
+ submit claude "claude task" >/dev/null
196
+ submit codex "codex task" >/dev/null
197
+ DEVLOG_PROJECT_DIR="$P" bash "$SCRIPT_DIR/clean-devlog.sh" --confirmed >/dev/null 2>"$TMP_ROOT/cleanerr"; rc=$?
198
+ check "clean refuses with two open rounds" '[ "$rc" -ne 0 ] && grep -q "其他平台還有進行中的輪次" "$TMP_ROOT/cleanerr" && [ -f "$D/.round-current@codex.md" ]'
199
+ check "refusal names the pause/start escape hatch" 'grep -q "若該平台已不再使用,可先 pause 再 start 清掉它的標記。" "$TMP_ROOT/cleanerr"'
200
+ printf '## Round 9 — t\n\n### Status\nDONE\n' > "$D/devlog.md"
201
+ DEVLOG_PROJECT_DIR="$P" bash "$SCRIPT_DIR/keep-move.sh" --from 9 --to 9 --name x >/dev/null 2>"$TMP_ROOT/keeperr"; rc=$?
202
+ check "keep-move refuses with two open rounds" '[ "$rc" -ne 0 ] && grep -q "其他平台還有進行中的輪次(codex)" "$TMP_ROOT/keeperr" && [ ! -e "$D/devlog.x.md" ]'
203
+
204
+ DEVLOG_PROJECT_DIR="$P" bash "$SCRIPT_DIR/pause-devlog.sh" >/dev/null
205
+ check "pause clears every platform's markers" '[ ! -e "$D/.round-open" ] && [ ! -e "$D/.round-open@codex" ]'
206
+
207
+ new_project clean1
208
+ printf '## Round 1 — t\n\n### Status\nDONE\n\n## Round 2 — t\n\n### Status\nDONE\n' > "$D/devlog.md"
209
+ submit codex "only codex" >/dev/null
210
+ check "codex got round 3 before clean" 'grep -q "^## Round 3 — .* · codex$" "$D/.round-current@codex.md"'
211
+ echo "old claude handoff" > "$D/handoff.md"
212
+ echo "old codex handoff" > "$D/handoff@codex.md"
213
+ DEVLOG_PROJECT_DIR="$P" bash "$SCRIPT_DIR/clean-devlog.sh" --confirmed >/dev/null 2>&1
214
+ check "clean keeps the single open codex round as Round 1" 'grep -q "^## Round 1 — .* · codex$" "$D/.round-current@codex.md"'
215
+ check "clean removes every platform handoff" '[ ! -e "$D/handoff.md" ] && [ ! -e "$D/handoff@codex.md" ]'
216
+
217
+ # --- Span rounds are renumbered at merge when their number is taken -------
218
+ # A span-written round (no .round-open of its own) numbered by the LLM.
219
+ span_round() { # platform heading
220
+ local f="$D/.round-current@$1.md" t="$D/.turn-start@$1"
221
+ [ "$1" != claude ] || { f="$D/.round-current.md"; t="$D/.turn-start"; }
222
+ printf '%s\n\n### User Input\n```text\nspan tick\n```\n\n### Status\nIN_PROGRESS\n' "$2" > "$f"
223
+ echo stale > "$t"
224
+ close_round "$1" IN_PROGRESS "$f" 2>"$TMP_ROOT/spanerr"
225
+ }
226
+ headings() { grep -o '^## Round .*' "$D/devlog.md" | sed 's/ — [^ ]*//' | tr '\n' ','; }
227
+
228
+ new_project renum_reserved
229
+ printf '## Round 1 — t\n\n### Status\nDONE\n' > "$D/devlog.md"
230
+ submit codex "codex task" >/dev/null
231
+ span_round claude "## Round 2 — 2026-09-28T10:00:00+0800"
232
+ # shellcheck disable=SC2034 # read inside check's eval strings
233
+ rc=$?
234
+ check "span claude round passes Stop" '[ "$rc" -eq 0 ] || { cat "$TMP_ROOT/spanerr"; false; }'
235
+ check "claude span round 2 renumbered past codex reservation" '[ "$(headings)" = "## Round 1,## Round 3," ]'
236
+ check "renumbered heading keeps its timestamp" 'grep -q "^## Round 3 — 2026-09-28T10:00:00+0800$" "$D/devlog.md"'
237
+ close_round codex IN_PROGRESS "$D/.round-current@codex.md" 2>"$TMP_ROOT/spanerr"
238
+ check "codex reserved round keeps 2" '[ "$(headings)" = "## Round 1,## Round 3,## Round 2 · codex," ]'
239
+
240
+ new_project renum_existing
241
+ printf '## Round 1 — t\n\n### Status\nDONE\n\n## Round 2 — t · codex\n\n### Status\nDONE\n\n## Round 3 — t\n\n### Status\nDONE\n' > "$D/devlog.md"
242
+ span_round claude "## Round 3 — t"
243
+ check "span round whose number exists gets the next free one" '[ "$(headings)" = "## Round 1,## Round 2 · codex,## Round 3,## Round 4," ]'
244
+
245
+ new_project renum_suffix
246
+ printf '## Round 4 — t\n\n### Status\nDONE\n' > "$D/devlog.md"
247
+ span_round codex "## Round 5 — t"
248
+ check "codex span round without suffix gets it on merge" '[ "$(headings)" = "## Round 4,## Round 5 · codex," ]'
249
+
250
+ new_project renum_passthrough
251
+ submit claude "claude task" >/dev/null
252
+ submit codex "codex task" >/dev/null
253
+ # shellcheck disable=SC2034 # read inside check's eval strings
254
+ BEFORE="$(head -1 "$D/.round-current.md")"
255
+ close_round claude IN_PROGRESS "$D/.round-current.md" 2>/dev/null
256
+ check "reserved round heading passes through unchanged" '[ "$(grep "^## Round " "$D/devlog.md")" = "$BEFORE" ]'
257
+
258
+ new_project renum_own_dup
259
+ submit claude "claude task" >/dev/null
260
+ OWN_N="$(head -1 "$D/.round-current.md" | sed 's/^## Round \([0-9]*\).*/\1/')"
261
+ printf '\n## Round %s — dup\n\n### Status\nIN_PROGRESS\n' "$OWN_N" >> "$D/.round-current.md"
262
+ close_round claude IN_PROGRESS "$D/.round-current.md" 2>/dev/null
263
+ # shellcheck disable=SC2034 # read inside check's eval strings
264
+ DUP_N=$((OWN_N + 1))
265
+ check "second heading reusing the reserved number is renumbered" '[ "$(grep -c "^## Round $OWN_N " "$D/devlog.md")" -eq 1 ] && grep -q "^## Round $DUP_N — dup$" "$D/devlog.md"'
266
+
267
+ new_project renum_unwritable
268
+ printf '## Round 1 — t\n\n### Status\nDONE\n' > "$D/devlog.md"
269
+ printf '## Round 1 — t\n' > "$D/.round-current.md"
270
+ chmod a-w "$D"
271
+ # shellcheck disable=SC2034 # read inside check's eval strings
272
+ RENUM_ERR="$(DEVLOG_DIR="$D" DEVLOG_PLATFORM=claude bash -c '. "$1/devlog-path.sh"; . "$1/devlog-md.sh"; _devlog_renumber_current "$2" "$3"' _ "$SCRIPT_DIR" "$D/devlog.md" "$D/.round-current.md" 2>&1)"
273
+ chmod u+w "$D"
274
+ check "renumber stays quiet and leaves round-current alone when it cannot write" '[ -z "$RENUM_ERR" ] && [ "$(cat "$D/.round-current.md")" = "## Round 1 — t" ]'
275
+
276
+ # --- Reply Fold prefers the caller's own round when numbers repeat ---------
277
+ new_project fold_dup
278
+ printf '## Round 5 — t · codex\n\n### Summary\ncodex asked\n\n### Status\nDONE\n\n## Round 5 — t\n\n### Summary\nclaude five\n\n### Status\nDONE\n' > "$D/devlog.md"
279
+ printf '{"round": 5, "opened_at": "t"}\n' > "$D/.awaiting-reply@codex"
280
+ submit codex "the answer" >/dev/null
281
+ check "codex fold reopens codex's Round 5" 'grep -q "codex asked" "$D/.round-current@codex.md" && grep -q "claude five" "$D/devlog.md" && ! grep -q "codex asked" "$D/devlog.md"'
282
+ close_round codex IN_PROGRESS "$D/.round-current@codex.md" 2>/dev/null
283
+ check "folded round keeps its number on merge" '[ "$(headings)" = "## Round 5,## Round 5 · codex," ]'
284
+
285
+ if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
286
+ else echo "Some checks FAILED."; exit 1; fi
@@ -71,5 +71,15 @@ assert_exit "zero -> exit 1" 1 $?
71
71
  bash "$SCRIPT_DIR/segment-watch-set.sh" >/dev/null 2>&1
72
72
  assert_exit "missing arg -> exit 1" 1 $?
73
73
 
74
+ # --- 5: threshold applies to every platform's .segment-state ----------------
75
+ printf '%s\n' '{"last_change_epoch": 7, "last_seen_cksum": "", "max_silent_seconds": 600, "session_id": "c1"}' \
76
+ > "$TMP_ROOT/.devlog/.segment-state@codex"
77
+ bash "$SCRIPT_DIR/segment-watch-set.sh" 300 >/dev/null
78
+ assert_exit "multi-platform set -> exit 0" 0 $?
79
+ grep -q '"max_silent_seconds": 300' "$TMP_ROOT/.devlog/.segment-state" \
80
+ && echo "PASS: claude segment-state gets 300" || { echo "FAIL: claude segment-state"; FAIL=1; }
81
+ grep -q '"max_silent_seconds": 300' "$TMP_ROOT/.devlog/.segment-state@codex" \
82
+ && echo "PASS: codex segment-state gets 300" || { echo "FAIL: codex segment-state"; FAIL=1; }
83
+
74
84
  if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
75
85
  else echo "Some checks FAILED."; exit 1; fi
@@ -603,6 +603,21 @@ else
603
603
  echo "FAIL: clear should be silent, got: $OUTPUT"; FAIL=1
604
604
  fi
605
605
 
606
+ # --- other platform's handoff: noted by path, not injected ---------------
607
+ touch "$DEVLOG_DIR/.platform-claimed"
608
+ rm -f "$DEVLOG_DIR/handoff.md"
609
+ printf '<session-handoff>\n<decisions>\n- codex only decision\n</decisions>\n</session-handoff>\n' > "$DEVLOG_DIR/handoff@codex.md"
610
+ OUTPUT="$(echo '{"source":"startup"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
611
+ assert_contains "claude notes codex handoff" "另一個平台(codex)有未完成的交接:.devlog/handoff@codex.md" "$OUTPUT"
612
+ assert_not_contains "claude does not inject codex handoff" "codex only decision" "$OUTPUT"
613
+ OUTPUT="$(echo '{"source":"clear"}' | bash "$SCRIPT_DIR/session-start-devlog.sh" 2>&1)"
614
+ if [ -z "$OUTPUT" ]; then
615
+ echo "PASS: clear stays silent with another platform's handoff"
616
+ else
617
+ echo "FAIL: clear should be silent, got: $OUTPUT"; FAIL=1
618
+ fi
619
+ rm -f "$DEVLOG_DIR/handoff@codex.md"
620
+
606
621
  if [ "$FAIL" -eq 0 ]; then
607
622
  echo "All checks passed."
608
623
  exit 0
@@ -6,6 +6,7 @@ PLUGIN_SCRIPTS="$(cd "$SCRIPT_DIR/../../core/scripts" && pwd)"
6
6
  INPUT="$(cat 2>/dev/null || true)"
7
7
  ROOT="$(printf '%s' "$INPUT" | bash "$SCRIPT_DIR/project-dir.sh")"
8
8
  export DEVLOG_PROJECT_DIR="$ROOT"
9
+ export DEVLOG_PLATFORM=cursor
9
10
  ERR_FILE="$(mktemp "${TMPDIR:-/tmp}/cursor-pre-tool.XXXXXX")" || { echo '{}'; exit 0; }
10
11
  printf '%s' "$INPUT" | bash "$PLUGIN_SCRIPTS/segment-watch.sh" >/dev/null 2>"$ERR_FILE"
11
12
  RESULT=$?
@@ -6,6 +6,7 @@ PLUGIN_SCRIPTS="$(cd "$SCRIPT_DIR/../../core/scripts" && pwd)"
6
6
  INPUT="$(cat 2>/dev/null || true)"
7
7
  ROOT="$(printf '%s' "$INPUT" | bash "$SCRIPT_DIR/project-dir.sh")"
8
8
  export DEVLOG_PROJECT_DIR="$ROOT"
9
+ export DEVLOG_PLATFORM=cursor
9
10
  printf '%s' "$INPUT" | bash "$PLUGIN_SCRIPTS/on-session-end.sh" >/dev/null 2>&1 || true
10
11
  echo '{}'
11
12
  exit 0
@@ -6,6 +6,7 @@ PLUGIN_SCRIPTS="$(cd "$SCRIPT_DIR/../../core/scripts" && pwd)"
6
6
  INPUT="$(cat 2>/dev/null || true)"
7
7
  ROOT="$(printf '%s' "$INPUT" | bash "$SCRIPT_DIR/project-dir.sh")"
8
8
  export DEVLOG_PROJECT_DIR="$ROOT"
9
+ export DEVLOG_PLATFORM=cursor
9
10
  EXCERPT="$(printf '%s' '{"source":"startup"}' | bash "$PLUGIN_SCRIPTS/session-start-devlog.sh" 2>/dev/null || true)"
10
11
  if command -v jq >/dev/null 2>&1; then
11
12
  jq -n --arg c "$EXCERPT" '{additional_context:$c}'
@@ -6,6 +6,7 @@ PLUGIN_SCRIPTS="$(cd "$SCRIPT_DIR/../../core/scripts" && pwd)"
6
6
  INPUT="$(cat 2>/dev/null || true)"
7
7
  ROOT="$(printf '%s' "$INPUT" | bash "$SCRIPT_DIR/project-dir.sh")"
8
8
  export DEVLOG_PROJECT_DIR="$ROOT"
9
+ export DEVLOG_PLATFORM=cursor
9
10
  STATUS=""
10
11
  LOOP_COUNT=0
11
12
  if command -v jq >/dev/null 2>&1; then
@@ -6,6 +6,7 @@ PLUGIN_SCRIPTS="$(cd "$SCRIPT_DIR/../../core/scripts" && pwd)"
6
6
  INPUT="$(cat 2>/dev/null || true)"
7
7
  ROOT="$(printf '%s' "$INPUT" | bash "$SCRIPT_DIR/project-dir.sh")"
8
8
  export DEVLOG_PROJECT_DIR="$ROOT"
9
+ export DEVLOG_PLATFORM=cursor
9
10
  PROMPT=""
10
11
  SESSION_ID=""
11
12
  if command -v jq >/dev/null 2>&1; then
@@ -6,6 +6,7 @@ PLUGIN_SCRIPTS="$(cd "$SCRIPT_DIR/../../core/scripts" && pwd)"
6
6
  INPUT="$(cat 2>/dev/null || true)"
7
7
  ROOT="$(printf '%s' "$INPUT" | bash "$SCRIPT_DIR/project-dir.sh")"
8
8
  export DEVLOG_PROJECT_DIR="$ROOT"
9
+ export DEVLOG_PLATFORM=cursor
9
10
  if command -v jq >/dev/null 2>&1; then
10
11
  IS_INTERRUPT="$(printf '%s' "$INPUT" | jq -r '.is_interrupt // .tool_result.is_interrupt // false' 2>/dev/null || echo false)"
11
12
  else
@@ -58,17 +58,19 @@ assert_single_json "empty workspace single json" "$EMPTY"
58
58
  SUBMIT="$TMP/submit"
59
59
  mkdir -p "$SUBMIT/.devlog"
60
60
  touch "$SUBMIT/.devlog/.enabled"
61
+ touch "$SUBMIT/.devlog/.platform-claimed"
61
62
  OUT="$(printf '{"workspace_roots":["%s"],"prompt":"hello","session_id":"cursor-1"}' "$SUBMIT" | bash "$SCRIPT_DIR/on-submit-prompt.sh")"
62
63
  case "$OUT" in *'"continue":true'*) echo "PASS: submit continues" ;; *) echo "FAIL: submit [$OUT]"; FAIL=1 ;; esac
63
64
  assert_single_json "submit single json" "$OUT"
64
- grep -q 'hello' "$SUBMIT/.devlog/.round-current.md" && echo "PASS: submit writes round" || { echo "FAIL: submit round"; FAIL=1; }
65
- ROUNDS="$(grep -c '^## Round ' "$SUBMIT/.devlog/.round-current.md" || true)"
65
+ grep -q 'hello' "$SUBMIT/.devlog/.round-current@cursor.md" && echo "PASS: submit writes round" || { echo "FAIL: submit round"; FAIL=1; }
66
+ ROUNDS="$(grep -c '^## Round ' "$SUBMIT/.devlog/.round-current@cursor.md" || true)"
66
67
  if [ "$ROUNDS" -eq 1 ]; then echo "PASS: submit writes one round"; else echo "FAIL: submit round count [$ROUNDS]"; FAIL=1; fi
67
68
 
68
69
  # submit prompt with stale 工作區 forwards additional_context
69
70
  MIS="$TMP/mismatch-submit"
70
71
  mkdir -p "$MIS/.devlog"
71
72
  touch "$MIS/.devlog/.enabled"
73
+ touch "$MIS/.devlog/.platform-claimed"
72
74
  git -C "$MIS" init -q -b main
73
75
  git -C "$MIS" config user.email test@example.com
74
76
  git -C "$MIS" config user.name test
@@ -76,7 +78,7 @@ echo hello > "$MIS/a.txt"
76
78
  git -C "$MIS" add a.txt
77
79
  git -C "$MIS" commit -q -m init
78
80
  cat > "$MIS/.devlog/devlog.md" <<'EOF'
79
- ## Round 1 — 2026-09-11T00:00:00+08:00
81
+ ## Round 1 — 2026-09-11T00:00:00+08:00 · cursor
80
82
 
81
83
  ### Handoff
82
84
  #### 工作區
@@ -93,17 +95,17 @@ OUT="$(printf '{"workspace_roots":["%s"],"prompt":"keep going","session_id":"cur
93
95
  case "$OUT" in *'"continue":true'*) echo "PASS: mismatch submit continues" ;; *) echo "FAIL: mismatch submit [$OUT]"; FAIL=1 ;; esac
94
96
  case "$OUT" in *'"additional_context"'*工作區*) echo "PASS: mismatch submit additional_context" ;; *) echo "FAIL: mismatch context [$OUT]"; FAIL=1 ;; esac
95
97
  assert_single_json "mismatch submit single json" "$OUT"
96
- [ -f "$MIS/.devlog/.workspace-mismatch" ] && echo "PASS: mismatch submit writes marker" || { echo "FAIL: no cursor marker"; FAIL=1; }
98
+ [ -f "$MIS/.devlog/.workspace-mismatch@cursor" ] && echo "PASS: mismatch submit writes marker" || { echo "FAIL: no cursor marker"; FAIL=1; }
97
99
 
98
100
  # preToolUse allowlists writes to the devlog and denies expired other tools
99
101
  NOW="$(date +%s)"
100
102
  OLD=$((NOW - 1000))
101
- SUM="$(cksum < "$SUBMIT/.devlog/.round-current.md" | tr -d '\n')"
102
- printf '{"last_change_epoch": %s, "last_seen_cksum": "%s", "max_silent_seconds": 900, "session_id": "cursor-1"}\n' "$OLD" "$SUM" > "$SUBMIT/.devlog/.segment-state"
103
- OUT="$(printf '{"workspace_roots":["%s"],"tool_name":"Write","tool_input":{"file_path":"%s/.devlog/.round-current.md"},"session_id":"cursor-1"}' "$SUBMIT" "$SUBMIT" | bash "$SCRIPT_DIR/on-pre-tool.sh")"
103
+ SUM="$(cksum < "$SUBMIT/.devlog/.round-current@cursor.md" | tr -d '\n')"
104
+ printf '{"last_change_epoch": %s, "last_seen_cksum": "%s", "max_silent_seconds": 900, "session_id": "cursor-1"}\n' "$OLD" "$SUM" > "$SUBMIT/.devlog/.segment-state@cursor"
105
+ OUT="$(printf '{"workspace_roots":["%s"],"tool_name":"Write","tool_input":{"file_path":"%s/.devlog/.round-current@cursor.md"},"session_id":"cursor-1"}' "$SUBMIT" "$SUBMIT" | bash "$SCRIPT_DIR/on-pre-tool.sh")"
104
106
  case "$OUT" in *'"permission":"deny"'*) echo "FAIL: devlog write denied"; FAIL=1 ;; *) echo "PASS: devlog write allowed" ;; esac
105
107
  assert_single_json "preTool write single json" "$OUT"
106
- OUT="$(printf '{"workspace_roots":["%s"],"tool_name":"StrReplace","tool_input":{"file_path":"%s/.devlog/.round-current.md"},"session_id":"cursor-1"}' "$SUBMIT" "$SUBMIT" | bash "$SCRIPT_DIR/on-pre-tool.sh")"
108
+ OUT="$(printf '{"workspace_roots":["%s"],"tool_name":"StrReplace","tool_input":{"file_path":"%s/.devlog/.round-current@cursor.md"},"session_id":"cursor-1"}' "$SUBMIT" "$SUBMIT" | bash "$SCRIPT_DIR/on-pre-tool.sh")"
107
109
  case "$OUT" in *'"permission":"deny"'*) echo "FAIL: StrReplace devlog denied"; FAIL=1 ;; *) echo "PASS: StrReplace devlog allowed" ;; esac
108
110
  assert_single_json "preTool StrReplace single json" "$OUT"
109
111
  OUT="$(printf '{"workspace_roots":["%s"],"tool_name":"Bash","tool_input":{},"session_id":"cursor-1"}' "$SUBMIT" | bash "$SCRIPT_DIR/on-pre-tool.sh")"
@@ -122,15 +124,15 @@ case "$OUT" in *followup_message*) echo "FAIL: non-completed stop enforced [$OUT
122
124
  assert_single_json "non-completed stop single json" "$OUT"
123
125
 
124
126
  # aborted stop and session end close the open round
125
- printf '%s\n' '{"round": 1, "opened_at": "now"}' > "$SUBMIT/.devlog/.round-open"
127
+ printf '%s\n' '{"round": 1, "opened_at": "now"}' > "$SUBMIT/.devlog/.round-open@cursor"
126
128
  OUT="$(printf '{"workspace_roots":["%s"],"status":"aborted"}' "$SUBMIT" | bash "$SCRIPT_DIR/on-stop.sh")"
127
129
  grep -q 'INTERRUPTED' "$SUBMIT/.devlog/devlog.md" && echo "PASS: aborted closes round" || { echo "FAIL: aborted"; FAIL=1; }
128
130
  assert_single_json "aborted stop single json" "$OUT"
129
131
 
130
132
  # sessionEnd delegates its reason to the existing close helper
131
- printf '\n## Round 2 — now\n\n### Status\nIN_PROGRESS\n' > "$SUBMIT/.devlog/.round-current.md"
132
- printf '%s\n' '{"round": 2, "opened_at": "now"}' > "$SUBMIT/.devlog/.round-open"
133
- cksum < "$SUBMIT/.devlog/.round-current.md" > "$SUBMIT/.devlog/.turn-start"
133
+ printf '\n## Round 2 — now\n\n### Status\nIN_PROGRESS\n' > "$SUBMIT/.devlog/.round-current@cursor.md"
134
+ printf '%s\n' '{"round": 2, "opened_at": "now"}' > "$SUBMIT/.devlog/.round-open@cursor"
135
+ cksum < "$SUBMIT/.devlog/.round-current@cursor.md" > "$SUBMIT/.devlog/.turn-start@cursor"
134
136
  OUT="$(printf '{"workspace_roots":["%s"],"reason":"windowClosed"}' "$SUBMIT" | bash "$SCRIPT_DIR/on-session-end.sh")"
135
137
  grep -q 'SessionEnd:windowClosed' "$SUBMIT/.devlog/devlog.md" && echo "PASS: session end reason" || { echo "FAIL: session end"; FAIL=1; }
136
138
  assert_single_json "sessionEnd single json" "$OUT"
@@ -146,17 +148,18 @@ ROOT="$(printf '{"workspace_roots":["%s"]}' "$SUBMIT" | PATH="$PATH_NO_JQ" bash
146
148
  if [ "$ROOT" = "$SUBMIT" ]; then echo "PASS: project-dir without jq"; else echo "FAIL: project-dir without jq [$ROOT]"; FAIL=1; fi
147
149
 
148
150
  # tool interruption marker
149
- rm -f "$SUBMIT/.devlog/.interrupted"
151
+ rm -f "$SUBMIT/.devlog/.interrupted@cursor"
150
152
  OUT="$(printf '{"workspace_roots":["%s"],"is_interrupt":true}' "$SUBMIT" | bash "$SCRIPT_DIR/on-tool-failure.sh")"
151
- [ -f "$SUBMIT/.devlog/.interrupted" ] && echo "PASS: tool interruption marked" || { echo "FAIL: interrupt marker"; FAIL=1; }
153
+ [ -f "$SUBMIT/.devlog/.interrupted@cursor" ] && echo "PASS: tool interruption marked" || { echo "FAIL: interrupt marker"; FAIL=1; }
152
154
  assert_single_json "toolFailure single json" "$OUT"
153
155
 
154
156
  # --- no-jq session_id must reach segment-state via round-start ------------
155
157
  NOJQ_HOME="$TMP/nojq_home"
156
158
  mkdir -p "$NOJQ_HOME/.devlog" "$TMP/nojq_path"
157
159
  touch "$NOJQ_HOME/.devlog/.enabled"
160
+ touch "$NOJQ_HOME/.devlog/.platform-claimed"
158
161
  printf '%s\n' '{"last_change_epoch": 0, "last_seen_cksum": "", "max_silent_seconds": 600, "session_id": ""}' \
159
- > "$NOJQ_HOME/.devlog/.segment-state"
162
+ > "$NOJQ_HOME/.devlog/.segment-state@cursor"
160
163
  CLEAN_PATH="$TMP/nojq_path"
161
164
  # Resolve real binaries (zsh `command -v` may return aliases / builtins).
162
165
  for cmd in bash sh cksum date grep sed cat mkdir tr mktemp rm head awk touch \
@@ -171,7 +174,7 @@ done
171
174
  OUT="$(printf '{"workspace_roots":["%s"],"prompt":"nojq-hi","session_id":"cursor-nojq"}' "$NOJQ_HOME" \
172
175
  | env PATH="$CLEAN_PATH" bash "$SCRIPT_DIR/on-submit-prompt.sh")"
173
176
  case "$OUT" in *'"continue":true'*) echo "PASS: nojq submit continues" ;; *) echo "FAIL: nojq submit [$OUT]"; FAIL=1 ;; esac
174
- SEG_SID="$(grep -o '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' "$NOJQ_HOME/.devlog/.segment-state" 2>/dev/null | head -1 | sed 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || true)"
177
+ SEG_SID="$(grep -o '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' "$NOJQ_HOME/.devlog/.segment-state@cursor" 2>/dev/null | head -1 | sed 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || true)"
175
178
  if [ "$SEG_SID" = "cursor-nojq" ]; then
176
179
  echo "PASS: nojq session_id stored in segment-state"
177
180
  else
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlog-tracker",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
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"