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.
Files changed (53) hide show
  1. package/README.md +41 -10
  2. package/README.zh-TW.md +41 -10
  3. package/cli/agents-md.js +1 -0
  4. package/cli/agents-md.test.js +10 -0
  5. package/cli/platforms/claude.js +1 -0
  6. package/codex/hooks/on-interrupt.sh +13 -0
  7. package/codex/hooks/on-pre-tool.sh +69 -1
  8. package/codex/hooks/on-session-end.sh +5 -5
  9. package/codex/hooks/on-session-start.sh +5 -6
  10. package/codex/hooks/on-stop.sh +3 -2
  11. package/codex/hooks/on-subagent-start.sh +19 -0
  12. package/codex/hooks/project-dir.sh +16 -1
  13. package/codex/hooks/test-adapters.sh +127 -9
  14. package/codex/hooks.json +21 -0
  15. package/commands/continue.md +3 -3
  16. package/commands/keep-all.md +1 -1
  17. package/commands/keep.md +2 -2
  18. package/commands/lessons-on.md +1 -1
  19. package/commands/migrate.md +18 -0
  20. package/commands/pr.md +3 -3
  21. package/commands/resume.md +1 -1
  22. package/core/scripts/close-open-round.sh +8 -2
  23. package/core/scripts/devlog-md.sh +6 -11
  24. package/core/scripts/enforce-devlog.sh +79 -85
  25. package/core/scripts/handoff-convert.sh +187 -0
  26. package/core/scripts/handoff-fields.sh +219 -0
  27. package/core/scripts/handoff-file.sh +19 -88
  28. package/core/scripts/lessons-subagent-start.sh +1 -1
  29. package/core/scripts/migrate-handoff.sh +91 -0
  30. package/core/scripts/round-start.sh +13 -2
  31. package/core/scripts/segment-watch.sh +1 -1
  32. package/core/scripts/tests/lib/xml-fixture.sh +10 -0
  33. package/core/scripts/tests/test-close-open-round.sh +2 -1
  34. package/core/scripts/tests/test-devlog-md.sh +21 -0
  35. package/core/scripts/tests/test-enforce-devlog-files.sh +4 -1
  36. package/core/scripts/tests/test-enforce-devlog-handoff-order.sh +149 -81
  37. package/core/scripts/tests/test-enforce-devlog-session-handoff.sh +92 -11
  38. package/core/scripts/tests/test-enforce-devlog-workspace.sh +42 -26
  39. package/core/scripts/tests/test-enforce-devlog.sh +70 -0
  40. package/core/scripts/tests/test-handoff-fields.sh +171 -0
  41. package/core/scripts/tests/test-handoff-file.sh +67 -45
  42. package/core/scripts/tests/test-migrate-handoff.sh +278 -0
  43. package/core/scripts/tests/test-round-start.sh +54 -1
  44. package/core/scripts/tests/test-session-start-devlog.sh +45 -0
  45. package/core/scripts/tests/test-workspace-snapshot.sh +32 -0
  46. package/core/scripts/timeline-render.js +26 -2
  47. package/core/scripts/timeline-render.test.js +23 -1
  48. package/core/scripts/workspace-snapshot.sh +15 -2
  49. package/package.json +3 -3
  50. package/skills/devlog-tracker/SKILL.md +81 -56
  51. package/skills/devlog-tracker/references/contract.md +4 -3
  52. package/skills/devlog-tracker/references/lessons-mode.md +7 -7
  53. package/skills/devlog-tracker/references/round-segments.md +11 -5
@@ -9,6 +9,18 @@ FAIL=0
9
9
  ROOT="$(printf '{"cwd":"%s"}' "$TMP" | bash "$SCRIPT_DIR/project-dir.sh")"
10
10
  if [ "$ROOT" = "$TMP" ]; then echo "PASS: project-dir reads cwd"; else echo "FAIL: project-dir [$ROOT]"; FAIL=1; fi
11
11
 
12
+ mkdir -p "$TMP/nested/.devlog-tracker" "$TMP/nested/src/lib"
13
+ ROOT="$(printf '{"cwd":"%s"}' "$TMP/nested/src/lib" | bash "$SCRIPT_DIR/project-dir.sh")"
14
+ if [ "$ROOT" = "$TMP/nested" ]; then echo "PASS: nested cwd resolves installed project"; else echo "FAIL: nested cwd [$ROOT]"; FAIL=1; fi
15
+
16
+ mkdir -p "$TMP/plugin-project/.git" "$TMP/plugin-project/src/lib"
17
+ ROOT="$(printf '{"cwd":"%s"}' "$TMP/plugin-project/src/lib" | bash "$SCRIPT_DIR/project-dir.sh")"
18
+ if [ "$ROOT" = "$TMP/plugin-project" ]; then echo "PASS: plugin nested cwd resolves git root"; else echo "FAIL: plugin nested cwd [$ROOT]"; FAIL=1; fi
19
+
20
+ # Codex Stop requires JSON stdout on a successful exit.
21
+ OUT="$(printf '{"cwd":"%s","hook_event_name":"Stop"}' "$TMP/nested" | bash "$SCRIPT_DIR/on-stop.sh")"
22
+ if [ "$OUT" = '{}' ]; then echo "PASS: inactive Stop returns JSON"; else echo "FAIL: inactive Stop output [$OUT]"; FAIL=1; fi
23
+
12
24
  # sessionStart: injects last Round's content as plain stdout
