devlog-tracker 0.25.1 → 0.29.2

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 (130) hide show
  1. package/README.md +131 -151
  2. package/README.zh-TW.md +235 -0
  3. package/claude/hooks.json +75 -0
  4. package/cli/agents-md.js +15 -8
  5. package/cli/agents-md.test.js +21 -1
  6. package/cli/init.js +6 -4
  7. package/cli/init.test.js +5 -4
  8. package/cli/merge-hooks.js +5 -2
  9. package/cli/merge-hooks.test.js +52 -0
  10. package/cli/platforms/claude.js +55 -0
  11. package/cli/platforms/claude.test.js +178 -0
  12. package/cli/platforms/codex.js +6 -18
  13. package/cli/skills-from-commands.js +32 -0
  14. package/cli/skills-from-commands.test.js +51 -0
  15. package/cli/vendor.js +8 -1
  16. package/cli/vendor.test.js +24 -5
  17. package/codex/hooks/on-pre-tool.sh +3 -3
  18. package/codex/hooks/on-session-end.sh +2 -2
  19. package/codex/hooks/on-session-start.sh +2 -2
  20. package/codex/hooks/on-stop.sh +3 -3
  21. package/codex/hooks/on-user-prompt-submit.sh +2 -2
  22. package/codex/hooks/test-adapters.sh +2 -2
  23. package/commands/checkpoint.md +2 -2
  24. package/commands/clean.md +3 -3
  25. package/commands/compact.md +3 -3
  26. package/commands/continue.md +4 -4
  27. package/commands/keep.md +21 -5
  28. package/commands/lessons-drift.md +4 -4
  29. package/commands/lessons-off.md +3 -3
  30. package/commands/lessons-on.md +3 -3
  31. package/commands/lessons.md +3 -3
  32. package/commands/overview.md +3 -3
  33. package/commands/pause.md +3 -3
  34. package/commands/resume.md +3 -3
  35. package/commands/search.md +22 -0
  36. package/commands/segment-watch.md +3 -3
  37. package/commands/span.md +2 -2
  38. package/commands/start.md +3 -3
  39. package/commands/status.md +5 -4
  40. package/{hooks → core}/scripts/await-open.sh +1 -1
  41. package/{hooks → core}/scripts/checkpoint-set.sh +1 -1
  42. package/{hooks → core}/scripts/clean-devlog.sh +2 -1
  43. package/{hooks → core}/scripts/close-open-round.sh +1 -1
  44. package/{hooks → core}/scripts/compact-devlog.sh +1 -1
  45. package/{hooks → core}/scripts/devlog-lock.sh +17 -1
  46. package/{hooks → core}/scripts/devlog-path.sh +15 -8
  47. package/{hooks → core}/scripts/enforce-devlog.sh +26 -1
  48. package/core/scripts/handoff-file.sh +106 -0
  49. package/{hooks → core}/scripts/keep-move.sh +1 -1
  50. package/{hooks → core}/scripts/kept-list.sh +1 -1
  51. package/core/scripts/lessons-advisory-state.sh +51 -0
  52. package/{hooks → core}/scripts/lessons-append.sh +18 -2
  53. package/core/scripts/lessons-drift-set.sh +46 -0
  54. package/{hooks → core}/scripts/lessons-off.sh +1 -1
  55. package/{hooks → core}/scripts/lessons-on.sh +9 -4
  56. package/{hooks → core}/scripts/lessons-read.sh +1 -1
  57. package/{hooks → core}/scripts/on-tool-failure.sh +1 -1
  58. package/{hooks → core}/scripts/pause-devlog.sh +1 -1
  59. package/core/scripts/project-dir.sh +19 -0
  60. package/{hooks → core}/scripts/resume-devlog.sh +1 -1
  61. package/{hooks → core}/scripts/round-start.sh +52 -17
  62. package/{hooks → core}/scripts/run-tests.sh +1 -1
  63. package/core/scripts/search-devlog.sh +63 -0
  64. package/{hooks → core}/scripts/segment-watch-set.sh +1 -1
  65. package/{hooks → core}/scripts/segment-watch.sh +1 -1
  66. package/{hooks → core}/scripts/session-start-devlog.sh +8 -1
  67. package/{hooks → core}/scripts/span-close.sh +1 -1
  68. package/{hooks → core}/scripts/span-open.sh +1 -1
  69. package/{hooks → core}/scripts/start-devlog.sh +1 -1
  70. package/{hooks → core}/scripts/status-devlog.sh +5 -5
  71. package/{hooks → core}/scripts/tests/test-clean-devlog.sh +12 -0
  72. package/core/scripts/tests/test-cli-init-e2e.sh +68 -0
  73. package/core/scripts/tests/test-devlog-lock.sh +76 -0
  74. package/{hooks → core}/scripts/tests/test-devlog-path.sh +44 -0
  75. package/{hooks → core}/scripts/tests/test-enforce-devlog-files.sh +11 -0
  76. package/core/scripts/tests/test-enforce-devlog-session-handoff.sh +178 -0
  77. package/{hooks → core}/scripts/tests/test-enforce-devlog-workspace.sh +35 -0
  78. package/{hooks → core}/scripts/tests/test-enforce-devlog.sh +174 -0
  79. package/core/scripts/tests/test-handoff-file.sh +98 -0
  80. package/core/scripts/tests/test-lessons-advisory-state.sh +55 -0
  81. package/{hooks → core}/scripts/tests/test-lessons-append.sh +18 -0
  82. package/{hooks → core}/scripts/tests/test-lessons-drift-set.sh +14 -5
  83. package/{hooks → core}/scripts/tests/test-lessons-on-off.sh +16 -6
  84. package/core/scripts/tests/test-project-dir.sh +30 -0
  85. package/{hooks → core}/scripts/tests/test-round-start.sh +247 -13
  86. package/core/scripts/tests/test-search-devlog.sh +126 -0
  87. package/{hooks → core}/scripts/tests/test-session-start-devlog.sh +61 -2
  88. package/{hooks → core}/scripts/tests/test-status-span.sh +3 -3
  89. package/{hooks → core}/scripts/workspace-snapshot.sh +2 -2
  90. package/cursor/hooks/on-pre-tool.sh +2 -2
  91. package/cursor/hooks/on-session-end.sh +2 -2
  92. package/cursor/hooks/on-session-start.sh +2 -2
  93. package/cursor/hooks/on-stop.sh +2 -2
  94. package/cursor/hooks/on-submit-prompt.sh +2 -2
  95. package/cursor/hooks/on-tool-failure.sh +2 -2
  96. package/package.json +5 -4
  97. package/skills/devlog-tracker/SKILL.md +90 -22
  98. package/skills/devlog-tracker/references/checkpoint-mode.md +21 -6
  99. package/skills/devlog-tracker/references/contract.md +83 -0
  100. package/skills/devlog-tracker/references/lessons-mode.md +33 -9
  101. package/skills/devlog-tracker/references/reply-fold.md +1 -1
  102. package/hooks/scripts/lessons-drift-set.sh +0 -42
  103. package/hooks/scripts/tests/test-cli-init-e2e.sh +0 -38
  104. package/hooks/scripts/tests/test-devlog-lock.sh +0 -37
  105. /package/{hooks → core}/scripts/detect-pending-question.sh +0 -0
  106. /package/{hooks → core}/scripts/devlog-md.sh +0 -0
  107. /package/{hooks → core}/scripts/files-snapshot.sh +0 -0
  108. /package/{hooks → core}/scripts/json-field.sh +0 -0
  109. /package/{hooks → core}/scripts/on-session-end.sh +0 -0
  110. /package/{hooks → core}/scripts/on-stop-failure.sh +0 -0
  111. /package/{hooks → core}/scripts/redact-prompt.sh +0 -0
  112. /package/{hooks → core}/scripts/tests/test-await-open.sh +0 -0
  113. /package/{hooks → core}/scripts/tests/test-branch-scoped-integration.sh +0 -0
  114. /package/{hooks → core}/scripts/tests/test-checkpoint-set.sh +0 -0
  115. /package/{hooks → core}/scripts/tests/test-close-open-round.sh +0 -0
  116. /package/{hooks → core}/scripts/tests/test-compact-devlog.sh +0 -0
  117. /package/{hooks → core}/scripts/tests/test-devlog-md.sh +0 -0
  118. /package/{hooks → core}/scripts/tests/test-enforce-devlog-handoff-order.sh +0 -0
  119. /package/{hooks → core}/scripts/tests/test-files-snapshot.sh +0 -0
  120. /package/{hooks → core}/scripts/tests/test-json-field.sh +0 -0
  121. /package/{hooks → core}/scripts/tests/test-keep-move.sh +0 -0
  122. /package/{hooks → core}/scripts/tests/test-kept-list.sh +0 -0
  123. /package/{hooks → core}/scripts/tests/test-lessons-read.sh +0 -0
  124. /package/{hooks → core}/scripts/tests/test-on-interrupt.sh +0 -0
  125. /package/{hooks → core}/scripts/tests/test-redact-prompt.sh +0 -0
  126. /package/{hooks → core}/scripts/tests/test-resume-devlog.sh +0 -0
  127. /package/{hooks → core}/scripts/tests/test-segment-watch-set.sh +0 -0
  128. /package/{hooks → core}/scripts/tests/test-segment-watch.sh +0 -0
  129. /package/{hooks → core}/scripts/tests/test-start-pause-devlog.sh +0 -0
  130. /package/{hooks → core}/scripts/tests/test-workspace-snapshot.sh +0 -0
