devlog-tracker 0.31.1 → 0.33.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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  *English | [繁體中文](README.zh-TW.md)*
4
4
 
5
- **Version** 0.31.1
5
+ **Version** 0.33.0
6
6
 
7
7
  Maintains a `.devlog/devlog.md` in your project, turning each conversation round's requests, decisions, and outcomes into a permanent record. A conversation disappears the moment you `/clear` or switch sessions; this file fills that gap so work can pause and resume. Nothing is touched until you explicitly run `/devlog-tracker:start` — installing the plugin alone doesn't create or modify any files.
8
8
 
@@ -90,7 +90,7 @@ bash "$DEVLOG_TRACKER_ROOT/core/scripts/segment-watch-set.sh" 600
90
90
  bash "$DEVLOG_TRACKER_ROOT/core/scripts/checkpoint-set.sh" 20
91
91
  bash "$DEVLOG_TRACKER_ROOT/core/scripts/report-devlog.sh" --json
92
92
  bash "$DEVLOG_TRACKER_ROOT/core/scripts/timeline-devlog.sh"
93
- # pause / span-open / span-close / compact / keep-move / clean / resume / pr / promote: see commands/*.md
93
+ # pause / span-open / span-close / compact / keep-move / keep-all / clean / resume / pr / promote: see commands/*.md
94
94
  ```
95
95
 
96
96
  ## Quick start
@@ -115,7 +115,8 @@ The table below uses the plugin's `/devlog-tracker:*` namespace; `npx init --cla
115
115
  | `/devlog-tracker:continue` | Reads `.devlog/devlog.md`, checks the last round's Handoff "Workspace" section, then continues per its next step. Use this after `/clear` to resume. See [`docs/design/continue.md`](docs/design/continue.md). |
116
116
  | `/devlog-tracker:pause` | Pauses enforced recording; history files are untouched, and you can `start` again later. |
117
117
  | `/devlog-tracker:compact` | A script moves older `DONE` rounds into `devlog.archive.md` (Checkpoints and unfinished rounds stay in the main file). |
118
- | `/devlog-tracker:keep` | Scans the whole file, groups it by topic, lists suggestions at once, then — after confirmation — moves each section out into its own `devlog.<name>.md` (leaving a `## Kept index` pointer line with a one-sentence topic description in the main file); can also extract a single section or merge everything into one history file. Not the same as compact. See [`docs/design/keep.md`](docs/design/keep.md). |
118
+ | `/devlog-tracker:keep` | Scans the current branch's main file (to reorganize every devlog at once, use `keep-all`), groups it by topic, lists suggestions at once, then — after confirmation — moves each section out into its own `devlog.<name>.md` (leaving a `## Kept index` pointer line with a one-sentence topic description in the main file); can also extract a single section or merge everything into one history file. Not the same as compact. See [`docs/design/keep.md`](docs/design/keep.md). |
119
+ | `/devlog-tracker:keep-all` | Reorganizes **every** devlog, not just the current branch's: the current main file, every other branch's `devlog.<branch>.md`, `devlog.archive.md`, and all files earlier `keep`/`keep-all` runs produced. Claude regroups their Rounds by topic across files (one topic spread over main, a feature branch and the archive ends up in one file), lists every proposed file at once, and after confirmation one script run writes them all or nothing. Existing kept files are broken up and rebuilt; each branch's unfinished tail (Rounds after its last `DONE`) and the open Round never move; branch files are never deleted. Every touched file is backed up to `.devlog/.keep-all-backup/<timestamp>/` first. Needs Node ≥18. See [`docs/design/keep-all.md`](docs/design/keep-all.md). |
119
120
  | `/devlog-tracker:overview` | Reads all kept `devlog.<name>.md` files and merges them into a cross-topic overview, plus a list of candidate rules that look like they belong in `CLAUDE.md`. Read-only — no workspace check, no confirmation, no writes. See the Kept index section of [`docs/design/keep.md`](docs/design/keep.md). |
120
121
  | `/devlog-tracker:promote` | Picks rule candidates from kept files, lessons files and Checkpoint `### 決策` sections, lists them numbered, and — only for the ones you choose — appends them to a managed `<!-- devlog-tracker:rules:begin/end -->` block in `CLAUDE.md` (or `AGENTS.md` when `CLAUDE.md` is just `@AGENTS.md`, or on Codex-only projects). Append-only, exact duplicates skipped; `init` never rewrites this block. |