13
25
  mkdir -p "$TMP/project/.devlog"
14
26
  cat > "$TMP/project/.devlog/devlog.md" <<'EOF'
@@ -24,12 +36,12 @@ done
24
36
  ### Status
25
37
  DONE
26
38
  EOF
27
- OUT="$(printf '{"cwd":"%s","how":"startup"}' "$TMP/project" | bash "$SCRIPT_DIR/on-session-start.sh")"
39
+ OUT="$(printf '{"cwd":"%s","source":"startup"}' "$TMP/project" | bash "$SCRIPT_DIR/on-session-start.sh")"
28
40
  case "$OUT" in *"Round 1"*) echo "PASS: sessionStart injects context" ;; *) echo "FAIL: sessionStart [$OUT]"; FAIL=1 ;; esac
29
41
 
30
42
  # userPromptSubmit: opens a Round in .round-current.md
31
43
  SUBMIT="$TMP/submit"
32
- mkdir -p "$SUBMIT/.devlog"
44
+ mkdir -p "$SUBMIT/.devlog" "$SUBMIT/.devlog-tracker" "$SUBMIT/src"
33
45
  touch "$SUBMIT/.devlog/.enabled"
34
46
  bash "$SCRIPT_DIR/on-user-prompt-submit.sh" <<EOF2 >/dev/null
35
47
  {"cwd":"$SUBMIT","prompt":"hello","session_id":"codex-1"}
@@ -46,10 +58,14 @@ OLD=$((NOW - 1000))
46
58
  SUM="$(cksum < "$SUBMIT/.devlog/.round-current.md" | tr -d '\n')"
47
59
  printf '{"last_change_epoch": %s, "last_seen_cksum": "%s", "max_silent_seconds": 900, "session_id": "codex-1"}\n' "$OLD" "$SUM" > "$SUBMIT/.devlog/.segment-state"
48
60
  set +e
49
- printf '{"cwd":"%s","tool_name":"Bash","tool_input":{},"session_id":"codex-1"}' "$SUBMIT" | bash "$SCRIPT_DIR/on-pre-tool.sh" >/dev/null 2>/dev/null
61
+ ERR="$(printf '{"cwd":"%s","tool_name":"Bash","tool_input":{},"session_id":"codex-1"}' "$SUBMIT" | bash "$SCRIPT_DIR/on-pre-tool.sh" 2>&1 >/dev/null)"
50
62
  RC=$?
51
63
  set -e
52
64
  if [ "$RC" -eq 2 ]; then echo "PASS: expired tool blocked (exit 2)"; else echo "FAIL: preTool exit [$RC]"; FAIL=1; fi
65
+ case "$ERR" in
66
+ *'cat '*'apply_patch'*) echo "PASS: Codex block explains recovery tools" ;;
67
+ *) echo "FAIL: Codex recovery guidance [$ERR]"; FAIL=1 ;;
68
+ esac
53
69
 
54
70
  set +e
55
71
  printf '{"cwd":"%s","tool_name":"Write","tool_input":{"file_path":"%s/.devlog/.round-current.md"},"session_id":"codex-1"}' "$SUBMIT" "$SUBMIT" | bash "$SCRIPT_DIR/on-pre-tool.sh" >/dev/null 2>/dev/null
@@ -57,6 +73,77 @@ RC=$?
57
73
  set -e
58
74
  if [ "$RC" -eq 0 ]; then echo "PASS: devlog write allowed (exit 0)"; else echo "FAIL: allowed-tool exit [$RC]"; FAIL=1; fi
59
75
 