@@ -4,10 +4,10 @@ description: 關閉 Lessons Mode。不會刪除任何已寫的 devlog.lessons.*.
4
4
 
5
5
  請執行:
6
6
 
7
- 1. 先決定 plugin 根目錄(有 `CLAUDE_PLUGIN_ROOT` 用它;否則用 `DEVLOG_TRACKER_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
7
+ 1. 先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
8
8
  ```bash
9
- PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${DEVLOG_TRACKER_ROOT:-}}"
10
- CLAUDE_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/hooks/scripts/lessons-off.sh"
9
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
10
+ DEVLOG_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/core/scripts/lessons-off.sh"
11
11
  ```
12
12
  不要自己刪 `.lessons-enabled`。
13
13
  2. stdout 是 `NOT_ENABLED`:告知 Lessons Mode 本來就沒開,結束。
@@ -4,10 +4,10 @@ description: 開啟 Lessons Mode(開發歷程教訓,預設關閉)。隸屬
4
4
 
5
5
  請執行:
6
6
 
7
- 1. 先決定 plugin 根目錄(有 `CLAUDE_PLUGIN_ROOT` 用它;否則用 `DEVLOG_TRACKER_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
7
+ 1. 先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
8
8
  ```bash
9
- PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${DEVLOG_TRACKER_ROOT:-}}"
10
- CLAUDE_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/hooks/scripts/lessons-on.sh"
9
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
10
+ DEVLOG_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/core/scripts/lessons-on.sh"
11
11
  ```
12
12
  不要自己用手建 `.lessons-enabled`。
13
13
  2. stdout 是 `NOT_ENABLED`:告知這個專案還沒下過 `/devlog-tracker:start`,Lessons Mode 隸屬主開關,沒有 Round/Status 紀錄可判斷「BLOCKED→解開」,請先 `/devlog-tracker:start` 再開這個。
@@ -2,11 +2,11 @@
2
2
  description: 查看 Lessons 索引,或讀某個主題的完整教訓紀錄(純讀取,不核對工作區、不等確認)。
3
3
  ---
4
4
 
5
- 取得使用者是否有給 `<topic>`(可能沒有,代表只看索引)。記下你目前已經確認的專案根目錄絕對路徑(後面步驟都要用這個值,不要用 `$(pwd)` 重新推——理由同 `commands/continue.md` 步驟 1)。先決定 plugin 根目錄(有 `CLAUDE_PLUGIN_ROOT` 用它;否則用 `DEVLOG_TRACKER_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄),再跑:
5
+ 取得使用者是否有給 `<topic>`(可能沒有,代表只看索引)。記下你目前已經確認的專案根目錄絕對路徑(後面步驟都要用這個值,不要用 `$(pwd)` 重新推——理由同 `commands/continue.md` 步驟 1)。先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄),再跑:
6
6
 
7
7
  ```bash
8
- PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${DEVLOG_TRACKER_ROOT:-}}"
9
- CLAUDE_PROJECT_DIR="<剛才記下的專案根目錄絕對路徑>" bash "${PLUGIN_ROOT}/hooks/scripts/lessons-read.sh" "<topic,沒有就留空>"
8
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
9
+ DEVLOG_PROJECT_DIR="<剛才記下的專案根目錄絕對路徑>" bash "${PLUGIN_ROOT}/core/scripts/lessons-read.sh" "<topic,沒有就留空>"
10
10
  ```
11
11
 
12
12
  沒有 `<topic>`:
@@ -8,11 +8,11 @@ description: 把已 keep 的具名檔(devlog.<name>.md)整合成一份跨主
8
8
 
9
9
  ## 1. 取得已 keep 的檔案清單
10
10
 
11
- 記下你目前已經確認的專案根目錄絕對路徑(後面步驟都要用這個值,不要用 `$(pwd)` 重新推——理由同 `commands/continue.md` 步驟 1)。先決定 plugin 根目錄(有 `CLAUDE_PLUGIN_ROOT` 用它;否則用 `DEVLOG_TRACKER_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄),再跑:
11
+ 記下你目前已經確認的專案根目錄絕對路徑(後面步驟都要用這個值,不要用 `$(pwd)` 重新推——理由同 `commands/continue.md` 步驟 1)。先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄),再跑:
12
12
 