121
122
  | `/devlog-tracker:search <keyword>` | Case-insensitive string search across `devlog.md`/`devlog.archive.md`/kept `devlog.<name>.md`/`devlog.lessons.<topic>.md`; Claude answers in its own words from the hits (with file/heading/line as evidence). Read-only — no workspace check, no confirmation, no writes. |
@@ -233,7 +234,7 @@ When Claude ends a turn with a plain-text question and the next message is the a
233
234
 
234
235
  #### Per-branch devlog files
235
236
 
236
- Switching branches within the same working directory automatically splits the main file by the checked-out branch: `main`/`master` keeps using `.devlog/devlog.md`, while other branches each use `.devlog/devlog.<branch>.md` (slashes converted to `-`). A separate `git worktree` (a different directory) already has its own independent `.devlog/` and is unaffected by this mechanism. The first time a branch is detected without its own file while `devlog.md` already has content, it gets renamed (not copied) into that branch's file. See [`docs/design/branch-scoped-devlog.md`](docs/design/branch-scoped-devlog.md).
237
+ Switching branches within the same working directory automatically splits the main file by the checked-out branch: `main`/`master` keeps using `.devlog/devlog.md`, while other branches each use `.devlog/devlog.<branch>.md` (slashes converted to `-`). A separate `git worktree` (a different directory) already has its own independent `.devlog/` and is unaffected by this mechanism. The first time a branch is detected without its own file, only the unfinished tail of `devlog.md` (the Rounds after the last `DONE`, plus `handoff.md`) is cut into that branch's file; `main`'s own history, project summary, Checkpoints, and Kept/Lessons indexes stay in `devlog.md`. If the last Round is already `DONE`, or the branch checked out doesn't contain the current `main` tip (an older branch), nothing moves and the branch starts with an empty file. A branch file's first line is an origin marker, `<!-- devlog-origin: branch=<raw branch name> -->` (invisible when rendered), recording the real branch name that the sanitized filename can't carry; `/devlog-tracker:keep-all` uses it to tell branch files from kept files and to report whether the branch is still active, merged or deleted. Files created before this marker existed are left as they are. See [`docs/design/branch-scoped-devlog.md`](docs/design/branch-scoped-devlog.md).
237
238
 
238
239
  #### Lessons Mode (off by default, not automatic)
239
240
 
package/README.zh-TW.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  *[English](README.md) | 繁體中文*
4
4
 
5
- **版本** 0.31.1
5
+ **版本** 0.33.0
6
6
 
7
7
  在專案中維護一份 `.devlog/devlog.md`,把每一輪對話的請求、決策與結果寫成永久紀錄。對話一 `/clear` 或換 session 就沒了;這份檔案取代那個缺口,讓工作可以中斷再接。沒下過 `/devlog-tracker:start` 時,裝著也不會動任何檔案。
8
8
 
@@ -90,7 +90,7 @@ bash "$DEVLOG_TRACKER_ROOT/core/scripts/segment-watch-set.sh" 600
90
90
  bash "$DEVLOG_TRACKER_ROOT/core/scripts/checkpoint-set.sh" 20
91
91
  bash "$DEVLOG_TRACKER_ROOT/core/scripts/report-devlog.sh" --json
92
92
  bash "$DEVLOG_TRACKER_ROOT/core/scripts/timeline-devlog.sh"