76
+ set +e
77
+ printf '{"cwd":"%s","tool_name":"Bash","tool_input":{"command":"cat %s/.devlog/.round-current.md"},"session_id":"codex-1"}' \
78
+ "$SUBMIT" "$SUBMIT" | bash "$SCRIPT_DIR/on-pre-tool.sh" >/dev/null 2>/dev/null
79
+ RC=$?
80
+ set -e
81
+ if [ "$RC" -eq 0 ]; then echo "PASS: devlog cat allowed"; else echo "FAIL: devlog cat exit [$RC]"; FAIL=1; fi
82
+
83
+ set +e
84
+ printf '{"cwd":"%s","tool_name":"Bash","tool_input":{"command":"cat %s/.devlog/.round-current.md; echo unsafe"},"session_id":"codex-1"}' \
85
+ "$SUBMIT" "$SUBMIT" | bash "$SCRIPT_DIR/on-pre-tool.sh" >/dev/null 2>/dev/null
86
+ RC=$?
87
+ set -e
88
+ if [ "$RC" -eq 2 ]; then echo "PASS: chained cat blocked"; else echo "FAIL: chained cat exit [$RC]"; FAIL=1; fi
89
+
90
+ # Codex reports file edits as apply_patch with the patch in tool_input.command.
91
+ PATCH='*** Begin Patch
92
+ *** Update File: .devlog/.round-current.md
93
+ @@
94
+ -before
95
+ +after
96
+ *** End Patch'
97
+ set +e
98
+ printf '{"cwd":"%s","tool_name":"apply_patch","tool_input":{"command":%s},"session_id":"codex-1"}' \
99
+ "$SUBMIT" "$(node -p 'JSON.stringify(process.argv[1])' "$PATCH")" | bash "$SCRIPT_DIR/on-pre-tool.sh" >/dev/null 2>/dev/null
100
+ RC=$?
101
+ set -e
102
+ if [ "$RC" -eq 0 ]; then echo "PASS: devlog apply_patch allowed"; else echo "FAIL: devlog apply_patch exit [$RC]"; FAIL=1; fi
103
+
104
+ PATCH='*** Begin Patch
105
+ *** Update File: ../.devlog/.round-current.md
106
+ @@
107
+ -before
108
+ +after
109
+ *** End Patch'
110
+ set +e
111
+ printf '{"cwd":"%s","tool_name":"apply_patch","tool_input":{"command":%s},"session_id":"codex-1"}' \
112
+ "$SUBMIT/src" "$(node -p 'JSON.stringify(process.argv[1])' "$PATCH")" | bash "$SCRIPT_DIR/on-pre-tool.sh" >/dev/null 2>/dev/null
113
+ RC=$?
114
+ set -e
115
+ if [ "$RC" -eq 0 ]; then echo "PASS: nested cwd devlog apply_patch allowed"; else echo "FAIL: nested cwd apply_patch exit [$RC]"; FAIL=1; fi
116
+
117
+ PATCH='*** Begin Patch
118
+ *** Update File: .devlog/.round-current.md
119
+ @@
120
+ -before
121
+ +after
122
+ *** End Patch'
123
+ set +e
124
+ printf '{"cwd":"%s","tool_name":"apply_patch","tool_input":{"command":%s},"session_id":"codex-1"}' \
125
+ "$SUBMIT/src" "$(node -p 'JSON.stringify(process.argv[1])' "$PATCH")" | bash "$SCRIPT_DIR/on-pre-tool.sh" >/dev/null 2>/dev/null
126
+ RC=$?
127
+ set -e
128
+ if [ "$RC" -eq 2 ]; then echo "PASS: nested cwd unrelated apply_patch blocked"; else echo "FAIL: nested cwd unrelated patch exit [$RC]"; FAIL=1; fi
129
+
130
+ PATCH='*** Begin Patch
131
+ *** Update File: .devlog/.round-current.md
132
+ @@
133
+ -before
134
+ +after
135
+ *** Update File: src/main.js
136
+ @@
137
+ -before
138
+ +after
139
+ *** End Patch'
140
+ set +e
141
+ printf '{"cwd":"%s","tool_name":"apply_patch","tool_input":{"command":%s},"session_id":"codex-1"}' \
142
+ "$SUBMIT" "$(node -p 'JSON.stringify(process.argv[1])' "$PATCH")" | bash "$SCRIPT_DIR/on-pre-tool.sh" >/dev/null 2>/dev/null
143
+ RC=$?
144
+ set -e
145
+ if [ "$RC" -eq 2 ]; then echo "PASS: mixed apply_patch blocked"; else echo "FAIL: mixed apply_patch exit [$RC]"; FAIL=1; fi
146
+
60
147
  # stop: blocks (exit 2) while the round is unfinished
61
148
  printf '{"round": 1, "opened_at": "now"}\n' > "$SUBMIT/.devlog/.round-open"
62
149
  cksum < "$SUBMIT/.devlog/.round-current.md" > "$SUBMIT/.devlog/.turn-start"
@@ -66,16 +153,47 @@ RC=$?
66
153
  set -e
67
154
  if [ "$RC" -eq 2 ]; then echo "PASS: stop blocks unfinished round (exit 2)"; else echo "FAIL: stop exit [$RC]"; FAIL=1; fi
68
155
 
69
- # sessionEnd: closes the open round using "why" (not "reason")
156
+ # sessionEnd: closes the open round using the documented "reason"
70
157
  printf '\n## Round 2 — now\n\n### Status\nIN_PROGRESS\n' > "$SUBMIT/.devlog/.round-current.md"
71
158
  printf '{"round": 2, "opened_at": "now"}\n' > "$SUBMIT/.devlog/.round-open"
72
159
  cksum < "$SUBMIT/.devlog/.round-current.md" > "$SUBMIT/.devlog/.turn-start"