13
13
  ```bash
14
- PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${DEVLOG_TRACKER_ROOT:-}}"
15
- CLAUDE_PROJECT_DIR="<剛才記下的專案根目錄絕對路徑>" bash "${PLUGIN_ROOT}/hooks/scripts/kept-list.sh"
14
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
15
+ DEVLOG_PROJECT_DIR="<剛才記下的專案根目錄絕對路徑>" bash "${PLUGIN_ROOT}/core/scripts/kept-list.sh"
16
16
  ```
17
17
 
18
18
  - `NO_INDEX`:告知「目前沒有已 keep 的舊檔」,結束。
package/commands/pause.md CHANGED
@@ -6,11 +6,11 @@ description: 暫停這個專案的 devlog 強制記錄機制。不會刪除任
6
6
 
7
7
  1. 跑:
8
8
  ```bash
9
- 先決定 plugin 根目錄(有 `CLAUDE_PLUGIN_ROOT` 用它;否則用 `DEVLOG_TRACKER_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
9
+ 先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
10
10
 
11
11
  ```bash
12
- PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${DEVLOG_TRACKER_ROOT:-}}"
13
- CLAUDE_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/hooks/scripts/pause-devlog.sh"
12
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
13
+ DEVLOG_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/core/scripts/pause-devlog.sh"
14
14
  ```
15
15
  ```
16
16
  不要自己刪 `.enabled`。
@@ -2,11 +2,11 @@
2
2
  description: 讀取具名保存的 devlog,核對最後一輪 Handoff 工作區後再接續該段工作。
3
3
  ---
4
4
 
5
- 取得使用者提供的 `<name>`;沒有名稱時先詢問。記下你目前已經確認的專案根目錄絕對路徑(後面步驟都要用這個值,不要用 `$(pwd)` 重新推——理由同 `commands/continue.md` 步驟 1:Bash 工具的工作目錄會在對話裡持續累積前面呼叫的 `cd`,用當下的 `pwd` 可能已經不是這個專案根目錄)。先決定 plugin 根目錄(有 `CLAUDE_PLUGIN_ROOT` 用它;否則用 `DEVLOG_TRACKER_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄),再跑:
5
+ 取得使用者提供的 `<name>`;沒有名稱時先詢問。記下你目前已經確認的專案根目錄絕對路徑(後面步驟都要用這個值,不要用 `$(pwd)` 重新推——理由同 `commands/continue.md` 步驟 1:Bash 工具的工作目錄會在對話裡持續累積前面呼叫的 `cd`,用當下的 `pwd` 可能已經不是這個專案根目錄)。先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄),再跑:
6
6
 
7
7
  ```bash
8
- PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${DEVLOG_TRACKER_ROOT:-}}"
9
- CLAUDE_PROJECT_DIR="<剛才記下的專案根目錄絕對路徑,不要用 $(pwd) 重新推>" bash "${PLUGIN_ROOT}/hooks/scripts/resume-devlog.sh" --name "<name>"
8
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
9
+ DEVLOG_PROJECT_DIR="<剛才記下的專案根目錄絕對路徑,不要用 $(pwd) 重新推>" bash "${PLUGIN_ROOT}/core/scripts/resume-devlog.sh" --name "<name>"
10
10
  ```
11
11
 
12
12
  若回傳 `MISSING`,列出 `CANDIDATES` 讓使用者選,不要自動執行工作。