93
- # pause / span-open / span-close / compact / keep-move / clean / resume / pr / promote:見 commands/*.md
93
+ # pause / span-open / span-close / compact / keep-move / keep-all / clean / resume / pr / promote:見 commands/*.md
94
94
  ```
95
95
 
96
96
  ## 快速開始
@@ -115,7 +115,8 @@ $devlog-start # npx init --codex
115
115
  | `/devlog-tracker:continue` | 讀 `.devlog/devlog.md`,核對最後一輪 Handoff「工作區」後再依下一步接著做。`/clear` 之後要接續用這個。細節見 [`docs/design/continue.md`](docs/design/continue.md)。 |
116
116
  | `/devlog-tracker:pause` | 暫停強制記錄,歷史檔不動,之後可再 `start`。 |
117
117
  | `/devlog-tracker:compact` | 腳本把較舊的 `DONE` 輪次搬到 `devlog.archive.md`(Checkpoint 與未完成輪留在主檔)。 |
118
- | `/devlog-tracker:keep` | 掃全檔分主題,一次列出建議,確認後把各段各自搬走成 `devlog.<name>.md`(並在主檔留一個 `## Kept 索引` 指標行,含一句主題描述);也可抽出一段或合併成全部歷史一檔。不是 compact。細節見 [`docs/design/keep.md`](docs/design/keep.md)。 |
118
+ | `/devlog-tracker:keep` | 掃目前分支的主檔分主題(要一次整理所有 devlog 用 `keep-all`),一次列出建議,確認後把各段各自搬走成 `devlog.<name>.md`(並在主檔留一個 `## Kept 索引` 指標行,含一句主題描述);也可抽出一段或合併成全部歷史一檔。不是 compact。細節見 [`docs/design/keep.md`](docs/design/keep.md)。 |
119
+ | `/devlog-tracker:keep-all` | 整理**所有** devlog,不只目前分支:目前的主檔、其他分支的 `devlog.<branch>.md`、`devlog.archive.md`,以及先前 `keep`/`keep-all` 產生的所有具名檔。Claude 跨檔依主題重新分段(同一個主題散在 main、feature 分支與 archive 的片段會合回同一檔),一次列出所有建議檔案,確認後由一支腳本整批寫入,全有或全無。既有的具名檔會被拆開重組;各分支未完成的尾巴(最後一個 `DONE` 之後的 Round)與開著的那一輪不會被搬,分支檔也不會被刪。動手前會先把所有會動到的檔備份到 `.devlog/.keep-all-backup/<時間戳>/`。需要 Node ≥18。細節見 [`docs/design/keep-all.md`](docs/design/keep-all.md)。 |
119
120
  | `/devlog-tracker:overview` | 讀完所有已 keep 的 `devlog.<name>.md`,整合成跨主題總覽,並列出看起來該進 `CLAUDE.md` 的規範候選。純讀取,不核對工作區、不等確認、不寫檔。細節見 [`docs/design/keep.md`](docs/design/keep.md) Kept index。 |
120
121
  | `/devlog-tracker:promote` | 從已 keep 的檔、lessons 檔與 Checkpoint 的 `### 決策` 挑出規範候選並編號列出;只有你選定的才追加到 `CLAUDE.md`(`CLAUDE.md` 只有 `@AGENTS.md` 或只裝 Codex 時改寫 `AGENTS.md`)的 `<!-- devlog-tracker:rules:begin/end -->` 受管區塊。只追加、一字不差的重複會跳過;`init` 不會覆寫這個區塊。 |
121
122
  | `/devlog-tracker:search <關鍵字>` | 在 `devlog.md`/`devlog.archive.md`/已 keep 的 `devlog.<name>.md`/`devlog.lessons.<topic>.md` 裡做不分大小寫的字串搜尋;Claude 讀完命中後用自己的話回答(必要時附檔名/標題/行號)。純讀取,不核對工作區、不等確認、不寫檔。 |
@@ -233,7 +234,7 @@ Claude 用純文字結尾提出問題、下一則訊息才拿到答案時,不
233
234
 
234
235
  #### 分支各自的 devlog 檔
235
236
 
236
- 同一個工作目錄裡切換分支時,主檔會依目前 checkout 的分支自動分開:`main`/`master` 繼續用 `.devlog/devlog.md`,其他分支各自用 `.devlog/devlog.<branch>.md`(斜線轉成 `-`)。另開一個 `git worktree`(不同目錄)本來就有自己獨立的 `.devlog/`,不受這個機制影響。第一次在某分支偵測到還沒有專屬檔案、且 `devlog.md` 已有內容時,會把它改名(非複製)成該分支的檔案。細節見 [`docs/design/branch-scoped-devlog.md`](docs/design/branch-scoped-devlog.md)。
237
+ 同一個工作目錄裡切換分支時,主檔會依目前 checkout 的分支自動分開:`main`/`master` 繼續用 `.devlog/devlog.md`,其他分支各自用 `.devlog/devlog.<branch>.md`(斜線轉成 `-`)。另開一個 `git worktree`(不同目錄)本來就有自己獨立的 `.devlog/`,不受這個機制影響。第一次在某分支偵測到還沒有專屬檔案時,只會把 `devlog.md` 裡還沒完成的尾巴(最後一個 `DONE` 之後的 Round,連同 `handoff.md`)剪到該分支的檔案;`main` 自己的歷史、專案摘要、Checkpoint、Kept/Lessons 索引都留在 `devlog.md`。最後一輪已經是 `DONE`,或切到的是不含目前 `main` 最新 commit 的舊分支時,什麼都不搬,新分支從空檔開始。分支檔的第一行是 origin 標記 `<!-- devlog-origin: branch=<原始分支名> -->`(渲染時看不到),記下檔名轉換後已經還原不回來的真實分支名;`/devlog-tracker:keep-all` 靠它分辨分支檔與具名檔,並回報該分支還在開發、已合併或已刪除。這個標記出現前就存在的舊檔維持原樣。細節見 [`docs/design/branch-scoped-devlog.md`](docs/design/branch-scoped-devlog.md)。
237
238
 