73
- printf '{"cwd":"%s","why":"windowClosed"}' "$SUBMIT" | bash "$SCRIPT_DIR/on-session-end.sh" >/dev/null
74
- if grep -q 'SessionEnd:windowClosed' "$SUBMIT/.devlog/devlog.md" 2>/dev/null; then
75
- echo "PASS: sessionEnd closes round via 'why'"
160
+ printf '{"cwd":"%s","reason":"other"}' "$SUBMIT" | bash "$SCRIPT_DIR/on-session-end.sh" >/dev/null
161
+ if grep -q 'SessionEnd:other' "$SUBMIT/.devlog/devlog.md" 2>/dev/null; then
162
+ echo "PASS: sessionEnd closes round via reason"
163
+ else
164
+ echo "FAIL: sessionEnd reason"; FAIL=1
165
+ fi
166
+
167
+ # Interrupt closes the active round immediately, using Codex's event.
168
+ printf '\n## Round 3 — now\n\n### Status\nIN_PROGRESS\n' > "$SUBMIT/.devlog/.round-current.md"
169
+ printf '{"round": 3, "opened_at": "now"}\n' > "$SUBMIT/.devlog/.round-open"
170
+ cksum < "$SUBMIT/.devlog/.round-current.md" > "$SUBMIT/.devlog/.turn-start"
171
+ printf '{"cwd":"%s","hook_event_name":"Interrupt","turn_id":"turn-3"}' "$SUBMIT" | bash "$SCRIPT_DIR/on-interrupt.sh" >/dev/null
172
+ if grep -q 'Interrupt:cancelled' "$SUBMIT/.devlog/devlog.md" 2>/dev/null; then
173
+ echo "PASS: interrupt closes round"
174
+ else
175
+ echo "FAIL: interrupt did not close round"; FAIL=1
176
+ fi
177
+
178
+ # SubagentStart gives a Codex subagent the existing Lessons Mode guidance.
179
+ touch "$SUBMIT/.devlog/.lessons-enabled"
180
+ OUT="$(printf '{"cwd":"%s","hook_event_name":"SubagentStart","agent_id":"child-1","agent_type":"general-purpose"}' "$SUBMIT" | bash "$SCRIPT_DIR/on-subagent-start.sh")"
181
+ if printf '%s' "$OUT" | jq -e --arg p "$SUBMIT" '.hookSpecificOutput.hookEventName == "SubagentStart" and (.hookSpecificOutput.additionalContext | contains($p) and contains("lessons-append.sh"))' >/dev/null 2>&1; then
182
+ echo "PASS: subagent receives Lessons Mode guidance"
183
+ else
184
+ echo "FAIL: subagent guidance [$OUT]"; FAIL=1
185
+ fi
186
+
187
+ mkdir -p "$SUBMIT/.devlog-tracker/codex/hooks" "$SUBMIT/.devlog-tracker/core/scripts"
188
+ cp "$SCRIPT_DIR/on-subagent-start.sh" "$SCRIPT_DIR/project-dir.sh" "$SUBMIT/.devlog-tracker/codex/hooks/"
189
+ cp "$SCRIPT_DIR/../../core/scripts/lessons-subagent-start.sh" "$SCRIPT_DIR/../../core/scripts/json-field.sh" "$SUBMIT/.devlog-tracker/core/scripts/"
190
+ OUT="$(printf '{"cwd":"%s","hook_event_name":"SubagentStart","agent_id":"child-2","agent_type":"general-purpose"}' "$TMP/unrelated-worktree" | bash "$SUBMIT/.devlog-tracker/codex/hooks/on-subagent-start.sh")"
191
+ if printf '%s' "$OUT" | jq -e --arg p "$SUBMIT" '.hookSpecificOutput.additionalContext | contains($p)' >/dev/null 2>&1; then
192
+ echo "PASS: worktree subagent guidance uses main project"
76
193
  else
77
- echo "FAIL: sessionEnd 'why' fallback"; FAIL=1
194
+ echo "FAIL: worktree subagent guidance [$OUT]"; FAIL=1
78
195
  fi
196
+ rm -f "$SUBMIT/.devlog/.lessons-enabled"
79
197
 
80
198
  # fail-open: when ../../core/scripts is missing, preTool/stop wrappers must allow (exit 0)
81
199
  ORPHAN="$TMP/orphan"
@@ -89,7 +207,7 @@ for W in on-pre-tool on-stop; do
89
207
  if [ "$RC" -eq 0 ]; then echo "PASS: $W fails open without core/scripts (exit 0)"; else echo "FAIL: $W orphan exit [$RC]"; FAIL=1; fi
90
208
  done
91
209
 
92
- # sessionStart: payload with neither "how" nor "source" falls back to startup and still injects
210
+ # sessionStart: payload with neither "source" nor "how" falls back to startup and still injects
93
211
  OUT="$(printf '{"cwd":"%s"}' "$TMP/project" | bash "$SCRIPT_DIR/on-session-start.sh")"
94
212
  case "$OUT" in *"Round 1"*) echo "PASS: sessionStart falls back to startup without how/source" ;; *) echo "FAIL: sessionStart no-source [$OUT]"; FAIL=1 ;; esac
