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.
- package/README.md +131 -151
- package/README.zh-TW.md +235 -0
- package/claude/hooks.json +75 -0
- package/cli/agents-md.js +15 -8
- package/cli/agents-md.test.js +21 -1
- package/cli/init.js +6 -4
- package/cli/init.test.js +5 -4
- package/cli/merge-hooks.js +5 -2
- package/cli/merge-hooks.test.js +52 -0
- package/cli/platforms/claude.js +55 -0
- package/cli/platforms/claude.test.js +178 -0
- package/cli/platforms/codex.js +6 -18
- package/cli/skills-from-commands.js +32 -0
- package/cli/skills-from-commands.test.js +51 -0
- package/cli/vendor.js +8 -1
- package/cli/vendor.test.js +24 -5
- package/codex/hooks/on-pre-tool.sh +3 -3
- package/codex/hooks/on-session-end.sh +2 -2
- package/codex/hooks/on-session-start.sh +2 -2
- package/codex/hooks/on-stop.sh +3 -3
- package/codex/hooks/on-user-prompt-submit.sh +2 -2
- package/codex/hooks/test-adapters.sh +2 -2
- package/commands/checkpoint.md +2 -2
- package/commands/clean.md +3 -3
- package/commands/compact.md +3 -3
- package/commands/continue.md +4 -4
- package/commands/keep.md +21 -5
- package/commands/lessons-drift.md +4 -4
- package/commands/lessons-off.md +3 -3
- package/commands/lessons-on.md +3 -3
- package/commands/lessons.md +3 -3
- package/commands/overview.md +3 -3
- package/commands/pause.md +3 -3
- package/commands/resume.md +3 -3
- package/commands/search.md +22 -0
- package/commands/segment-watch.md +3 -3
- package/commands/span.md +2 -2
- package/commands/start.md +3 -3
- package/commands/status.md +5 -4
- package/{hooks → core}/scripts/await-open.sh +1 -1
- package/{hooks → core}/scripts/checkpoint-set.sh +1 -1
- package/{hooks → core}/scripts/clean-devlog.sh +2 -1
- package/{hooks → core}/scripts/close-open-round.sh +1 -1
- package/{hooks → core}/scripts/compact-devlog.sh +1 -1
- package/{hooks → core}/scripts/devlog-lock.sh +17 -1
- package/{hooks → core}/scripts/devlog-path.sh +15 -8
- package/{hooks → core}/scripts/enforce-devlog.sh +26 -1
- package/core/scripts/handoff-file.sh +106 -0
- package/{hooks → core}/scripts/keep-move.sh +1 -1
- package/{hooks → core}/scripts/kept-list.sh +1 -1
- package/core/scripts/lessons-advisory-state.sh +51 -0
- package/{hooks → core}/scripts/lessons-append.sh +18 -2
- package/core/scripts/lessons-drift-set.sh +46 -0
- package/{hooks → core}/scripts/lessons-off.sh +1 -1
- package/{hooks → core}/scripts/lessons-on.sh +9 -4
- package/{hooks → core}/scripts/lessons-read.sh +1 -1
- package/{hooks → core}/scripts/on-tool-failure.sh +1 -1
- package/{hooks → core}/scripts/pause-devlog.sh +1 -1
- package/core/scripts/project-dir.sh +19 -0
- package/{hooks → core}/scripts/resume-devlog.sh +1 -1
- package/{hooks → core}/scripts/round-start.sh +52 -17
- package/{hooks → core}/scripts/run-tests.sh +1 -1
- package/core/scripts/search-devlog.sh +63 -0
- package/{hooks → core}/scripts/segment-watch-set.sh +1 -1
- package/{hooks → core}/scripts/segment-watch.sh +1 -1
- package/{hooks → core}/scripts/session-start-devlog.sh +8 -1
- package/{hooks → core}/scripts/span-close.sh +1 -1
- package/{hooks → core}/scripts/span-open.sh +1 -1
- package/{hooks → core}/scripts/start-devlog.sh +1 -1
- package/{hooks → core}/scripts/status-devlog.sh +5 -5
- package/{hooks → core}/scripts/tests/test-clean-devlog.sh +12 -0
- package/core/scripts/tests/test-cli-init-e2e.sh +68 -0
- package/core/scripts/tests/test-devlog-lock.sh +76 -0
- package/{hooks → core}/scripts/tests/test-devlog-path.sh +44 -0
- package/{hooks → core}/scripts/tests/test-enforce-devlog-files.sh +11 -0
- package/core/scripts/tests/test-enforce-devlog-session-handoff.sh +178 -0
- package/{hooks → core}/scripts/tests/test-enforce-devlog-workspace.sh +35 -0
- package/{hooks → core}/scripts/tests/test-enforce-devlog.sh +174 -0
- package/core/scripts/tests/test-handoff-file.sh +98 -0
- package/core/scripts/tests/test-lessons-advisory-state.sh +55 -0
- package/{hooks → core}/scripts/tests/test-lessons-append.sh +18 -0
- package/{hooks → core}/scripts/tests/test-lessons-drift-set.sh +14 -5
- package/{hooks → core}/scripts/tests/test-lessons-on-off.sh +16 -6
- package/core/scripts/tests/test-project-dir.sh +30 -0
- package/{hooks → core}/scripts/tests/test-round-start.sh +247 -13
- package/core/scripts/tests/test-search-devlog.sh +126 -0
- package/{hooks → core}/scripts/tests/test-session-start-devlog.sh +61 -2
- package/{hooks → core}/scripts/tests/test-status-span.sh +3 -3
- package/{hooks → core}/scripts/workspace-snapshot.sh +2 -2
- package/cursor/hooks/on-pre-tool.sh +2 -2
- package/cursor/hooks/on-session-end.sh +2 -2
- package/cursor/hooks/on-session-start.sh +2 -2
- package/cursor/hooks/on-stop.sh +2 -2
- package/cursor/hooks/on-submit-prompt.sh +2 -2
- package/cursor/hooks/on-tool-failure.sh +2 -2
- package/package.json +5 -4
- package/skills/devlog-tracker/SKILL.md +90 -22
- package/skills/devlog-tracker/references/checkpoint-mode.md +21 -6
- package/skills/devlog-tracker/references/contract.md +83 -0
- package/skills/devlog-tracker/references/lessons-mode.md +33 -9
- package/skills/devlog-tracker/references/reply-fold.md +1 -1
- package/hooks/scripts/lessons-drift-set.sh +0 -42
- package/hooks/scripts/tests/test-cli-init-e2e.sh +0 -38
- package/hooks/scripts/tests/test-devlog-lock.sh +0 -37
- /package/{hooks → core}/scripts/detect-pending-question.sh +0 -0
- /package/{hooks → core}/scripts/devlog-md.sh +0 -0
- /package/{hooks → core}/scripts/files-snapshot.sh +0 -0
- /package/{hooks → core}/scripts/json-field.sh +0 -0
- /package/{hooks → core}/scripts/on-session-end.sh +0 -0
- /package/{hooks → core}/scripts/on-stop-failure.sh +0 -0
- /package/{hooks → core}/scripts/redact-prompt.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-await-open.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-branch-scoped-integration.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-checkpoint-set.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-close-open-round.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-compact-devlog.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-devlog-md.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-enforce-devlog-handoff-order.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-files-snapshot.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-json-field.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-keep-move.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-kept-list.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-lessons-read.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-on-interrupt.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-redact-prompt.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-resume-devlog.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-segment-watch-set.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-segment-watch.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-start-pause-devlog.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-workspace-snapshot.sh +0 -0
package/commands/lessons-off.md
CHANGED
|
@@ -4,10 +4,10 @@ description: 關閉 Lessons Mode。不會刪除任何已寫的 devlog.lessons.*.
|
|
|
4
4
|
|
|
5
5
|
請執行:
|
|
6
6
|
|
|
7
|
-
1. 先決定 plugin 根目錄(有 `
|
|
7
|
+
1. 先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
|
|
8
8
|
```bash
|
|
9
|
-
PLUGIN_ROOT="${
|
|
10
|
-
|
|
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 本來就沒開,結束。
|
package/commands/lessons-on.md
CHANGED
|
@@ -4,10 +4,10 @@ description: 開啟 Lessons Mode(開發歷程教訓,預設關閉)。隸屬
|
|
|
4
4
|
|
|
5
5
|
請執行:
|
|
6
6
|
|
|
7
|
-
1. 先決定 plugin 根目錄(有 `
|
|
7
|
+
1. 先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
|
|
8
8
|
```bash
|
|
9
|
-
PLUGIN_ROOT="${
|
|
10
|
-
|
|
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` 再開這個。
|
package/commands/lessons.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
description: 查看 Lessons 索引,或讀某個主題的完整教訓紀錄(純讀取,不核對工作區、不等確認)。
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
取得使用者是否有給 `<topic>`(可能沒有,代表只看索引)。記下你目前已經確認的專案根目錄絕對路徑(後面步驟都要用這個值,不要用 `$(pwd)` 重新推——理由同 `commands/continue.md` 步驟 1)。先決定 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="${
|
|
9
|
-
|
|
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>`:
|
package/commands/overview.md
CHANGED
|
@@ -8,11 +8,11 @@ description: 把已 keep 的具名檔(devlog.<name>.md)整合成一份跨主
|
|
|
8
8
|
|
|
9
9
|
## 1. 取得已 keep 的檔案清單
|
|
10
10
|
|
|
11
|
-
記下你目前已經確認的專案根目錄絕對路徑(後面步驟都要用這個值,不要用 `$(pwd)` 重新推——理由同 `commands/continue.md` 步驟 1)。先決定 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="${
|
|
15
|
-
|
|
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 根目錄(有 `
|
|
9
|
+
先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
PLUGIN_ROOT="${
|
|
13
|
-
|
|
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`。
|
package/commands/resume.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
description: 讀取具名保存的 devlog,核對最後一輪 Handoff 工作區後再接續該段工作。
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
取得使用者提供的 `<name>`;沒有名稱時先詢問。記下你目前已經確認的專案根目錄絕對路徑(後面步驟都要用這個值,不要用 `$(pwd)` 重新推——理由同 `commands/continue.md` 步驟 1:Bash 工具的工作目錄會在對話裡持續累積前面呼叫的 `cd`,用當下的 `pwd` 可能已經不是這個專案根目錄)。先決定 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="${
|
|
9
|
-
|
|
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 根目錄(有 `
|
|
9
|
+
先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
PLUGIN_ROOT="${
|
|
13
|
-
|
|
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 根目錄(有 `
|
|
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
|
-
`
|
|
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 根目錄(有 `
|
|
7
|
+
1. 先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
|
|
8
8
|
```bash
|
|
9
|
-
PLUGIN_ROOT="${
|
|
10
|
-
|
|
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。
|
package/commands/status.md
CHANGED
|
@@ -4,11 +4,12 @@ description: 查看這個專案 devlog 強制記錄是否開著、span / checkpo
|
|
|
4
4
|
|
|
5
5
|
跑:
|
|
6
6
|
```bash
|
|
7
|
-
先決定 plugin 根目錄(有 `
|
|
7
|
+
先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
PLUGIN_ROOT="${
|
|
11
|
-
|
|
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 開關狀態;`
|
|
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
|
|
18
|
-
# $1 (defaults to "."). main/master (any case) and anything
|
|
19
|
-
# git repo or has no resolvable branch keep the shared
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
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
|
-
#
|
|
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
|