238
239
  #### Lessons Mode(預設關閉,不自動)
239
240
 
package/cli/agents-md.js CHANGED
@@ -23,6 +23,7 @@ function codexBlock() {
23
23
  | 歸檔 / compact | \`commands/compact.md\` |
24
24
  | 清空重編 / clean(不可復原,先問使用者確認) | \`commands/clean.md\` |
25
25
  | 保存主題 / keep、接續具名檔 / resume、跨主題總覽 / overview | \`commands/keep.md\`、\`commands/resume.md\`、\`commands/overview.md\` |
26
+ | 整理所有 devlog(跨分支檔、archive、既有 keep 檔重新分主題) / keep-all | \`commands/keep-all.md\` |
26
27
  | 沉澱規範寫進 CLAUDE.md/AGENTS.md / promote | \`commands/promote.md\` |
27
28
  | 搜尋 / search | \`commands/search.md\` |
28
29
  | 統計 / report、HTML 時間軸 / timeline | \`commands/report.md\`、\`commands/timeline.md\` |
@@ -22,6 +22,7 @@ function claudeBlock() {
22
22
  | 歸檔 / compact | \`commands/compact.md\` |
23
23
  | 清空重編 / clean(不可復原,先問使用者確認) | \`commands/clean.md\` |
24
24
  | 保存主題 / keep、接續具名檔 / resume、跨主題總覽 / overview | \`commands/keep.md\`、\`commands/resume.md\`、\`commands/overview.md\` |
25
+ | 整理所有 devlog(跨分支檔、archive、既有 keep 檔重新分主題) / keep-all | \`commands/keep-all.md\` |
25
26
  | 沉澱規範寫進 CLAUDE.md/AGENTS.md / promote | \`commands/promote.md\` |
26
27
  | 統計 / report、HTML 時間軸 / timeline | \`commands/report.md\`、\`commands/timeline.md\` |
27
28
  | 產生 PR 描述 / pr | \`commands/pr.md\` |
@@ -0,0 +1,93 @@
1
+ ---
2
+ description: 整理所有 devlog:跨主檔、所有分支檔、archive 與既有 keep 檔,依主題重新分成 devlog.<name>.md
3
+ ---
4
+
5
+ 請執行 devlog keep-all(整理全部 devlog)。這是使用者主動執行 `/devlog-tracker:keep-all` 時才做的事,不要自動觸發。
6
+
7
+ 跟 `/devlog-tracker:keep` 的差別:keep 只整理目前分支的主檔;keep-all 一次看 `.devlog/` 裡**所有** devlog 檔——目前分支的主檔、其他分支的 `devlog.<branch>.md`、`devlog.archive.md`,以及先前 keep/keep-all 產生的具名檔——跨檔依主題重新分段。既有的具名檔會被拆開重組,所以一個主題散在 main、feature 分支與 archive 的片段可以合回同一個檔。設計見 `docs/design/keep-all.md`。
8
+
9
+ 確認之前不要寫任何檔。
10
+
11
+ 先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄),並記下目前已確認的專案根目錄絕對路徑(後面兩次呼叫都用同一個值,不要用 `$(pwd)` 重新推)。
12
+
13
+ ## 1. 掃描
14
+
15
+ ```bash
16
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
17
+ DEVLOG_PROJECT_DIR="<專案根目錄絕對路徑>" bash "${PLUGIN_ROOT}/core/scripts/keep-all.sh" --scan
18
+ ```
19
+
20
+ - `NO_NODE`:告知 keep-all 需要 Node.js(可改用 `/devlog-tracker:keep` 整理目前分支),結束。
21
+ - `NOTHING`:告知目前沒有可整理的 Round,不要建立任何檔,結束。
22
+ - 否則輸出是:
23
+ - `FINGERPRINT=<fp> COUNT=<n>`:記下這兩個值,步驟 4 原樣帶回。
24
+ - `SOURCE file=… kind=…`:每個來源檔。`kind` 由腳本判定,不要自己依檔名猜:
25
+ - `current`:目前分支的主檔
26
+ - `branch`:其他分支的主檔;`origin` 是分支名,`state` 是 `active`(還在開發)/`merged`(已合併進 main)/`gone`(本地分支已刪)/`unknown`/`n/a`
27
+ - `archive`:`devlog.archive.md`
28
+ - `kept`:既有的具名檔(第一行是 `# Kept log`)
29
+ - `ROUND id=… file=… round=… line=… ts=… status=… movable=0|1`:每個 Round 一行,`id` 是依時間排序的全域編號(不同檔的 Round 編號會重複,一律用 `id` 指稱)。`movable=0` 的是目前開著的 Round,或其他分支最後一個 `DONE` 之後還沒完成的尾巴——這些不能搬,也不要列進任何段落。
30
+
31
+ ## 2. 讀檔、分主題
32
+
33
+ 讀完每個 `SOURCE` 檔(至少讀 `movable=1` 的 Round 所在範圍)。依 `### User Input` 與 `### Summary` 的主題,把 `movable=1` 的 Round 分成主題段落:
34
+
35
+ - 一段是一組 `id`,**可以跨檔、可以不連續**——同一主題在 main、分支與 archive 的片段應該放進同一段。
36
+ - `kind=kept` 來源的 Round **全部都必須**分進某一段(它們先前已被判定值得留),可以跟其他主題合併或拆開重組。
37
+ - 其他來源的瑣碎 Round(只有確認、閒聊、重複)可以不收,留在原檔。判斷標準同 `/devlog-tracker:keep` 步驟 2。
38
+ - 每段產生建議 `<name>`(小寫 ASCII kebab-case,2–4 段,只反映主題,不加日期),以及一句這段在做什麼的描述(純文字、不換行、不含 tab 與反引號;步驟 4 原樣寫進索引)。同一批兩段撞名時,較晚的加 `-2`/`-3`。既有 kept 檔的名稱若仍貼切可以沿用——它會被這次整理取代。
39
+
40
+ ## 3. 一次列出,然後停下來等
41
+
42
+ ```
43
+ 掃到 N 段主題:
44
+ 1. <一句描述> → devlog.<name>.md
45
+ <來源檔> Round <編號…>(#<id…>)、<來源檔> Round <編號…>(#<id…>,分支 <origin> <state>)
46
+ 2. ...
47
+ 會被重整並刪除的既有 keep 檔:<檔名,逗號分隔,或「無」>
48
+ 可搬但偏瑣碎、留在原檔:<來源檔與 Round,或「無」>
49
+ 不能搬、保持原樣:<各分支未完成尾巴與開著的 Round,或「無」>
50
+
51
+ 回覆:
52
+ 採用 → 全部照上面寫入
53
+ 改第 N 段檔名 <name> / 移除第 N 段 / 第 N 段併入第 M 段
54
+ 摘要第 N 段 → 搬完後把該段敘事改寫得更精簡
55
+ 取消
56
+ ```
57
+
58
+ 同一則回覆可以合併多條修改;`N` 一律對應原始編號。使用者用文字要求把某些 Round 換段,照做後重新列一次再等。然後停止,使用者回覆前不要寫任何檔。
59
+
60
+ - 取消 → 不改檔,結束。
61
+ - `移除第 N 段`:該段若含 `kind=kept` 的 Round,拒絕並說明只能 `併入`;其餘 Round 留在原檔。
62
+ - `第 N 段併入第 M 段`:合併 id,沿用第 M 段的檔名與描述。
63
+ - 檔名規則同 `/devlog-tracker:keep` 步驟 4(`devlog.` 前綴與 `.md` 後綴會剝掉;不可為空、`archive`、`lessons.*`,不可含 `/`、`\`、`..`,最多 64 字元);目標檔已存在(且不是這次會被整理掉的 kept 檔)時改建議 `-2`。
64
+
65
+ ## 4. 寫計畫並執行(整批一次,全有或全無)
66
+
67
+ 把確認後的段落寫到 `.devlog/.keep-all-plan.tsv`,一段一行,三欄以 tab 分隔:
68
+
69
+ ```
70
+ <name> <描述> <id 清單,例如 3-8,20,24-25>
71
+ ```
72
+
73
+ 然後:
74
+
75
+ ```bash
76
+ DEVLOG_PROJECT_DIR="<專案根目錄絕對路徑>" bash "${PLUGIN_ROOT}/core/scripts/keep-all.sh" \
77
+ --apply "<專案根目錄絕對路徑>/.devlog/.keep-all-plan.tsv" --fingerprint <fp> --count <n>
78
+ ```
79
+
80
+ `<fp>`/`<n>` 用步驟 1 的值。不要自己搬檔、刪檔或改索引——腳本是唯一的實作:驗證計畫、把所有會動到的檔備份到 `.devlog/.keep-all-backup/<時間戳>/`、組好新檔、改寫來源、刪除被整理掉的 kept 檔與清空的 archive、重建 `## Kept 索引`(新索引行寫在目前分支的主檔)。分支檔與目前主檔即使搬空也不會被刪。
81
+
82
+ - exit 1:原樣顯示 stderr,不要自行重試或手動補做。驗證錯誤時不會寫任何檔;fingerprint 不符代表掃描後檔案被改過,回到步驟 1 重新掃描。
83
+ - 成功後刪除 `.devlog/.keep-all-plan.tsv`。
84
+
85
+ ## 5. 選配:摘要
86
+
87
+ 只對使用者標記「摘要第 N 段」的段落做,規則同 `/devlog-tracker:keep` 步驟 5.5(只改寫 `### Summary`、`### Reply`、`#### 決策`、`#### 現況` 的敘事;標題、`### User Input`、`#### 工作區`、`#### 檔案`、`#### 完成條件`、`#### 下一步`、`### Status` 一律不動;用 Edit 不用 Write)。
88
+
89
+ ## 6. 回報
90
+
91
+ 依 stdout 回報:每個 `KEPT=<路徑> ROUNDS=<n>`、每個 `DELETED=<路徑>`、`BACKUP=<目錄>`(出錯時可從這裡還原),以及哪幾段套用了摘要。
92
+
93
+ 若 `.devlog/.round-open` 存在:在開著的那一輪補上 `### Summary` / `### Handoff` / `### Status` 再結束(Stop hook 仍會檢查)。
package/commands/keep.md CHANGED
@@ -4,6 +4,8 @@ description: 掃描整份 devlog.md,把值得留名的主題段落一次分別
4
4
 
5
5
  請執行 devlog keep(具名搬走)。這是使用者主動執行 `/devlog-tracker:keep` 時才做的事,不要自動觸發。
6
6
 
7
+ 這個指令只整理**目前分支的主檔**(`devlog.md` 或 `devlog.<branch>.md`)。要一次整理所有 devlog——其他分支的檔、`devlog.archive.md`、既有的 keep 檔——用 `/devlog-tracker:keep-all`(`commands/keep-all.md`)。
8
+
7
9
  確認之前不要寫任何檔。若這一輪本身是開著的 Round,永遠不要把它搬走,這一輪結束前仍要補 `### Summary` / `### Handoff` / `### Status`(若沒有開著的 Round,見步驟 1 第 2 點)。
8
10
 
9
11
  ## 1. 讀檔、找出開著的 Round
@@ -169,13 +169,30 @@ devlog_round_segments_body() {
169
169
  ' "$1"
170
170
  }
171
171
 
172
+ # The first line of a branch-scoped devlog file: records which branch (or
173
+ # detached worktree dir) it belongs to, since the sanitized filename can't
174
+ # be mapped back. $1 is DEVLOG_ORIGIN ("branch=feat/x"). An HTML comment,
175
+ # so it neither renders nor parses as a `## ` block.
176
+ devlog_origin_line() {
177
+ printf '<!-- devlog-origin: %s -->\n' "$1"
178
+ }
179
+
180
+ # Prints the origin ("branch=feat/x") if line 1 of $1 is an origin marker.
181
+ devlog_origin_of() {
182
+ [ -f "$1" ] || return 0
183
+ sed -n '1s/^<!-- devlog-origin: \(.*\) -->$/\1/p' "$1"
184
+ }
185
+
172
186
  devlog_merge_round_current() {
173
187
  # Appends $2's content onto $1 (one blank line separator, matching the
174
188
  # existing "\n## Round N" append convention) and removes $2. No-op if $2
175
- # is absent or empty — nothing to merge.
189
+ # is absent or empty — nothing to merge. When $1 doesn't exist yet and
190
+ # DEVLOG_ORIGIN is set (a branch file being created), its origin marker
191
+ # goes first.
176
192
  local devlog="$1" current="$2"
177
193
  [ -s "$current" ] || return 0
178
194
  {
195
+ [ -s "$devlog" ] || [ -z "${DEVLOG_ORIGIN:-}" ] || devlog_origin_line "$DEVLOG_ORIGIN"
179
196
  printf '\n'
180
197
  cat "$current"
181
198
  } >> "$devlog" 2>/dev/null || return 1
@@ -6,6 +6,8 @@
6
6
  _DEVLOG_PATH_DIR="$(cd "${BASH_SOURCE[0]%/*}" && pwd)"
7
7
  # shellcheck source=devlog-lock.sh
8
8
  . "$_DEVLOG_PATH_DIR/devlog-lock.sh"
9
+ # shellcheck source=devlog-md.sh
10
+ . "$_DEVLOG_PATH_DIR/devlog-md.sh"
9
11
 
10
12
  # Turns an arbitrary branch/worktree name into a safe devlog.<name>.md
11
13
  # filename segment: anything outside [A-Za-z0-9._-] becomes '-', repeats
@@ -14,6 +16,79 @@ _devlog_sanitize_name() {
14
16
  printf '%s' "$1" | sed -E 's/[^A-Za-z0-9._-]/-/g; s/-+/-/g; s/^-+//; s/-+$//'
15
17
  }
16
18
 
19
+ # Cuts the unfinished tail of $2 (the shared devlog.md) into $3 (a branch
20
+ # file that doesn't exist yet): every "## Round" block after the last one
21
+ # whose Status is DONE. Nothing moves when the last round is DONE, when
22
+ # there are no rounds, or when HEAD doesn't contain the local main/master
23
+ # tip (an older branch being checked out, not one just forked from the
24
+ # work in progress). When $6 (an origin like "branch=feat/x") is given,
25
+ # the branch file starts with its origin marker. The project header,
26
+ # Checkpoints, and Kept/Lessons
27
+ # indexes always stay in $2. When rounds move, $4 (handoff.md) moves to
28
+ # $5 too, since it snapshots that same unfinished work. Caller holds the
29
+ # devlog lock.
30
+ _devlog_migrate_unfinished_tail() {
31
+ local dir="$1" src="$2" dst="$3" src_handoff="$4" dst_handoff="$5" origin="${6:-}"
32
+ [ -s "$src" ] || return 0
33
+
34
+ # Cheap early exit first: until the branch file exists, every hook
35
+ # re-resolves, so the common "main ended DONE" case must not scan every
36
+ # round each time.
37
+ local start end status
38
+ start="$(devlog_list_round_starts "$src" | awk 'END { print $1 }')"
39
+ [ -n "$start" ] || return 0
40
+ end="$(devlog_block_end "$src" "$start")"
41
+ status="$(devlog_round_status "$src" "$start" "$end" | tr -d '[:space:]')"
42
+ [ "$status" != DONE ] || return 0
43
+
44
+ local base
45
+ base="$(git -C "$dir" for-each-ref --format='%(refname:short)' refs/heads/ 2>/dev/null \
46
+ | grep -ixE 'main|master' | head -1)"
47
+ [ -n "$base" ] || return 0
48
+ git -C "$dir" merge-base --is-ancestor "$base" HEAD 2>/dev/null || return 0
49
+
50
+ local ranges=""
51
+ for start in $(devlog_list_round_starts "$src" | awk '{ print $1 }'); do
52
+ end="$(devlog_block_end "$src" "$start")"
53
+ status="$(devlog_round_status "$src" "$start" "$end" | tr -d '[:space:]')"
54
+ if [ "$status" = DONE ]; then ranges=""; else ranges="$ranges $start:$end"; fi
55
+ done
56
+ [ -n "$ranges" ] || return 0
57
+
58
+ # Split in one pass: lines inside a moved range go to $dst, the rest stay.
59
+ # Trailing blank lines are dropped from both so later appends keep the
60
+ # usual single-blank-line separator.
61
+ local drop_trailing_blanks='
62
+ /^[ \t]*$/ { pending = pending $0 ORS; next }
63
+ { printf "%s", pending; pending = ""; print }
64
+ '
65
+ awk -v ranges="$ranges" '
66
+ BEGIN {
67
+ n = split(ranges, r, " ")
68
+ for (i = 1; i <= n; i++) { split(r[i], se, ":"); s[i] = se[1]; e[i] = se[2] }
69
+ }
70
+ {
71
+ moved = 0
72
+ for (i = 1; i <= n; i++) if (NR >= s[i] && NR <= e[i]) { moved = 1; break }
73
+ print moved ? "M" $0 : "K" $0
74
+ }
75
+ ' "$src" > "$src.split" 2>/dev/null || { rm -f "$src.split"; return 0; }
76
+ {
77
+ [ -z "$origin" ] || { devlog_origin_line "$origin"; printf '\n'; }
78
+ sed -n 's/^M//p' "$src.split" | awk "$drop_trailing_blanks"
79
+ } > "$dst.tmp" 2>/dev/null
80
+ sed -n 's/^K//p' "$src.split" | awk "$drop_trailing_blanks" > "$src.tmp" 2>/dev/null
81
+ rm -f "$src.split"
82
+
83
+ # Branch file first: if the second mv fails the tail is duplicated, never lost.
84
+ if mv "$dst.tmp" "$dst" 2>/dev/null && mv "$src.tmp" "$src" 2>/dev/null; then
85
+ if [ -f "$src_handoff" ] && [ ! -f "$dst_handoff" ]; then
86
+ mv "$src_handoff" "$dst_handoff" 2>/dev/null || true
87
+ fi
88
+ fi
89
+ rm -f "$dst.tmp" "$src.tmp" 2>/dev/null || true
90
+ }
91
+
17
92
  # Sets DEVLOG_DIR, DEVLOG_FILE, and HANDOFF_FILE for the branch currently
18
93
  # checked out in $1 (defaults to "."). main/master (any case) and anything
19
94
  # that isn't a git repo or has no resolvable branch keep the shared
@@ -21,18 +96,21 @@ _devlog_sanitize_name() {
21
96
  # devlog.<sanitized-branch>.md and handoff.<sanitized-branch>.md; a
22
97
  # detached HEAD (or an unborn branch, which git also reports as "HEAD"
23
98
  # 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).
99
+ # time a branch resolves to a file that doesn't exist yet, only
100
+ # devlog.md's unfinished tail is cut into it (see
101
+ # _devlog_migrate_unfinished_tail); main's own history stays in devlog.md.
102
+ # Also sets DEVLOG_ORIGIN ("branch=<raw name>" / "detached=<dir>", empty
103
+ # for devlog.md): the first line written into a new branch file records it
104
+ # as an origin marker (docs/design/keep-all.md "Origin marker").
28
105
  devlog_resolve_paths() {
29
106
  local dir="${1:-.}"
30
107
  DEVLOG_DIR="$dir/.devlog"
31
108
  DEVLOG_FILE="$DEVLOG_DIR/devlog.md"
32
109
  # shellcheck disable=SC2034 # consumed by callers (e.g. enforce-devlog.sh), not used in this file
33
110
  HANDOFF_FILE="$DEVLOG_DIR/handoff.md"
111
+ DEVLOG_ORIGIN=""
34
112
 
35
- local branch raw name
113
+ local branch raw name origin
36
114
  branch="$(git -C "$dir" rev-parse --abbrev-ref HEAD 2>/dev/null || echo '')"
37
115
 
38
116
  case "$branch" in
@@ -41,9 +119,11 @@ devlog_resolve_paths() {
41
119
  ;;
42
120
  HEAD)
43
121
  raw="$(basename "$(cd "$dir" 2>/dev/null && pwd)" 2>/dev/null || echo '')"
122
+ origin="detached=$raw"
44
123
  ;;
45
124
  *)
46
125
  raw="$branch"
126
+ origin="branch=$raw"
47
127
  ;;
48
128
  esac
49
129
  [ -n "$raw" ] || return 0
@@ -66,12 +146,15 @@ devlog_resolve_paths() {
66
146
  esac
67
147
 
68
148
  local resolved="$DEVLOG_DIR/devlog.$name.md"
69
- if [ -f "$DEVLOG_DIR/.enabled" ] && [ ! -f "$resolved" ] && [ -f "$DEVLOG_FILE" ]; then
149
+ local resolved_handoff="$DEVLOG_DIR/handoff.$name.md"
150
+ if [ -f "$DEVLOG_DIR/.enabled" ] && [ ! -f "$resolved" ] && [ -s "$DEVLOG_FILE" ]; then
70
151
  devlog_lock_acquire
71
- mv "$DEVLOG_FILE" "$resolved" 2>/dev/null || true
152
+ [ -f "$resolved" ] || _devlog_migrate_unfinished_tail \
153
+ "$dir" "$DEVLOG_FILE" "$resolved" "$HANDOFF_FILE" "$resolved_handoff" "$origin"
72
154
  devlog_lock_release
73
155
  fi
74
156
  DEVLOG_FILE="$resolved"
75
157
  # shellcheck disable=SC2034 # consumed by callers (e.g. enforce-devlog.sh), not used in this file
76
- HANDOFF_FILE="$DEVLOG_DIR/handoff.$name.md"
158
+ HANDOFF_FILE="$resolved_handoff"
159
+ DEVLOG_ORIGIN="$origin"
77
160
  }