95
213
 
package/codex/hooks.json CHANGED
@@ -20,6 +20,16 @@
20
20
  ]
21
21
  }
22
22
  ],
23
+ "SubagentStart": [
24
+ {
25
+ "hooks": [
26
+ {
27
+ "type": "command",
28
+ "command": "bash \"${DEVLOG_TRACKER_ROOT}/codex/hooks/on-subagent-start.sh\""
29
+ }
30
+ ]
31
+ }
32
+ ],
23
33
  "PreToolUse": [
24
34
  {
25
35
  "hooks": [
@@ -49,6 +59,17 @@
49
59
  }
50
60
  ]
51
61
  }
62
+ ],
63
+ "Interrupt": [
64
+ {
65
+ "hooks": [
66
+ {
67
+ "type": "command",
68
+ "command": "bash \"${DEVLOG_TRACKER_ROOT}/codex/hooks/on-interrupt.sh\"",
69
+ "timeout": 3
70
+ }
71
+ ]
72
+ }
52
73
  ]
53
74
  }
54
75
  }
@@ -14,15 +14,15 @@ description: 讀取 .devlog/devlog.md,核對最後一輪 Handoff 的工作區
14
14
  PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
15
15
  DEVLOG_PROJECT_DIR="<步驟 1 記下的專案根目錄絕對路徑,不要用 $(pwd) 重新推>" bash "${PLUGIN_ROOT}/core/scripts/workspace-snapshot.sh"
16
16
  ```
17
- 把 `DEVLOG_PROJECT_DIR` 換成實際的絕對路徑字串再執行,不要真的呼叫 `pwd`。stdout 就是實際快照(1 行或 2 行)。腳本檔找不到時才退回 `skills/devlog-tracker/SKILL.md` `#### 工作區` 的七種格式手編。不要重跑測試套件,除非「下一步」本身就是跑測試。
18
- 2. 把編成的實際快照對照該歷史 Round 的 `#### 工作區` 正文。
17
+ 把 `DEVLOG_PROJECT_DIR` 換成實際的絕對路徑字串再執行,不要真的呼叫 `pwd`。stdout 就是實際快照(1 行或 2 行)。腳本檔找不到時才退回 `skills/devlog-tracker/SKILL.md` 工作區(`<workspace>`;舊格式 `#### 工作區`)的七種格式手編。不要重跑測試套件,除非「下一步」本身就是跑測試。
18
+ 2. 把編成的實際快照對照該歷史 Round 的工作區(`<workspace>`;舊格式 `#### 工作區`)正文。
19
19
  - 沒有這一節(舊 Round、`INTERRUPTED` stub):沒有宣稱可對,不算「不符」——不用寫 `### 段落`,直接以剛才編成的實際快照為準。
20
20
  - 有這一節但跟編成的實際快照不符:在**這一輪**先追加一段 `### 段落`,寫宣稱 vs 實際(用剛才腳本的 stdout 當實際快照)。
21
21
  - 有這一節且相符:不用寫 `### 段落`。
22
22
  3. 然後依**實際工作樹**行動(不要照 Handoff「工作區」或「現況」的字面當事實):
23
23
  - `IN_PROGRESS`/`INTERRUPTED`:做 Handoff「下一步」(沒有就依「現況」與實際工作樹推出並做)。對照「完成條件」判斷能否收成 `DONE`。
24
24
  - `BLOCKED`:看缺的外部輸入本身在不在——已經出現就做下一步;仍缺就說明缺什麼並停。git 相不相符不能證明缺件已到,不要發明輸入。
25
- 6. **寫回義務(L1 硬契約):** 接手=核對 → 行動 → **同一輪收尾寫回** `.devlog/devlog.md`。讀而不寫算失敗——下一任無法再接。編輯**這一個** Round(continue 的 skeleton/開著的 Round),不要再新增一個 `## Round`,也不要改歷史 Round。必寫 `### Summary`、`### Reply`、`### Handoff`、`### Status`(契約見 SKILL.md)。收尾時若 Status 是 `IN_PROGRESS`/`BLOCKED`,照契約寫本輪的 `#### 工作區`、`#### 完成條件`、`#### 下一步`。聊天不要旁白「已寫入 devlog」。
25
+ 6. **寫回義務(L1 硬契約):** 接手=核對 → 行動 → **同一輪收尾寫回** `.devlog/devlog.md`。讀而不寫算失敗——下一任無法再接。編輯**這一個** Round(continue 的 skeleton/開著的 Round),不要再新增一個 `## Round`,也不要改歷史 Round。必寫 `### Summary`、`### Reply`、`### Handoff`、`### Status`(契約見 SKILL.md)。收尾時若 Status 是 `IN_PROGRESS`/`BLOCKED`,照契約寫本輪的 `<workspace>`、`<done-when>`、`<next>`(XML 格式見 SKILL.md)。聊天不要旁白「已寫入 devlog」。
26
26
  - **有** `/devlog-tracker:start`(`.enabled`)時:Stop hook 會擋沒寫完的收尾。