@@ -0,0 +1,22 @@
1
+ ---
2
+ description: 在 devlog.md/archive/keep 檔/lessons 檔裡搜尋關鍵字,讀完命中後用自己的話回答(純讀取,不核對工作區、不等確認)。
3
+ ---
4
+
5
+ 取得使用者提供的 `<關鍵字>`(或自然語言查詢裡的關鍵片語);沒有給的話先問。這是純讀取,不做 `commands/continue.md`/`commands/resume.md` 那套「核對工作區、等確認才動手」流程——搜尋結果是導航用的參考,不是暫停中的工作主題。讀完之後不要自動據此修改任何檔案,除非使用者接著明確要求。
6
+
7
+ 記下你目前已經確認的專案根目錄絕對路徑(後面步驟都要用這個值,不要用 `$(pwd)` 重新推——理由同 `commands/continue.md` 步驟 1)。先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄),再跑:
8
+
9
+ ```bash
10
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
11
+ DEVLOG_PROJECT_DIR="<剛才記下的專案根目錄絕對路徑>" bash "${PLUGIN_ROOT}/core/scripts/search-devlog.sh" "<關鍵字>"
12
+ ```
13
+
14
+ 這支腳本會掃過 `.devlog/devlog*.md`(含 `devlog.md`、`devlog.archive.md`、已 keep 的 `devlog.<name>.md`、`devlog.lessons.<topic>.md`,以及分支各自的 `devlog.<branch>.md`——一個 glob 涵蓋所有種類,不分別處理),做不分大小寫的字串比對,不是正則、不含 vector/LLM。自然語言查詢由你先抽出要搜的關鍵片語再丟給腳本;腳本本身不做 NLP。
15
+
16
+ - `NO_INDEX`:告知目前還沒有任何 devlog 檔案,結束。
17
+ - `NO_MATCH`:告知這個關鍵字沒有命中,結束。
18
+ - 其他輸出:每個有命中的檔案先印一行 `FILE=.devlog/<檔名>`,接著是該檔案裡每一行命中,格式 `HEADING="<離命中最近的上方 ## 或 ### 標題>" LINE=<行號>: <命中行原文>`。
19
+
20
+ **回答姿勢(跟 `/devlog-tracker:overview` 不同,也跟「原樣貼出列表」不同):** 讀完腳本輸出後,用自己的話回答使用者在問什麼(決策、現況、誰提過什麼),把命中當依據串成敘事;必要時附上檔名、最近標題、行號當出處。不要把 `FILE=`/`HEADING=` 原始輸出整段貼給使用者當主回答。命中很多時先摘要再說細節,不要機械 dump。若命中落在具名檔或 lessons 檔,可提醒再用 `/devlog-tracker:resume <name>` 或 `/devlog-tracker:lessons <topic>` 看全文。
21
+
22
+ 標題比對不是圍欄感知的(不特別處理 ``` 區塊),命中或標題落在程式碼區塊裡時仍會照樣列出;這是刻意的簡化,換取不用重新實作 `devlog-md.sh` 的圍欄邏輯。
@@ -6,11 +6,11 @@ description: 調整 Segment Watch 的沉默門檻——同一輪連續多久沒
6
6
  換算成整數秒數 `<seconds>`,跑:
7
7
 
8
8
  ```bash
9
- 先決定 plugin 根目錄(有 `CLAUDE_PLUGIN_ROOT` 用它;否則用 `DEVLOG_TRACKER_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
9
+ 先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
10
10
 
11
11
  ```bash
12
- PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${DEVLOG_TRACKER_ROOT:-}}"
13
- CLAUDE_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/hooks/scripts/segment-watch-set.sh" <seconds>
12
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
13
+ DEVLOG_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/core/scripts/segment-watch-set.sh" <seconds>
14
14
  ```
15
15
  ```
16
16
 
package/commands/span.md CHANGED
@@ -5,9 +5,9 @@ description: 開啟或關閉 Span Mode,讓自動續接的長任務定期記錄
5
5
  先判斷使用者要開啟或關閉:
6
6
 
7
7
  - 使用者明確要求關閉,或 `.devlog/.span-open` 已存在且沒有明確要求重新開啟:
8
- 先決定 plugin 根目錄(有 `CLAUDE_PLUGIN_ROOT` 用它;否則用 `DEVLOG_TRACKER_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄),設 `PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${DEVLOG_TRACKER_ROOT:-}}"`,再跑 `CLAUDE_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/hooks/scripts/span-close.sh"`,
8
+ 先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄),設 `PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"`,再跑 `DEVLOG_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/core/scripts/span-close.sh"`,
9
9
  再依 SKILL 的 span 收尾規則寫一個**新的 Round**,總結整段 span。
10
10
  - 使用者要求開啟:先把目前 Round 正常寫完,`Status` 設為 `IN_PROGRESS`,再跑
11
- `CLAUDE_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/hooks/scripts/span-open.sh"`。
11
+ `DEVLOG_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/core/scripts/span-open.sh"`。
12
12
 
13
13
  不要手寫 `.span-open` JSON。腳本 exit 1 時顯示 stderr,停止操作。
package/commands/start.md CHANGED
@@ -4,10 +4,10 @@ description: 啟動這個專案的 devlog 強制記錄機制。之後每一輪
4
4
 
5
5
  請執行以下步驟:
6
6
 
7
- 1. 先決定 plugin 根目錄(有 `CLAUDE_PLUGIN_ROOT` 用它;否則用 `DEVLOG_TRACKER_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
7
+ 1. 先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
8
8
  ```bash
9
- PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${DEVLOG_TRACKER_ROOT:-}}"
10
- CLAUDE_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/hooks/scripts/start-devlog.sh"
9
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
10
+ DEVLOG_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/core/scripts/start-devlog.sh"
11
11
  ```
12
12
  不要自己用手建 `.enabled` / `.checkpoint-state` / `.segment-state`。
13
13
  2. 若 stdout 有 `GITIGNORE_DEVLOG=no`:鄭重提醒——`.devlog/` 會寫入使用者原文(遮罩只覆蓋常見 token 前綴,不是通用掃密)。**強烈建議**把 `.devlog/` 加進專案 `.gitignore`。問要不要現在加。只有使用者明確說要,才在 `.gitignore` 末尾追加一行 `.devlog/`(檔案不存在就建立)。不要改其他行。若使用者拒絕,再警告一次「之後若不小心 commit,prompt/殘留密鑰可能進版控」,然後繼續步驟 3。
@@ -4,11 +4,12 @@ description: 查看這個專案 devlog 強制記錄是否開著、span / checkpo
4
4
 
5
5
  跑:
6
6
  ```bash
7
- 先決定 plugin 根目錄(有 `CLAUDE_PLUGIN_ROOT` 用它;否則用 `DEVLOG_TRACKER_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
7
+ 先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
8
8
 
9
9
  ```bash
10
- PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${DEVLOG_TRACKER_ROOT:-}}"
11
- CLAUDE_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/hooks/scripts/status-devlog.sh"
10
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
11
+ DEVLOG_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/core/scripts/status-devlog.sh"
12
12
  ```
13
13
  ```
14
- 把 stdout 翻譯成給人看的幾行(含 `LESSONS=yes/no`:Lessons Mode 開關狀態;`LESSONS_DRIFT=<count>/<threshold>`:Lessons Mode 開著時,工作區漂移不符的累積次數/門檻)。不要改任何檔。`NOT_STARTED` 就說還沒 `/devlog-tracker:start`。
14
+ 把 stdout 翻譯成給人看的幾行(含 `LESSONS=yes/no`:Lessons Mode 開關狀態;`LESSONS_ADVISORY=<count>/<threshold>`:Lessons Mode 開著時,機制性訊號(工作區漂移不符、或
15
+ BLOCKED 輪次累積)的共用累積次數/門檻)。不要改任何檔。`NOT_STARTED` 就說還沒 `/devlog-tracker:start`。
@@ -9,7 +9,7 @@ SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
9
9
  # shellcheck source=devlog-path.sh
10
10
  . "$SCRIPT_DIR/devlog-path.sh"
11
11
 
12
- devlog_resolve_paths "${CLAUDE_PROJECT_DIR:-.}"
12
+ devlog_resolve_paths "${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}"
13
13
  [ -f "$DEVLOG_DIR/.enabled" ] || { echo "NOT_ENABLED"; exit 1; }
14
14
  [ ! -e "$DEVLOG_DIR/.awaiting-reply" ] || { echo "ALREADY_OPEN"; exit 1; }
15
15
  ROUND=""
@@ -8,7 +8,7 @@ SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
8
8
  # shellcheck source=json-field.sh
9
9
  . "$SCRIPT_DIR/json-field.sh"
10
10
 
11
- PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
11
+ PROJECT_DIR="${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}"
12
12
  DEVLOG_DIR="$PROJECT_DIR/.devlog"
13
13
 
14
14
  if [ ! -f "$DEVLOG_DIR/.enabled" ]; then
@@ -13,7 +13,7 @@ SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
13
13
  # replacement for that confirmation.
14
14
  [ "${1:-}" = "--confirmed" ] || { echo "需要 --confirmed(使用者尚未確認,不要呼叫這支腳本)" >&2; exit 1; }
15
15
 
16
- devlog_resolve_paths "${CLAUDE_PROJECT_DIR:-.}"
16
+ devlog_resolve_paths "${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}"
17
17
  devlog_lock_acquire
18
18
  trap 'devlog_lock_release' EXIT
19
19
  MAIN="$DEVLOG_FILE"
@@ -60,6 +60,7 @@ else
60
60
  fi
61
61
 
62
62
  rm -f "$DEVLOG_DIR/.span-open" "$DEVLOG_DIR/.interrupted" "$DEVLOG_DIR/.awaiting-reply"
63
+ rm -f "$HANDOFF_FILE"
63
64
  if [ -f "$DEVLOG_DIR/.checkpoint-state" ]; then
64
65
  json_int_set "$DEVLOG_DIR/.checkpoint-state" rounds_since_checkpoint 0
65
66
  json_int_set "$DEVLOG_DIR/.checkpoint-state" checkpoint_marker_count 0
@@ -32,7 +32,7 @@ SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
32
32
  # shellcheck source=devlog-md.sh
33
33
  . "$SCRIPT_DIR/devlog-md.sh"
34
34
 
35
- PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
35
+ PROJECT_DIR="${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}"
36
36
  devlog_resolve_paths "$PROJECT_DIR"
37
37
  ENABLED_FLAG="$DEVLOG_DIR/.enabled"
38
38
  ROUND_OPEN="$DEVLOG_DIR/.round-open"
@@ -7,7 +7,7 @@ SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
7
7
  . "$SCRIPT_DIR/devlog-lock.sh"
8
8
  . "$SCRIPT_DIR/devlog-path.sh"
9
9
 
10
- devlog_resolve_paths "${CLAUDE_PROJECT_DIR:-.}"
10
+ devlog_resolve_paths "${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}"
11
11
  devlog_lock_acquire
12
12
  trap 'devlog_lock_release' EXIT
13
13
  MAIN="$DEVLOG_FILE"
@@ -2,8 +2,9 @@
2
2
  # Sourced helper for serializing writes under .devlog.
3
3
  devlog_lock_acquire() {
4
4
  local dir="${DEVLOG_DIR:-.}/.lock"
5
- local start now
5
+ local start now pid
6
6
  LOCK_HELD=0
7
+ LOCK_CONTENDED_BY=""
7
8
  command -v mkdir >/dev/null 2>&1 || return 0
8
9
  command -v rmdir >/dev/null 2>&1 || return 0
9
10
  command -v date >/dev/null 2>&1 || return 0
@@ -12,10 +13,24 @@ devlog_lock_acquire() {
12
13
  while true; do
13
14
  if mkdir "$dir" 2>/dev/null; then
14
15
  LOCK_HELD=1
16
+ printf '%s\n' "$$" > "$dir/pid" 2>/dev/null || true
15
17
  return 0
16
18
  fi
19
+ pid="$(cat "$dir/pid" 2>/dev/null || true)"
20
+ case "$pid" in
21
+ ''|*[!0-9]*) pid='' ;;
22
+ esac
23
+ # Stale lock: the pid that created it is no longer running (a crashed
24
+ # session), so reclaim it immediately instead of waiting out the full
25
+ # contention timeout below.
26
+ if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; then
27
+ rm -rf "$dir" 2>/dev/null || true
28
+ continue
29
+ fi
17
30
  now="$(date +%s 2>/dev/null || echo 0)"
18
31
  if [ $((now - start)) -ge 2 ]; then
32
+ # shellcheck disable=SC2034 # consumed by callers (e.g. enforce-devlog.sh), not used in this file
33
+ LOCK_CONTENDED_BY="$pid"
19
34
  return 0
20
35
  fi
21
36
  sleep 0.1 2>/dev/null || true
@@ -24,6 +39,7 @@ devlog_lock_acquire() {
24
39
 
25
40
  devlog_lock_release() {
26
41
  if [ "${LOCK_HELD:-0}" -eq 1 ]; then
42
+ rm -f "${DEVLOG_DIR:-.}/.lock/pid" 2>/dev/null || true
27
43
  rmdir "${DEVLOG_DIR:-.}/.lock" 2>/dev/null || true
28
44
  LOCK_HELD=0
29
45
  fi
@@ -14,18 +14,23 @@ _devlog_sanitize_name() {
14
14
  printf '%s' "$1" | sed -E 's/[^A-Za-z0-9._-]/-/g; s/-+/-/g; s/^-+//; s/-+$//'
15
15
  }
16
16
 
17
- # Sets DEVLOG_DIR and DEVLOG_FILE for the branch currently checked out in
18
- # $1 (defaults to "."). main/master (any case) and anything that isn't a
19
- # git repo or has no resolvable branch keep the shared devlog.md. Any
20
- # other branch gets devlog.<sanitized-branch>.md; a detached HEAD (or an
21
- # unborn branch, which git also reports as "HEAD" here) falls back to the
22
- # working directory's own basename. The first time a branch resolves to a
23
- # file that doesn't exist yet while devlog.md already has content, the
24
- # existing devlog.md is renamed (not copied) into that branch's file.
17
+ # Sets DEVLOG_DIR, DEVLOG_FILE, and HANDOFF_FILE for the branch currently
18
+ # checked out in $1 (defaults to "."). main/master (any case) and anything
19
+ # that isn't a git repo or has no resolvable branch keep the shared
20
+ # devlog.md / handoff.md. Any other branch gets
21
+ # devlog.<sanitized-branch>.md and handoff.<sanitized-branch>.md; a
22
+ # detached HEAD (or an unborn branch, which git also reports as "HEAD"
23
+ # here) falls back to the working directory's own basename. The first
24
+ # time a branch resolves to a file that doesn't exist yet while
25
+ # devlog.md already has content, the existing devlog.md is renamed (not
26
+ # copied) into that branch's file. handoff.md is never renamed on first
27
+ # resolve (current-state snapshot, not history).
25
28
  devlog_resolve_paths() {
26
29
  local dir="${1:-.}"
27
30
  DEVLOG_DIR="$dir/.devlog"
28
31
  DEVLOG_FILE="$DEVLOG_DIR/devlog.md"
32
+ # shellcheck disable=SC2034 # consumed by callers (e.g. enforce-devlog.sh), not used in this file
33
+ HANDOFF_FILE="$DEVLOG_DIR/handoff.md"
29
34
 
30
35
  local branch raw name
31
36
  branch="$(git -C "$dir" rev-parse --abbrev-ref HEAD 2>/dev/null || echo '')"
@@ -67,4 +72,6 @@ devlog_resolve_paths() {
67
72
  devlog_lock_release
68
73
  fi
69
74
  DEVLOG_FILE="$resolved"
75
+ # shellcheck disable=SC2034 # consumed by callers (e.g. enforce-devlog.sh), not used in this file
76
+ HANDOFF_FILE="$DEVLOG_DIR/handoff.$name.md"
70
77
  }
@@ -31,13 +31,15 @@ SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
31
31
  . "$SCRIPT_DIR/files-snapshot.sh"
32
32
  # shellcheck source=devlog-md.sh
33
33
  . "$SCRIPT_DIR/devlog-md.sh"
34
+ # shellcheck source=handoff-file.sh
35
+ . "$SCRIPT_DIR/handoff-file.sh"
34
36
 
35
37
  # --- loop guard -------------------------------------------------------
36
38
  # 有 jq 就用 jq 精準解析;沒有 jq 就退化成字串比對(沒有更嚴謹的 parse,但
37
39
  # 足以涵蓋 Claude Code 實際送出的 stop_hook_active 欄位形狀),兩種環境都要生效。
38
40
  INPUT="$(cat 2>/dev/null || true)"
39
41
 
40
- PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
42
+ PROJECT_DIR="${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}"
41
43
  # 沒下過 /devlog-tracker:start,代表這個專案沒啟動強制記錄,直接放行。
42
44
  # 這是唯一的判斷依據——不猜這輪是否呼叫了某個 skill,也不解析 transcript。
43
45
  # 這個 gate 放在路徑解析之前,沒啟用時就不必付 git rev-parse 的成本;
@@ -89,6 +91,10 @@ TURN_MARKER="$DEVLOG_DIR/.turn-start"
89
91
 
90
92
  devlog_lock_acquire
91
93
  trap 'devlog_lock_release' EXIT
94
+ if [ -n "${LOCK_CONTENDED_BY:-}" ]; then
95
+ echo "另一個 session(pid ${LOCK_CONTENDED_BY})目前正在寫 ${DEVLOG_FILE},稍後再結束這一輪重試一次。" >&2
96
+ exit 2
97
+ fi
92
98
 
93
99
  # --- span 檢查(Span Mode:橫跨多次自動續接的長任務)---------------------
94
100
  # Claude 主動宣告的 .devlog/.span-open 存在時(見 SKILL.md),這個 tick 不
@@ -550,6 +556,15 @@ ${ACTUAL_DIRTY:-(沒有,工作樹乾淨)}"
550
556
  exit 2
551
557
  fi
552
558
  fi
559
+
560
+ # Session Handoff → .devlog/handoff.md(docs/design/session-handoff-file.md)
561
+ # 放在其他 IN_PROGRESS/BLOCKED 檢查之後,避免搶先蓋掉既有失敗訊息。
562
+ if [ "$STATUS_VAL" = "IN_PROGRESS" ] || [ "$STATUS_VAL" = "BLOCKED" ]; then
563
+ if ! handoff_session_section_ok "$LAST_ROUND"; then
564
+ echo "Status 是 IN_PROGRESS 或 BLOCKED 時,必須有 \`### Session Handoff\`,且依序包含 \`#### 決策\`/\`#### 待解問題\`/\`#### 失敗嘗試\`(可寫 \`- (無)\`)。寫完後 hook 會覆寫 .devlog/handoff.md 給下一 session。" >&2
565
+ exit 2
566
+ fi
567
+ fi
553
568
  fi
554
569
 
555
570
  rm -f "$DEVLOG_DIR/.workspace-mismatch" 2>/dev/null || true
@@ -573,6 +588,16 @@ rm -f "$DEVLOG_DIR/.workspace-mismatch" 2>/dev/null || true
573
588
  if [ -n "$LAST_ROUND" ]; then
574
589
  if devlog_merge_round_current "$DEVLOG_FILE" "$ROUND_CURRENT"; then
575
590
  rm -f "$DEVLOG_DIR/.round-open" 2>/dev/null || true
591
+ case "${STATUS_VAL:-}" in
592
+ IN_PROGRESS|BLOCKED)
593
+ handoff_write "$HANDOFF_FILE" "$LAST_ROUND" 2>/dev/null || \
594
+ echo "警告:無法寫入 Session Handoff 檔($HANDOFF_FILE),本輪仍已收尾。" >&2
595
+ ;;
596
+ DONE)
597
+ handoff_clear "$HANDOFF_FILE" 2>/dev/null || \
598
+ echo "警告:無法清除 Session Handoff 檔($HANDOFF_FILE),本輪仍已收尾。" >&2
599
+ ;;
600
+ esac
576
601
  fi
577
602
  else
578
603
  rm -f "$DEVLOG_DIR/.round-open" 2>/dev/null || true
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env bash
2
+ # Sourced helper: validate / extract / write / clear Session Handoff file.
3
+ # See docs/design/session-handoff-file.md.
4
+
5
+ # Odd ``` count → treat as no fence (same fail-open as enforce-devlog.sh).
6
+ _handoff_nofence_flag() {
7
+ local blob="$1" n
8
+ n="$(printf '%s\n' "$blob" | grep -c '^[ \t]*```' 2>/dev/null || echo 0)"
9
+ case "$n" in ''|*[!0-9]*) n=0 ;; esac
10
+ if [ $((n % 2)) -eq 1 ]; then
11
+ echo 1
12
+ else
13
+ echo 0
14
+ fi
15
+ }
16
+
17
+ # Returns 0 if $1 (a Round blob) has ### Session Handoff with #### 決策 /
18
+ # #### 待解問題 / #### 失敗嘗試 in that order, each with a non-empty body.
19
+ handoff_session_section_ok() {
20
+ local blob="$1" nofence
21
+ nofence="$(_handoff_nofence_flag "$blob")"
22
+ printf '%s\n' "$blob" | awk -v nofence="$nofence" '
23
+ BEGIN { ok = 0 }
24
+ /^[ \t]*```/ { if (!nofence) fence = !fence; next }
25
+ fence { next }
26
+ /^### Session Handoff[ \t]*$/ { in_sh = 1; next }
27
+ in_sh && /^### / { exit }
28
+ in_sh && /^## / { exit }
29
+ in_sh && /^#### / {
30
+ name = $0
31
+ sub(/^#### [ \t]*/, "", name)
32
+ sub(/[ \t]+$/, "", name)
33
+ if (name == "決策" || name == "待解問題" || name == "失敗嘗試") {
34
+ if (expecting_body) { exit }
35
+ if (name == "決策") {
36
+ if (seen_decision || seen_open || seen_failed) exit
37
+ seen_decision = 1
38
+ expecting_body = 1
39
+ } else if (name == "待解問題") {
40
+ if (!seen_decision || seen_open || seen_failed) exit
41
+ seen_open = 1
42
+ expecting_body = 1
43
+ } else {
44
+ if (!seen_decision || !seen_open || seen_failed) exit
45
+ seen_failed = 1
46
+ expecting_body = 1
47
+ }
48
+ }
49
+ next
50
+ }
51
+ in_sh && expecting_body {
52
+ if ($0 ~ /[^[:space:]]/) {
53
+ expecting_body = 0
54
+ if (seen_decision && seen_open && seen_failed) ok = 1
55
+ }
56
+ next
57
+ }
58
+ END { exit(ok && seen_decision && seen_open && seen_failed && !expecting_body ? 0 : 1) }
59
+ '
60
+ }
61
+
62
+ # Print independent-file markdown from Round blob $1. Exit 1 if invalid.
63
+ handoff_extract_file_body() {
64
+ local blob="$1" nofence
65
+ handoff_session_section_ok "$blob" || return 1
66
+ nofence="$(_handoff_nofence_flag "$blob")"
67
+ printf '%s\n' "$blob" | awk -v nofence="$nofence" '
68
+ BEGIN { print "## Session Handoff"; print "" }
69
+ /^[ \t]*```/ { if (!nofence) fence = !fence; next }
70
+ fence { next }
71
+ /^### Session Handoff[ \t]*$/ { in_sh = 1; next }
72
+ in_sh && /^### / { exit }
73
+ in_sh && /^## / { exit }
74
+ in_sh && /^#### / {
75
+ name = $0
76
+ sub(/^#### [ \t]*/, "", name)
77
+ sub(/[ \t]+$/, "", name)
78
+ if (name == "決策" || name == "待解問題" || name == "失敗嘗試") {
79
+ if (printing) print ""
80
+ print "### " name
81
+ printing = 1
82
+ grab = 1
83
+ next
84
+ }
85
+ grab = 0
86
+ next
87
+ }
88
+ in_sh && grab { print }
89
+ '
90
+ }
91
+
92
+ # Atomic write of extracted body to $1 from Round blob $2.
93
+ handoff_write() {
94
+ local path="$1" blob="$2" dir tmp body
95
+ body="$(handoff_extract_file_body "$blob")" || return 1
96
+ dir="$(dirname "$path")"
97
+ mkdir -p "$dir" 2>/dev/null || true
98
+ tmp="$(mktemp "$dir/.handoff.XXXXXX")" || return 1
99
+ printf '%s\n' "$body" > "$tmp" || { rm -f "$tmp"; return 1; }
100
+ mv "$tmp" "$path" || { rm -f "$tmp"; return 1; }
101
+ return 0
102
+ }
103
+
104
+ handoff_clear() {
105
+ rm -f "$1"
106
+ }
@@ -33,7 +33,7 @@ NAME="$(slugify "$NAME")"
33
33
  || { echo "檔名無效" >&2; exit 1; }
34
34
  [ "${#NAME}" -le 64 ] || { echo "檔名超過 64 字元" >&2; exit 1; }
35
35
 
36
- devlog_resolve_paths "${CLAUDE_PROJECT_DIR:-.}"
36
+ devlog_resolve_paths "${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}"
37
37
  devlog_lock_acquire
38
38
  trap 'devlog_lock_release' EXIT
39
39
  MAIN="$DEVLOG_FILE"
@@ -13,7 +13,7 @@ SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
13
13
  # shellcheck source=devlog-path.sh
14
14
  . "$SCRIPT_DIR/devlog-path.sh"
15
15
 
16
- PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
16
+ PROJECT_DIR="${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}"
17
17
  devlog_resolve_paths "$PROJECT_DIR"
18
18
  MAIN="$DEVLOG_FILE"
19
19
 
@@ -0,0 +1,51 @@
1
+ # shellcheck shell=bash
2
+ # Sourced by round-start.sh / lessons-on.sh / lessons-drift-set.sh. Owns the shared
3
+ # `.lessons-advisory-state` file (docs/design/lessons-mode.md 「機制性訊號:
4
+ # 共用計數器」): a `count`/`threshold` pair fed by more than one mechanical
5
+ # signal (workspace drift, accumulated BLOCKED rounds), so the file and
6
+ # function names are signal-agnostic rather than "drift"-specific.
7
+ _lessons_advisory_state_src="${BASH_SOURCE[0]}"
8
+ _LESSONS_ADVISORY_STATE_DIR="$(cd "${_lessons_advisory_state_src%/*}" && pwd)"
9
+ # shellcheck source=json-field.sh
10
+ . "$_LESSONS_ADVISORY_STATE_DIR/json-field.sh"
11
+
12
+ # One-time migration from the old, drift-only file name/fields. No-op unless
13
+ # the old file exists and the new one doesn't; safe to call on every
14
+ # invocation (idempotent, cheap after the first migration).
15
+ lessons_advisory_migrate() {
16
+ local devlog_dir="$1"
17
+ local old="$devlog_dir/.lessons-drift-state"
18
+ local new="$devlog_dir/.lessons-advisory-state"
19
+ [ -f "$new" ] && return 0
20
+ [ -f "$old" ] || return 0
21
+ local count threshold
22
+ count="$(json_int_get "$old" mismatch_count)"
23
+ threshold="$(json_int_get "$old" threshold)"
24
+ case "$count" in ''|*[!0-9]*) count=0 ;; esac
25
+ case "$threshold" in ''|*[!0-9]*) threshold=3 ;; esac
26
+ printf '{"count": %s, "threshold": %s}\n' "$count" "$threshold" > "$new" 2>/dev/null || return 0
27
+ rm -f "$old" 2>/dev/null || true
28
+ }
29
+
30
+ # Increments $1 (the advisory-state file, created with defaults if missing),
31
+ # printing the shared advisory line and resetting to 0 once the threshold is
32
+ # reached. Callers call this once per mechanical signal occurrence; a
33
+ # printed line means "surface this to Claude" (round-start.sh's stdout is
34
+ # the only place a non-blocking hook message reaches Claude's context).
35
+ lessons_advisory_bump() {
36
+ local file="$1" count max newcount
37
+ if [ ! -f "$file" ]; then
38
+ printf '%s\n' '{"count": 0, "threshold": 3}' > "$file" 2>/dev/null || true
39
+ fi
40
+ count="$(json_int_get "$file" count)"
41
+ max="$(json_int_get "$file" threshold)"
42
+ case "$count" in ''|*[!0-9]*) count=0 ;; esac
43
+ case "$max" in ''|*[!0-9]*) max=3 ;; esac
44
+ newcount=$((count + 1))
45
+ if [ "$newcount" -ge "$max" ]; then
46
+ json_int_set "$file" count 0
47
+ printf '\n[Lessons Mode 提示] 流程訊號已累積出現 %s 次(門檻 %s)。可考慮用 lessons-append.sh 記一筆流程教訓,非強制。\n' "$newcount" "$max"
48
+ else
49
+ json_int_set "$file" count "$newcount"
50
+ fi
51
+ }
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # Claude-invoked only (docs/design/lessons-mode.md), never wired into
3
- # hooks/hooks.json — same posture as keep-move.sh. Appends one free-prose
3
+ # claude/hooks.json — same posture as keep-move.sh. Appends one free-prose
4
4
  # entry to .devlog/devlog.lessons.<topic>.md (creating it if new), then
5
5
  # rebuilds the trailing "## Lessons 索引" block in devlog.md.
6
6
  set -uo pipefail
@@ -26,7 +26,7 @@ while [ "$#" -gt 0 ]; do
26
26
  esac
27
27
  done
28
28
 
29
- PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
29
+ PROJECT_DIR="${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}"
30
30
  devlog_resolve_paths "$PROJECT_DIR"
31
31
  MAIN="$DEVLOG_FILE"
32
32
 
@@ -52,8 +52,10 @@ trap 'devlog_lock_release' EXIT
52
52
  TARGET="$DEVLOG_DIR/devlog.lessons.$TOPIC.md"
53
53
  TS="$(date -Iseconds 2>/dev/null || date '+%Y-%m-%dT%H:%M:%S%z')"
54
54
 
55
+ IS_NEW_TOPIC=0
55
56
  if [ ! -f "$TARGET" ]; then
56
57
  printf '# Lessons: %s\n\n- source: `.devlog/devlog.md`\n' "$TOPIC" > "$TARGET" || exit 1
58
+ IS_NEW_TOPIC=1
57
59
  fi
58
60
  {
59
61
  printf '\n## %s\n' "$TS"
@@ -66,6 +68,7 @@ fi
66
68
  # heading in that file, truncated at the first 。/. (whichever comes
67
69
  # first); no truncation if neither appears.
68
70
  INDEX_LINES=""
71
+ OTHER_TOPICS=""
69
72
  shopt -s nullglob
70
73
  for f in "$DEVLOG_DIR"/devlog.lessons.*.md; do
71
74
  [ -f "$f" ] || continue
@@ -73,6 +76,13 @@ for f in "$DEVLOG_DIR"/devlog.lessons.*.md; do
73
76
  n="$(grep -c '^## ' "$f" 2>/dev/null || echo 0)"
74
77
  case "$n" in ''|*[!0-9]*) n=0 ;; esac
75
78
  [ "$n" -gt 0 ] || continue
79
+ if [ "$leaf" = "devlog.lessons.$TOPIC.md" ]; then
80
+ THIS_TOPIC_COUNT="$n"
81
+ else
82
+ OTHER_TOPIC_NAME="${leaf#devlog.lessons.}"
83
+ OTHER_TOPIC_NAME="${OTHER_TOPIC_NAME%.md}"
84
+ OTHER_TOPICS="${OTHER_TOPICS:+$OTHER_TOPICS, }${OTHER_TOPIC_NAME}"
85
+ fi
76
86
  last_ln="$(grep -n '^## ' "$f" | tail -1 | cut -d: -f1)"
77
87
  updated_at="$(sed -n "${last_ln}p" "$f" | sed -E 's/^## //')"
78
88
  title="$(awk -v start="$last_ln" 'NR>start && $0 ~ /[^[:space:]]/ {print; exit}' "$f")"
@@ -91,3 +101,9 @@ devlog_strip_lessons_index "$MAIN" "$STRIPPED"
91
101
  } > "$STRIPPED.new" && mv "$STRIPPED.new" "$MAIN" || exit 1
92
102
 
93
103
  printf 'PATH=.devlog/devlog.lessons.%s.md\n' "$TOPIC"
104
+ if [ -n "${THIS_TOPIC_COUNT:-}" ] && [ $((THIS_TOPIC_COUNT % 3)) -eq 0 ]; then
105
+ printf '[Lessons Mode 提示] 這個主題已經累積 %s 則。可考慮升級成 docs/design/*.md 的正式決策,不強制。\n' "$THIS_TOPIC_COUNT"
106
+ fi
107
+ if [ "$IS_NEW_TOPIC" -eq 1 ] && [ -n "${OTHER_TOPICS:-}" ]; then
108
+ printf 'NEW_TOPIC。既有主題:%s(如果內容其實屬於這些主題之一,改用 --topic 該名稱重跑,避免同一件事分裂成兩個檔案)。\n' "$OTHER_TOPICS"
109
+ fi