27
27
  - **沒有** Stop/未 `start`/Cursor 未裝 hook:仍必須自行用 Edit/Write 寫回;沒有擋關不代表可以省略。
28
28
  - **子 agent** 若只改程式不收尾:主對話負責寫回,或在派出 brief 裡要求子任務寫回同一 Round。
@@ -87,7 +87,7 @@ DEVLOG_PROJECT_DIR="<專案根目錄絕對路徑>" bash "${PLUGIN_ROOT}/core/scr
87
87
 
88
88
  ## 5. 選配:摘要
89
89
 
90
- 只對使用者標記「摘要第 N 段」的段落做,規則同 `/devlog-tracker:keep` 步驟 5.5(只改寫 `### Summary`、`### Reply`、`#### 決策`、`#### 現況` 的敘事;標題、`### User Input`、`#### 工作區`、`#### 檔案`、`#### 完成條件`、`#### 下一步`、`### Status` 一律不動;用 Edit 不用 Write)。
90
+ 只對使用者標記「摘要第 N 段」的段落做,規則同 `/devlog-tracker:keep` 步驟 5.5(只改寫 `### Summary`、`### Reply`、`<decisions>`/舊格式 `#### 決策`、`<state>`/舊格式 `#### 現況` 的敘事;標題、`### User Input`、`<workspace>`/舊格式 `#### 工作區`、`<files>`/舊格式 `#### 檔案`、`<done-when>`/舊格式 `#### 完成條件`、`<next>`/舊格式 `#### 下一步`、`### Status` 一律不動;改寫 XML 欄位內容時,標籤行本身不動;用 Edit 不用 Write)。
91
91
 
92
92
  ## 6. 回報
93
93
 
package/commands/keep.md CHANGED
@@ -101,8 +101,8 @@ DEVLOG_PROJECT_DIR="<專案根目錄絕對路徑,不要用 $(pwd) 重新推>"
101
101
  這一步是 Claude 自己改寫,不是另外呼叫 LLM API、不是腳本邏輯——`keep-move.sh` 在步驟 5 已經把該段逐字搬進 `devlog.<name>.md`,機器可核對的欄位跟這一步無關。對每個被標記的段落:
102
102
 
103
103
  1. 用 Read 讀出該段剛寫入的 `devlog.<name>.md` 全文。
104
- 2. 只能改寫每個 `## Round` 裡的敘事文字:`### Summary`、`### Reply`、`#### 決策`、`#### 現況`。把同一輪裡這幾節的內容改寫得更精簡,可以合併重複的敘述,但不要跨 Round 合併——Round 的數量與編號必須維持原樣。
105
- 3. 絕對不要動:`## Round <n> — <時間戳>` 標題本身、`### User Input`、`#### 工作區`、`#### 檔案`、`#### 完成條件`、`#### 下一步`、`### Status`,以及 `## Kept 索引`。這些是機器核對過的事實或 `resume`/`continue` 接手時要逐字比對的欄位,摘要只能動無法被 git 或 Stop 驗證的敘事散文。
104
+ 2. 只能改寫每個 `## Round` 裡的敘事文字:`### Summary`、`### Reply`、`<decisions>`/舊格式 `#### 決策`、`<state>`/舊格式 `#### 現況` 的敘事。把同一輪裡這幾節的內容改寫得更精簡,可以合併重複的敘述,但不要跨 Round 合併——Round 的數量與編號必須維持原樣。改寫 XML 欄位內容時,標籤行本身不動。
105
+ 3. 絕對不要動:`## Round <n> — <時間戳>` 標題本身、`### User Input`、`<workspace>`/舊格式 `#### 工作區`、`<files>`/舊格式 `#### 檔案`、`<done-when>`/舊格式 `#### 完成條件`、`<next>`/舊格式 `#### 下一步`、`### Status`,以及 `## Kept 索引`。這些是機器核對過的事實或 `resume`/`continue` 接手時要逐字比對的欄位,摘要只能動無法被 git 或 Stop 驗證的敘事散文。
106
106
  4. 用 Edit 把改寫後的內容寫回同一個檔案(不要用 Write 整檔覆寫,避免不小心動到上面列的欄位)。
107
107
 
108
108
  改寫完成後才進入步驟 6/7 回報;回報時註明哪幾段套用了摘要。
@@ -14,7 +14,7 @@ description: 開啟 Lessons Mode(開發歷程教訓,預設關閉)。隸屬
14
14
  3. stdout 是 `LESSONS_ENABLED=...`:告知 Lessons Mode 已開啟。簡短說明:
15
15
  - 這是給「開發歷程中的困難/決策」用的,不是架構知識庫(那個留在 `docs/design/*.md`)
16
16
  - 只有兩種訊號會讓你考慮記一筆:這輪的 Status 從 `BLOCKED` 解開,或你自己判斷這輪明顯繞了一圈才對
17
- - 完全不強制——寫不寫都不影響這一輪能不能收尾,跟 `#### 決策` 同一種「沒有就整節省略」的精神
17
+ - 完全不強制——寫不寫都不影響這一輪能不能收尾,跟 Handoff 的 `<decisions>`(舊格式 `#### 決策`)同一種「沒有就整個標籤省略」的精神
18
18
  - 用 `/devlog-tracker:lessons` 查現有教訓,或 `/devlog-tracker:lessons-off` 關掉
19
19
 
20
20
  不要因為 `.lessons-enabled` 已經存在就跳過步驟 3——重新確認一次現在的狀態即可。
@@ -0,0 +1,18 @@
1
+ ---
2
+ description: 把 .devlog 裡舊格式(#### 小節)的 Handoff/Session Handoff 轉成 XML 標籤格式;Stop hook 擋下舊格式時也會叫 agent 跑它
3
+ ---
4
+
5
+ 請執行 Handoff 格式遷移:
6
+
7
+ 1. 決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄),然後跑(不要自己改檔):
8
+ ```bash
9
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
10
+ DEVLOG_PROJECT_DIR="<專案根目錄絕對路徑,不要用 $(pwd) 重新推>" bash "${PLUGIN_ROOT}/core/scripts/migrate-handoff.sh"
11
+ ```
12
+ 2. 依 stdout 回報一句話:
13
+ - `NO_DEVLOG`:這個專案沒有 `.devlog/`,沒有東西要轉。
14
+ - `LOCKED <pid>`:另一個 session 正在寫 devlog,稍後再跑一次。
15
+ - 否則用 `MIGRATED=` 與 `SKIPPED=` 回報轉了幾輪、跳過幾輪;`BACKUP=` 是轉換前的備份。
16
+ 3. 有 `SKIP ... Round <N>: <原因>` 時:歷史輪次保留原樣即可(讀取端相容舊格式)。只有**這一輪**(開著的 `.round-current.md`)被跳過時,才照 `skills/devlog-tracker/SKILL.md`「每一輪的紀錄格式」的 XML 模板手動改寫這一輪的 Handoff/Session Handoff。
17
+
18
+ 只轉 `.round-current.md`、`devlog.md`、branch 檔(首行是 `<!-- devlog-origin: ... -->`)與 `handoff*.md`;不轉 `devlog.archive.md`、keep 產生的具名檔、lessons 檔。
package/commands/pr.md CHANGED
@@ -24,7 +24,7 @@ DEVLOG_PROJECT_DIR="<剛才記下的專案根目錄絕對路徑>" bash "${PLUGIN
24
24
  ## 2. 讀資料
25
25
 
26
26
  - 跑 `git log --reverse --format='%h %s%n%b' <BASE_REF>..HEAD` 與 `git diff --stat <BASE_REF>...HEAD`。
27
- - 有 `ROUNDS` 時,用 Read 讀 `DEVLOG_FILE` 全文,只看這些 Round(`ROUNDS` 是每個 `## Round` 標題的行號)。每個 Round 取 `### Summary`、`#### 決策`、`#### 檔案`、驗證紀錄(測試指令與結果,通常在 Summary、`#### 現況` 或 `### 段落`)。**不要讀或引用 `### User Input`。**
27
+ - 有 `ROUNDS` 時,用 Read 讀 `DEVLOG_FILE` 全文,只看這些 Round(`ROUNDS` 是每個 `## Round` 標題的行號)。每個 Round 取 `### Summary`、`<decisions>`(舊格式 `#### 決策`)、`<files>`(舊格式 `#### 檔案`)、驗證紀錄(測試指令與結果,通常在 Summary、`<state>`(舊格式 `#### 現況`)或 `### 段落`)。**不要讀或引用 `### User Input`。**
28
28
  - `NO_BRANCH_DEVLOG` 時只依 git log 與 diff 產生,並在對話裡說明「這個 branch 沒有 devlog 紀錄,描述只根據 commit 產生」。
29
29
 
30
30
  ## 3. 產生 PR body
@@ -32,8 +32,8 @@ DEVLOG_PROJECT_DIR="<剛才記下的專案根目錄絕對路徑>" bash "${PLUGIN
32
32
  語言跟使用者一致。四節固定,順序不變:
33
33
 
34
34
  1. **Summary**:這個 branch 做了什麼,2–4 條,寫結果不寫過程。
35
- 2. **Decisions**:取自 Rounds 的 `#### 決策`,只留最終版本;被後面 Round 推翻或改掉的不列。沒有就寫「無」。
36
- 3. **Changes**:依 commit 或檔案分組,每組一行說明。用 `#### 檔案` 與 git log 交叉比對;兩邊對不上時以 git 為準。
35
+ 2. **Decisions**:取自 Rounds 的 `<decisions>`(舊格式 `#### 決策`),只留最終版本;被後面 Round 推翻或改掉的不列。沒有就寫「無」。
36
+ 3. **Changes**:依 commit 或檔案分組,每組一行說明。用 `<files>`(舊格式 `#### 檔案`)與 git log 交叉比對;兩邊對不上時以 git 為準。
37
37
  4. **Test plan**:取自 Rounds 的驗證紀錄,列出實際跑過的指令與結果。沒有紀錄就寫「未記錄」——**不要捏造沒跑過的測試。**
38
38
 
39
39
  專案的 CLAUDE.md/AGENTS.md 若規定 PR 描述結尾格式(例如署名行),照做;此外不要自己加簽名。
@@ -12,7 +12,7 @@ DEVLOG_PROJECT_DIR="<剛才記下的專案根目錄絕對路徑,不要用 $(pw
12
12
  若回傳 `MISSING`,列出 `CANDIDATES` 讓使用者選,不要自動執行工作。
13
13
  找到檔案後,讀最後一個歷史 Round。`DONE`:告訴使用者該主題已結束,等新需求。不核對、不開工。
14
14
  `IN_PROGRESS`、`INTERRUPTED`、`BLOCKED`:先做 `commands/continue.md` **步驟 5.1–5.2**(沿用上面同一個專案根目錄絕對路徑,不要重新推;不要跟著做 5.3)。步驟 5.2 的 `### 段落` 寫進 `devlog.md` 的 open Round,不要寫進 keep 檔。然後依核對後的**實際工作樹**提出接續方式,**等待使用者確認後才做下一步**:
15
- - `IN_PROGRESS`/`INTERRUPTED`:提出 Handoff「下一步」(沒有就依「現況」與實際工作樹推)。
15
+ - `IN_PROGRESS`/`INTERRUPTED`:提出 Handoff 的下一步(`<next>`;舊格式 `#### 下一步`)(沒有就依現況(`<state>`;舊格式 `#### 現況`)與實際工作樹推)。
16
16
  - `BLOCKED`:說明缺什麼;缺的外部輸入已經出現就提出下一步,仍缺就停。git 相不相符不能證明缺件已到,不要發明輸入。
17
17
 
18
18
  後續紀錄一律寫進 `.devlog/devlog.md`,不要改寫具名 keep 檔。使用者確認並開工後,**同一輪必須收尾寫回** Summary/Reply/Handoff/Status(L1 寫回義務同 `commands/continue.md` 步驟 6)。
@@ -129,8 +129,11 @@ awk -v reason="$REASON" -v detail="$DETAIL" -v recovered="$RECOVERED" '
129
129
  }
130
130
  if (!has_h) {
131
131
  print "### Handoff"
132
- print "#### 現況"
132
+ print "<handoff>"
133
+ print "<state>"
133
134
  print handoff_stub()
135
+ print "</state>"
136
+ print "</handoff>"
134
137
  print ""
135
138
  }
136
139
  print "### Status"
@@ -154,8 +157,11 @@ awk -v reason="$REASON" -v detail="$DETAIL" -v recovered="$RECOVERED" '
154
157
  }
155
158
  if (!has_h) {
156
159
  print "### Handoff"
157
- print "#### 現況"
160
+ print "<handoff>"
161
+ print "<state>"
158
162
  print handoff_stub()
163
+ print "</state>"
164
+ print "</handoff>"
159
165
  print ""
160
166
  }
161
167
  print "### Status"
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env bash
2
2
 
3
3
  _DEVLOG_MD_DIR="$(cd "${BASH_SOURCE[0]%/*}" && pwd)"
4
+ # shellcheck source=handoff-fields.sh
5
+ . "$_DEVLOG_MD_DIR/handoff-fields.sh"
4
6
 
5
7
  # NOFENCE 防呆(跟 enforce-devlog.sh 同一套邏輯):如果 [start,end] 這個範圍
6
8
  # 內 ``` 記號數量是奇數,代表某處圍欄沒有正常收尾(例如 User Input 貼了一段
@@ -139,17 +141,10 @@ devlog_strip_lessons_index() {
139
141
  }
140
142
 
141
143
  devlog_round_workspace_body() {
142
- local nofence
143
- nofence="$(_devlog_fence_nofence "$1" "$2" "$3")"
144
- awk -v start="$2" -v end="$3" -v nofence="$nofence" '
145
- NR < start || NR > end { next }
146
- /^[ \t]*```/ { if (!nofence) fence = !fence; next }
147
- !fence && /^#### 工作區[[:space:]]*$/ { grab = 1; next }
148
- grab && !fence && /^#### / { grab = 0 }
149
- grab && !fence && /^### / { grab = 0 }
150
- grab && !fence && /^## / { grab = 0 }
151
- grab { print }
152
- ' "$1" | sed -e '/^[[:space:]]*$/d'
144
+ local blob body
145
+ blob="$(awk -v start="$2" -v end="$3" 'NR >= start && NR <= end' "$1")"
146
+ body="$(handoff_section_of "$blob" Handoff)"
147
+ handoff_field "$body" workspace | sed -e '/^[[:space:]]*$/d'
153
148
  }
154
149
 
155
150
  devlog_round_segments_body() {