devlog-tracker 0.30.0 → 0.31.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +22 -6
  2. package/README.zh-TW.md +22 -6
  3. package/bin/devlog-tracker.js +14 -1
  4. package/cli/agents-md.js +3 -0
  5. package/cli/agents-md.test.js +32 -0
  6. package/cli/core-script.js +25 -0
  7. package/cli/core-script.test.js +62 -0
  8. package/cli/platforms/claude.js +3 -0
  9. package/commands/overview.md +1 -1
  10. package/commands/pr.md +61 -0
  11. package/commands/promote.md +54 -0
  12. package/commands/report.md +23 -0
  13. package/commands/timeline.md +18 -0
  14. package/core/scripts/devlog-lock.sh +10 -0
  15. package/core/scripts/json-field.sh +16 -0
  16. package/core/scripts/lessons-subagent-start.sh +2 -10
  17. package/core/scripts/pr-context.sh +64 -0
  18. package/core/scripts/promote-sources.sh +40 -0
  19. package/core/scripts/promote-target.sh +37 -0
  20. package/core/scripts/promote-write.sh +74 -0
  21. package/core/scripts/report-devlog.sh +140 -0
  22. package/core/scripts/report-scan.awk +74 -0
  23. package/core/scripts/round-start.sh +1 -1
  24. package/core/scripts/tests/test-close-open-round.sh +54 -0
  25. package/core/scripts/tests/test-devlog-lock.sh +44 -0
  26. package/core/scripts/tests/test-json-field.sh +5 -0
  27. package/core/scripts/tests/test-pr-context.sh +107 -0
  28. package/core/scripts/tests/test-promote.sh +158 -0
  29. package/core/scripts/tests/test-report.sh +197 -0
  30. package/core/scripts/tests/test-round-start.sh +28 -0
  31. package/core/scripts/tests/test-timeline.sh +65 -0
  32. package/core/scripts/timeline-devlog.sh +41 -0
  33. package/core/scripts/timeline-render.js +247 -0
  34. package/core/scripts/timeline-render.test.js +101 -0
  35. package/package.json +2 -2
  36. package/skills/devlog-tracker/SKILL.md +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  *English | [繁體中文](README.zh-TW.md)*
4
4
 
5
- **Version** 0.30.0
5
+ **Version** 0.31.1
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
 
@@ -75,7 +75,7 @@ npx devlog-tracker init
75
75
 
76
76
  Without `--claude`/`--codex`/`--cursor`, it interactively asks which platform(s) to install; in an environment without a TTY (e.g. CI) and no flags given, `init` skips the prompt and installs all three platforms directly. You can also combine flags, e.g. `npx devlog-tracker init --claude --codex`.
77
77
 
78
- Copies `core/scripts/`, `claude/hooks.json`, `codex/hooks/`, `cursor/hooks/`, `skills/`, and `commands/` into the project's `.devlog-tracker/`. Re-running `npx devlog-tracker init` upgrades to the package's current version; `npx devlog-tracker status` checks whether the installed version is behind.
78
+ Copies `core/scripts/`, `claude/hooks.json`, `codex/hooks/`, `cursor/hooks/`, `skills/`, and `commands/` into the project's `.devlog-tracker/`. Re-running `npx devlog-tracker init` upgrades to the package's current version; `npx devlog-tracker status` checks whether the installed version is behind. `npx devlog-tracker report [--json] [--all-branches]` and `npx devlog-tracker timeline [--all-branches] [--out <path>]` run the same scripts as `/devlog-tracker:report` and `/devlog-tracker:timeline`, using the vendored copy when there is one.
79
79
 
80
80
  `init` writes this machine's absolute paths into each platform's hooks config and into `.devlog-tracker/env.sh`. If you commit these files to git, each teammate needs to run `npx devlog-tracker init` on their own machine (paths differ per machine); alternatively, add `.devlog-tracker/` and the generated hooks config files to `.gitignore`.
81
81
 
@@ -88,7 +88,9 @@ bash "$DEVLOG_TRACKER_ROOT/core/scripts/start-devlog.sh"
88
88
  bash "$DEVLOG_TRACKER_ROOT/core/scripts/status-devlog.sh"
89
89
  bash "$DEVLOG_TRACKER_ROOT/core/scripts/segment-watch-set.sh" 600
90
90
  bash "$DEVLOG_TRACKER_ROOT/core/scripts/checkpoint-set.sh" 20
91
- # pause / span-open / span-close / compact / keep-move / clean / resume: see commands/*.md
91
+ bash "$DEVLOG_TRACKER_ROOT/core/scripts/report-devlog.sh" --json
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
92
94
  ```
93
95
 
94
96
  ## Quick start
@@ -115,7 +117,11 @@ The table below uses the plugin's `/devlog-tracker:*` namespace; `npx init --cla
115
117
  | `/devlog-tracker:compact` | A script moves older `DONE` rounds into `devlog.archive.md` (Checkpoints and unfinished rounds stay in the main file). |
116
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). |
117
119
  | `/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
+ | `/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. |
118
121
  | `/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. |
122
+ | `/devlog-tracker:report` | Prints devlog stats: round counts (main + archive), per-Status counts, BLOCKED ratio, Checkpoint / kept / lessons counts, and first/last round time. `--all-branches` sums every branch file. Read-only. Also `npx devlog-tracker report [--json]` for machine-readable output. |
123
+ | `/devlog-tracker:timeline` | Renders the devlog into a self-contained, offline HTML timeline at `.devlog/timeline.html` (round cards coloured by Status, Checkpoints interleaved, Status/branch/keyword filters, dark mode). No User Input text. Needs Node ≥18; `--all-branches` includes every branch file. Also `npx devlog-tracker timeline`. |
124
+ | `/devlog-tracker:pr` | Drafts a PR description from this branch's devlog Rounds plus `git log` (Summary / Decisions / Changes / Test plan; no User Input text), writes it to `.devlog/pr-body.md`, and — only after you confirm — runs `gh pr create` or `gh pr edit`. Refuses on `main`/`master`. |
119
125
  | `/devlog-tracker:resume <name>` | Reads the last round and Handoff of a named saved file, checks the "Workspace" section, and proposes how to continue; new work is still written back to the main `devlog.md`. |
120
126
  | `/devlog-tracker:clean` | Unconditionally clears `devlog.md` (including the project summary and all Round history) — no move, no backup, not reversible; it always asks first, and only proceeds once you explicitly reply "clear". Keeps only the currently open round, renumbered as `## Round 1`. |
121
127
  | `/devlog-tracker:status` | Shows whether enforced recording is on, Span, Checkpoint, Segment Watch, the Lessons Mode workspace-drift count, and the last round's Status. |
@@ -127,6 +133,17 @@ The table below uses the plugin's `/devlog-tracker:*` namespace; `npx init --cla
127
133
  | `/devlog-tracker:lessons [<topic>]` | No topic given: prints the `## Lessons index`. Topic given: prints that topic file's full content. Read-only — no workspace check, no confirmation. |
128
134
  | `/devlog-tracker:lessons-drift <count>` | Adjusts the threshold for Lessons Mode's "recurring workspace drift" mechanical reminder (default 3). Subordinate to Lessons Mode; reports `LESSONS_NOT_ENABLED` if it's off. |
129
135
 
136
+ ## Using what's recorded
137
+
138
+ Four commands turn the devlog into something you can hand to others or feed back into the project:
139
+
140
+ - **`report`** — counts only (rounds, Status mix, BLOCKED ratio, Checkpoint / kept / lessons totals). `npx devlog-tracker report --json` is the machine-readable form for CI or dashboards; `--rounds` adds per-Round data.
141
+ - **`timeline`** — renders `.devlog/timeline.html`, one offline page you can open in a browser or attach to a hand-off. Re-run it to refresh; it overwrites the file.
142
+ - **`pr`** — on a feature branch, drafts `.devlog/pr-body.md` from that branch's Rounds and `git log`, then asks before running `gh`. Without `gh` (or not logged in) it stops at the draft.
143
+ - **`promote`** — lifts lasting rules out of kept files, lessons and Checkpoint decisions into a managed block in `CLAUDE.md`/`AGENTS.md`. It lists numbered candidates and writes only the ones you pick; edit or delete rules in that block by hand.
144
+
145
+ None of these copy `### User Input` text into their output (`report --with-input` is the one explicit opt-in). `pr-body.md` and `timeline.html` live under `.devlog/`, which is usually gitignored — if your project commits `.devlog/`, don't commit those two files. `report` and `timeline` are treated like `status` and don't open a Round; `pr` and `promote` do, because they change things outside `.devlog/`. See [`docs/design/read-side-and-promote.md`](docs/design/read-side-and-promote.md).
146
+
130
147
  ## Once enforced recording is on
131
148
 
132
149
  ```mermaid
@@ -225,11 +242,10 @@ When enabled, a development-lesson entry is only considered when a `BLOCKED` sta
225
242
  ## Tests
226
243
 
227
244
  ```
228
- bash core/scripts/run-tests.sh
245
+ bash core/scripts/run-tests.sh # hook self-checks, including the Cursor and Codex adapters
246
+ npm test # CLI, timeline renderer and scripts (Node)
229
247
  ```
230
248
 
231
- Includes the Cursor adapter.
232
-
233
249
  ## License
234
250
 
235
251
  Apache-2.0
package/README.zh-TW.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  *[English](README.md) | 繁體中文*
4
4
 
5
- **版本** 0.30.0
5
+ **版本** 0.31.1
6
6
 
7
7
  在專案中維護一份 `.devlog/devlog.md`,把每一輪對話的請求、決策與結果寫成永久紀錄。對話一 `/clear` 或換 session 就沒了;這份檔案取代那個缺口,讓工作可以中斷再接。沒下過 `/devlog-tracker:start` 時,裝著也不會動任何檔案。
8
8
 
@@ -75,7 +75,7 @@ npx devlog-tracker init
75
75
 
76
76
  沒帶 `--claude`/`--codex`/`--cursor` 時會互動式問要裝哪個平台;在沒有 TTY 的環境(例如 CI)且沒帶旗標時,`init` 不會詢問,直接安裝全部三個平台。也可以組合指定,例如 `npx devlog-tracker init --claude --codex`。
77
77
 
78
- 會把 `core/scripts/`、`claude/hooks.json`、`codex/hooks/`、`cursor/hooks/`、`skills/`、`commands/` 複製進專案的 `.devlog-tracker/`。重新執行 `npx devlog-tracker init` 可以升級到套件目前的版本;`npx devlog-tracker status` 可以查目前裝的版本是否落後。
78
+ 會把 `core/scripts/`、`claude/hooks.json`、`codex/hooks/`、`cursor/hooks/`、`skills/`、`commands/` 複製進專案的 `.devlog-tracker/`。重新執行 `npx devlog-tracker init` 可以升級到套件目前的版本;`npx devlog-tracker status` 可以查目前裝的版本是否落後。`npx devlog-tracker report [--json] [--all-branches]` 和 `npx devlog-tracker timeline [--all-branches] [--out <路徑>]` 跑的是跟 `/devlog-tracker:report`、`/devlog-tracker:timeline` 同一支腳本,有 vendored 版本就用它。
79
79
 
80
80
  `init` 會把這台機器專屬的絕對路徑寫進各平台的 hooks 設定檔與 `.devlog-tracker/env.sh`。如果你把這些檔案 commit 進 git,每位隊友都要在自己的機器上跑一次 `npx devlog-tracker init`(路徑每台機器不同);或者改成把 `.devlog-tracker/` 與產生出來的 hooks 設定檔加進 `.gitignore`。
81
81
 
@@ -88,7 +88,9 @@ bash "$DEVLOG_TRACKER_ROOT/core/scripts/start-devlog.sh"
88
88
  bash "$DEVLOG_TRACKER_ROOT/core/scripts/status-devlog.sh"
89
89
  bash "$DEVLOG_TRACKER_ROOT/core/scripts/segment-watch-set.sh" 600
90
90
  bash "$DEVLOG_TRACKER_ROOT/core/scripts/checkpoint-set.sh" 20
91
- # pause / span-open / span-close / compact / keep-move / clean / resume:見 commands/*.md
91
+ bash "$DEVLOG_TRACKER_ROOT/core/scripts/report-devlog.sh" --json
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
92
94
  ```
93
95
 
94
96
  ## 快速開始
@@ -115,7 +117,11 @@ $devlog-start # npx init --codex
115
117
  | `/devlog-tracker:compact` | 腳本把較舊的 `DONE` 輪次搬到 `devlog.archive.md`(Checkpoint 與未完成輪留在主檔)。 |
116
118
  | `/devlog-tracker:keep` | 掃全檔分主題,一次列出建議,確認後把各段各自搬走成 `devlog.<name>.md`(並在主檔留一個 `## Kept 索引` 指標行,含一句主題描述);也可抽出一段或合併成全部歷史一檔。不是 compact。細節見 [`docs/design/keep.md`](docs/design/keep.md)。 |
117
119
  | `/devlog-tracker:overview` | 讀完所有已 keep 的 `devlog.<name>.md`,整合成跨主題總覽,並列出看起來該進 `CLAUDE.md` 的規範候選。純讀取,不核對工作區、不等確認、不寫檔。細節見 [`docs/design/keep.md`](docs/design/keep.md) Kept index。 |
120
+ | `/devlog-tracker:promote` | 從已 keep 的檔、lessons 檔與 Checkpoint 的 `### 決策` 挑出規範候選並編號列出;只有你選定的才追加到 `CLAUDE.md`(`CLAUDE.md` 只有 `@AGENTS.md` 或只裝 Codex 時改寫 `AGENTS.md`)的 `<!-- devlog-tracker:rules:begin/end -->` 受管區塊。只追加、一字不差的重複會跳過;`init` 不會覆寫這個區塊。 |
118
121
  | `/devlog-tracker:search <關鍵字>` | 在 `devlog.md`/`devlog.archive.md`/已 keep 的 `devlog.<name>.md`/`devlog.lessons.<topic>.md` 裡做不分大小寫的字串搜尋;Claude 讀完命中後用自己的話回答(必要時附檔名/標題/行號)。純讀取,不核對工作區、不等確認、不寫檔。 |
122
+ | `/devlog-tracker:report` | 印出 devlog 統計:Round 數(主檔 + archive)、各 Status 數、BLOCKED 比例、Checkpoint/keep/lessons 數量、第一輪與最後一輪時間。`--all-branches` 合計所有 branch 檔。純讀取。機器可讀輸出用 `npx devlog-tracker report [--json]`。 |
123
+ | `/devlog-tracker:timeline` | 把 devlog 產生成離線可開的自足 HTML 時間軸 `.devlog/timeline.html`(Round 卡片依 Status 上色、穿插 Checkpoint、可依 Status/branch/關鍵字篩選、支援深色模式)。不含 User Input 原文。需要 Node ≥18;`--all-branches` 納入所有 branch 檔。也可用 `npx devlog-tracker timeline`。 |
124
+ | `/devlog-tracker:pr` | 從這個 branch 的 devlog Rounds 與 `git log` 產生 PR 描述(Summary/Decisions/Changes/Test plan,不含 User Input 原文),寫到 `.devlog/pr-body.md`;你確認後才跑 `gh pr create` 或 `gh pr edit`。在 `main`/`master` 上不執行。 |
119
125
  | `/devlog-tracker:resume <name>` | 讀具名保存檔的最後一輪與 Handoff,核對「工作區」後提出接續;新工作仍寫回主 `devlog.md`。 |
120
126
  | `/devlog-tracker:clean` | 無條件清空 `devlog.md`(含專案摘要與所有 Round 歷史),不搬移、不備份、不可復原;執行前一定會先問,要明確回覆「清空」才動手。只留目前開著的那一輪,重編成 `## Round 1`。 |
121
127
  | `/devlog-tracker:status` | 查看強制記錄開關、Span、Checkpoint、Segment Watch、Lessons Mode 工作區漂移計數與最後一輪 Status。 |
@@ -127,6 +133,17 @@ $devlog-start # npx init --codex
127
133
  | `/devlog-tracker:lessons [<topic>]` | 沒給 topic:印 `## Lessons 索引`。給 topic:印該主題檔全文。純讀取,不核對工作區、不等確認。 |
128
134
  | `/devlog-tracker:lessons-drift <次數>` | 調整 Lessons Mode「工作區漂移重複發生」機制性提醒的門檻(預設 3 次)。隸屬 Lessons Mode,沒開會回報 `LESSONS_NOT_ENABLED`。 |
129
135
 
136
+ ## 善用記下來的內容
137
+
138
+ 有四個指令能把 devlog 變成可以交給別人、或回流到專案的東西:
139
+
140
+ - **`report`**:只給數字(Round 數、各 Status 分布、BLOCKED 比例、Checkpoint/keep/lessons 數量)。`npx devlog-tracker report --json` 是給 CI 或儀表板用的機器可讀格式;加 `--rounds` 會附上每個 Round 的資料。
141
+ - **`timeline`**:產生 `.devlog/timeline.html`,一頁離線就能開的網頁,可以用瀏覽器看,也可以附在交接資料裡。重跑就會更新,會直接覆寫。
142
+ - **`pr`**:在 feature branch 上,從這個 branch 的 Rounds 和 `git log` 產生 `.devlog/pr-body.md` 草稿,要跑 `gh` 前會先問你。沒有 `gh` 或沒登入時,就停在草稿。
143
+ - **`promote`**:把 keep 檔、lessons、Checkpoint 決策裡該長期遵守的規則,寫進 `CLAUDE.md`/`AGENTS.md` 的受管區塊。它會列出編號候選,只寫入你選的那幾條;區塊裡的規則要改或刪,請直接手動編輯。
144
+
145
+ 這四個指令都不會把 `### User Input` 原文複製進輸出(唯一的例外是明確加上 `report --with-input`)。`pr-body.md` 和 `timeline.html` 放在 `.devlog/` 底下,這個目錄通常已被 gitignore;如果你的專案會 commit `.devlog/`,別把這兩個檔 commit 進去。`report` 和 `timeline` 跟 `status` 一樣不會開 Round;`pr` 和 `promote` 會,因為它們會動到 `.devlog/` 以外的東西。細節見 [`docs/design/read-side-and-promote.md`](docs/design/read-side-and-promote.md)。
146
+
130
147
  ## 強制記錄開著之後
131
148
 
132
149
  ```mermaid
@@ -225,11 +242,10 @@ Claude 用純文字結尾提出問題、下一則訊息才拿到答案時,不
225
242
  ## 測試
226
243
 
227
244
  ```
228
- bash core/scripts/run-tests.sh
245
+ bash core/scripts/run-tests.sh # hook 自檢,含 Cursor 與 Codex 轉接層
246
+ npm test # CLI、timeline renderer 與 scripts 的 node 測試
229
247
  ```
230
248
 
231
- 含 Cursor adapter。
232
-
233
249
  ## License
234
250
 
235
251
  Apache-2.0
@@ -11,7 +11,7 @@ async function main(argv) {
11
11
  return 0;
12
12
  }
13
13
  if (!command || command === '--help' || command === '-h') {
14
- console.log('Usage: devlog-tracker <init|status> [--codex] [--cursor]');
14
+ console.log('Usage: devlog-tracker <init|status|report|timeline> [--codex] [--cursor] [--json] [--all-branches] [--out <path>]');
15
15
  return 0;
16
16
  }
17
17
  if (command === 'init') {
@@ -38,6 +38,19 @@ async function main(argv) {
38
38
  }
39
39
  return 0;
40
40
  }
41
+ const CORE_COMMANDS = { report: 'report-devlog.sh', timeline: 'timeline-devlog.sh' };
42
+ if (Object.prototype.hasOwnProperty.call(CORE_COMMANDS, command)) {
43
+ const { runCoreScript } = require('../cli/core-script');
44
+ const r = runCoreScript({
45
+ targetDir: process.cwd(),
46
+ repoRoot: path.join(__dirname, '..'),
47
+ name: CORE_COMMANDS[command],
48
+ args: rest,
49
+ });
50
+ process.stdout.write(r.stdout);
51
+ process.stderr.write(r.stderr);
52
+ return r.status;
53
+ }
41
54
  console.error(`Unknown command: ${command}`);
42
55
  return 1;
43
56
  }
package/cli/agents-md.js CHANGED
@@ -23,7 +23,10 @@ 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
+ | 沉澱規範寫進 CLAUDE.md/AGENTS.md / promote | \`commands/promote.md\` |
26
27
  | 搜尋 / search | \`commands/search.md\` |
28
+ | 統計 / report、HTML 時間軸 / timeline | \`commands/report.md\`、\`commands/timeline.md\` |
29
+ | 產生 PR 描述 / pr | \`commands/pr.md\` |
27
30
  | 長任務定期記錄 / span | \`commands/span.md\` |
28
31
  | 調整沉默門檻 / checkpoint、segment-watch | \`commands/checkpoint.md\`、\`commands/segment-watch.md\` |
29
32
  | 開發歷程教訓 / lessons、lessons-on、lessons-off、lessons-drift | \`commands/lessons.md\`、\`commands/lessons-on.md\`、\`commands/lessons-off.md\`、\`commands/lessons-drift.md\` |
@@ -40,6 +40,38 @@ test('is idempotent and replaces the block in place', () => {
40
40
  assert.equal(once.split(BEGIN).length, 2);
41
41
  });
42
42
 
43
+ const RULES_BLOCK = '<!-- devlog-tracker:rules:begin -->\n## devlog-tracker 沉澱的規範\n\n- rule one\n<!-- devlog-tracker:rules:end -->\n';
44
+
45
+ test('upsert never touches the promote rules block (rules before the init block)', () => {
46
+ const dir = tmp();
47
+ fs.writeFileSync(path.join(dir, 'AGENTS.md'), `# Mine\n\n${RULES_BLOCK}`);
48
+ upsertAgentsMd(dir);
49
+ upsertAgentsMd(dir);
50
+ const text = fs.readFileSync(path.join(dir, 'AGENTS.md'), 'utf8');
51
+ assert.ok(text.includes(RULES_BLOCK));
52
+ assert.equal(text.split(BEGIN).length, 2);
53
+ });
54
+
55
+ test('upsert never touches the promote rules block (rules after the init block)', () => {
56
+ const dir = tmp();
57
+ upsertAgentsMd(dir);
58
+ fs.appendFileSync(path.join(dir, 'AGENTS.md'), `\n${RULES_BLOCK}`);
59
+ upsertAgentsMd(dir);
60
+ const text = fs.readFileSync(path.join(dir, 'AGENTS.md'), 'utf8');
61
+ assert.ok(text.includes(RULES_BLOCK));
62
+ assert.ok(text.indexOf(END) < text.indexOf(RULES_BLOCK));
63
+ });
64
+
65
+ test('claude CLAUDE.md upsert never touches the promote rules block', () => {
66
+ const dir = tmp();
67
+ fs.writeFileSync(path.join(dir, 'CLAUDE.md'), `# Mine\n\n${RULES_BLOCK}`);
68
+ upsertMarkdown(dir, { fileName: 'CLAUDE.md', block: `${BEGIN}\nx\n${END}\n` });
69
+ upsertMarkdown(dir, { fileName: 'CLAUDE.md', block: `${BEGIN}\ny\n${END}\n` });
70
+ const text = fs.readFileSync(path.join(dir, 'CLAUDE.md'), 'utf8');
71
+ assert.ok(text.includes(RULES_BLOCK));
72
+ assert.ok(text.includes('\ny\n') && !text.includes('\nx\n'));
73
+ });
74
+
43
75
  test('block mentions every command doc and Codex skill naming convention', () => {
44
76
  const commandsDir = path.join(__dirname, '..', 'commands');
45
77
  const text = fs.readFileSync(upsertAgentsMd(tmp()), 'utf8');
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { spawnSync } = require('child_process');
5
+
6
+ // npx 子命令(report/timeline)直接轉呼 core/scripts 的 bash 腳本:專案有
7
+ // vendored 版本就用它(與 hooks 同一份),否則用套件內附的版本。
8
+ function resolveScript({ targetDir, repoRoot, name }) {
9
+ const vendored = path.join(targetDir, '.devlog-tracker', 'core', 'scripts', name);
10
+ return fs.existsSync(vendored) ? vendored : path.join(repoRoot, 'core', 'scripts', name);
11
+ }
12
+
13
+ function runCoreScript({ targetDir, repoRoot, name, args }) {
14
+ const script = resolveScript({ targetDir, repoRoot, name });
15
+ const r = spawnSync('bash', [script, ...args], {
16
+ cwd: targetDir,
17
+ env: { ...process.env, DEVLOG_PROJECT_DIR: targetDir },
18
+ encoding: 'utf8',
19
+ // Default 1 MB would truncate a large `report --json --rounds`.
20
+ maxBuffer: 64 * 1024 * 1024,
21
+ });
22
+ return { status: r.status === null ? 1 : r.status, stdout: r.stdout || '', stderr: r.stderr || '' };
23
+ }
24
+
25
+ module.exports = { resolveScript, runCoreScript };
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+ const test = require('node:test');
3
+ const assert = require('node:assert/strict');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+ const { spawnSync } = require('child_process');
8
+ const { resolveScript, runCoreScript } = require('./core-script');
9
+
10
+ function tmp() {
11
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'devlog-tracker-core-'));
12
+ }
13
+ function writeScript(dir, name, body) {
14
+ const scripts = path.join(dir, 'core', 'scripts');
15
+ fs.mkdirSync(scripts, { recursive: true });
16
+ fs.writeFileSync(path.join(scripts, name), body);
17
+ }
18
+
19
+ test('prefers the vendored copy and passes DEVLOG_PROJECT_DIR + args', () => {
20
+ const target = tmp();
21
+ const repo = tmp();
22
+ writeScript(path.join(target, '.devlog-tracker'), 'x.sh', 'echo "vendored $DEVLOG_PROJECT_DIR $*"\n');
23
+ writeScript(repo, 'x.sh', 'echo packaged\n');
24
+ assert.equal(resolveScript({ targetDir: target, repoRoot: repo, name: 'x.sh' }),
25
+ path.join(target, '.devlog-tracker', 'core', 'scripts', 'x.sh'));
26
+ const r = runCoreScript({ targetDir: target, repoRoot: repo, name: 'x.sh', args: ['--json'] });
27
+ assert.equal(r.status, 0);
28
+ assert.equal(r.stdout, `vendored ${target} --json\n`);
29
+ });
30
+
31
+ test('falls back to the packaged script when nothing is vendored', () => {
32
+ const target = tmp();
33
+ const repo = tmp();
34
+ writeScript(repo, 'x.sh', 'echo packaged\n');
35
+ const r = runCoreScript({ targetDir: target, repoRoot: repo, name: 'x.sh', args: [] });
36
+ assert.equal(r.stdout, 'packaged\n');
37
+ });
38
+
39
+ test('propagates exit code and stderr', () => {
40
+ const target = tmp();
41
+ const repo = tmp();
42
+ writeScript(repo, 'x.sh', 'echo oops >&2; exit 3\n');
43
+ const r = runCoreScript({ targetDir: target, repoRoot: repo, name: 'x.sh', args: [] });
44
+ assert.equal(r.status, 3);
45
+ assert.equal(r.stderr, 'oops\n');
46
+ });
47
+
48
+ test('bin report runs the real packaged report-devlog.sh', () => {
49
+ const cwd = tmp();
50
+ const bin = path.join(__dirname, '..', 'bin', 'devlog-tracker.js');
51
+ const r = spawnSync(process.execPath, [bin, 'report'], { cwd, encoding: 'utf8' });
52
+ assert.equal(r.status, 0);
53
+ assert.equal(r.stdout, 'NOT_STARTED\n');
54
+ });
55
+
56
+ test('bin timeline runs the real packaged timeline-devlog.sh', () => {
57
+ const cwd = tmp();
58
+ const bin = path.join(__dirname, '..', 'bin', 'devlog-tracker.js');
59
+ const r = spawnSync(process.execPath, [bin, 'timeline'], { cwd, encoding: 'utf8' });
60
+ assert.equal(r.status, 0);
61
+ assert.equal(r.stdout, 'NOT_STARTED\n');
62
+ });
@@ -22,6 +22,9 @@ 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
+ | 沉澱規範寫進 CLAUDE.md/AGENTS.md / promote | \`commands/promote.md\` |
26
+ | 統計 / report、HTML 時間軸 / timeline | \`commands/report.md\`、\`commands/timeline.md\` |
27
+ | 產生 PR 描述 / pr | \`commands/pr.md\` |
25
28
  | 長任務定期記錄 / span | \`commands/span.md\` |
26
29
  | 調整沉默門檻 / checkpoint、segment-watch | \`commands/checkpoint.md\`、\`commands/segment-watch.md\` |
27
30
  | 開發歷程教訓 / lessons、lessons-on、lessons-off、lessons-drift | \`commands/lessons.md\`、\`commands/lessons-on.md\`、\`commands/lessons-off.md\`、\`commands/lessons-drift.md\` |
@@ -25,7 +25,7 @@ DEVLOG_PROJECT_DIR="<剛才記下的專案根目錄絕對路徑>" bash "${PLUGIN
25
25
  用 Read 讀取每個 `EXISTS=1` 的檔案全文。讀完後在對話裡輸出兩塊,不寫入任何檔案:
26
26
 
27
27
  1. **跨主題總覽**:依主題(不是依檔案機械條列)概述做了什麼、關鍵決定、還沒收尾或之後可能要接續的事。同一主題如果分散在多個檔案,合併描述,不用一檔一段硬切。
28
- 2. **可能該進 CLAUDE.md 的規範**:從內容裡挑出讀起來像「規則、決定、之後應該一直遵守」的部分(不是單次任務細節、不是已經過時或被後續內容取代的決定)。格式盡量貼近 CLAUDE.md 條列寫法(一行一條規則,必要時附一句原因),方便使用者直接複製貼上。找不到夠格的候選就不要輸出這一節、也不要硬湊。
28
+ 2. **可能該進 CLAUDE.md 的規範**:從內容裡挑出讀起來像「規則、決定、之後應該一直遵守」的部分(不是單次任務細節、不是已經過時或被後續內容取代的決定)。格式盡量貼近 CLAUDE.md 條列寫法(一行一條規則,必要時附一句原因),方便使用者直接複製貼上。找不到夠格的候選就不要輸出這一節、也不要硬湊。有輸出這一節時,最後補一句:要把其中幾條正式寫進 CLAUDE.md/AGENTS.md,可以執行 `/devlog-tracker:promote`(它會另外把 lessons 與 Checkpoint 決策也納入,並在你選定後才寫入)。
29
29
 
30
30
  ## 3. 收尾
31
31
 
package/commands/pr.md ADDED
@@ -0,0 +1,61 @@
1
+ ---
2
+ description: 從這個 branch 的 devlog 與 git log 產生 PR 描述,寫到 .devlog/pr-body.md;使用者確認後才用 gh 建立或更新 PR。
3
+ ---
4
+
5
+ 這是使用者主動執行 `/devlog-tracker:pr` 時才做的事。產生草稿是純讀取;**建立或更新 PR 是對外動作,一定要等使用者明確確認才做。**
6
+
7
+ ## 1. 取得 context
8
+
9
+ 記下你目前已經確認的專案根目錄絕對路徑(後面步驟都要用這個值,不要用 `$(pwd)` 重新推——理由同 `commands/continue.md` 步驟 1)。先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄),再跑:
10
+
11
+ ```bash
12
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
13
+ DEVLOG_PROJECT_DIR="<剛才記下的專案根目錄絕對路徑>" bash "${PLUGIN_ROOT}/core/scripts/pr-context.sh"
14
+ ```
15
+
16
+ - `NOT_A_REPO`:告知這不是 git repo,結束。
17
+ - `DETACHED_HEAD`:告知目前是 detached HEAD,先切到要開 PR 的 branch,結束。
18
+ - `ON_DEFAULT_BRANCH`:告知目前在 `main`/`master`,PR 要從 feature branch 開,結束。不要猜範圍。
19
+ - `NO_BASE`:告知找不到 base branch(沒有 `origin/HEAD`、也沒有本地 `main`/`master`),請使用者告訴你 base 名稱後再手動用 `git log <base>..HEAD` 繼續;結束這個流程。
20
+ - 其他:記下 `BRANCH`、`BASE`、`BASE_REF`、`DEVLOG_FILE`、`ROUNDS`(或 `NO_BRANCH_DEVLOG`)、`COMMITS`、`GH`、`PR`。
21
+
22
+ `COMMITS=0` 時告知這個 branch 相對 `BASE` 沒有 commit,問使用者是否仍要繼續。
23
+
24
+ ## 2. 讀資料
25
+
26
+ - 跑 `git log --reverse --format='%h %s%n%b' <BASE_REF>..HEAD` 與 `git diff --stat <BASE_REF>...HEAD`。
27
+ - 有 `ROUNDS` 時,用 Read 讀 `DEVLOG_FILE` 全文,只看這些 Round(`ROUNDS` 是每個 `## Round` 標題的行號)。每個 Round 取 `### Summary`、`#### 決策`、`#### 檔案`、驗證紀錄(測試指令與結果,通常在 Summary、`#### 現況` 或 `### 段落`)。**不要讀或引用 `### User Input`。**
28
+ - `NO_BRANCH_DEVLOG` 時只依 git log 與 diff 產生,並在對話裡說明「這個 branch 沒有 devlog 紀錄,描述只根據 commit 產生」。
29
+
30
+ ## 3. 產生 PR body
31
+
32
+ 語言跟使用者一致。四節固定,順序不變:
33
+
34
+ 1. **Summary**:這個 branch 做了什麼,2–4 條,寫結果不寫過程。
35
+ 2. **Decisions**:取自 Rounds 的 `#### 決策`,只留最終版本;被後面 Round 推翻或改掉的不列。沒有就寫「無」。
36
+ 3. **Changes**:依 commit 或檔案分組,每組一行說明。用 `#### 檔案` 與 git log 交叉比對;兩邊對不上時以 git 為準。
37
+ 4. **Test plan**:取自 Rounds 的驗證紀錄,列出實際跑過的指令與結果。沒有紀錄就寫「未記錄」——**不要捏造沒跑過的測試。**
38
+
39
+ 專案的 CLAUDE.md/AGENTS.md 若規定 PR 描述結尾格式(例如署名行),照做;此外不要自己加簽名。
40
+
41
+ 用 Write 寫到 `<專案根目錄>/.devlog/pr-body.md`(`.devlog/` 通常已被 gitignore,沒有的話別把它 commit),並在對話裡完整顯示內容。
42
+
43
+ ## 4. 等確認,再送出
44
+
45
+ **停下來。** 問使用者要不要送出,並說明接下來會做什麼:
46
+
47
+ - `GH=no`:告知 `gh` 未安裝或未登入,草稿在 `.devlog/pr-body.md`,可以自己貼上;結束。
48
+ - `PR=none`:提議一個 PR 標題(conventional commit 風格,跟這個 repo 既有 commit 一致),等使用者確認標題與內容後才跑:
49
+
50
+ ```bash
51
+ gh pr create --base "<BASE>" --title "<確認過的標題>" --body-file "<專案根目錄>/.devlog/pr-body.md"
52
+ ```
53
+
54
+ branch 還沒 push 時,`gh` 會提示;先問使用者要不要 `git push -u origin <BRANCH>`,不要自己推。
55
+ - `PR=<n>`:提醒「會覆寫 PR #<n> 目前的描述(包含別人手改的內容)」,確認後才跑:
56
+
57
+ ```bash
58
+ gh pr edit <n> --body-file "<專案根目錄>/.devlog/pr-body.md"
59
+ ```
60
+
61
+ 使用者要求修改時,改 `.devlog/pr-body.md` 後再顯示一次、再等確認。送出後回報 PR 網址。
@@ -0,0 +1,54 @@
1
+ ---
2
+ description: 從已 keep 的檔、lessons 檔與 Checkpoint 決策挑出該長期遵守的規範,你選定後寫進 CLAUDE.md 或 AGENTS.md 的 devlog-tracker 規範區塊。
3
+ ---
4
+
5
+ 這是使用者主動執行 `/devlog-tracker:promote` 時才做的事。**沒有使用者明確選擇就不寫入任何檔案。**
6
+
7
+ ## 1. 取得來源與目標
8
+
9
+ 記下你目前已經確認的專案根目錄絕對路徑(後面步驟都要用這個值,不要用 `$(pwd)` 重新推——理由同 `commands/continue.md` 步驟 1)。先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄),再跑:
10
+
11
+ ```bash
12
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
13
+ DEVLOG_PROJECT_DIR="<專案根目錄絕對路徑>" bash "${PLUGIN_ROOT}/core/scripts/promote-sources.sh"
14
+ DEVLOG_PROJECT_DIR="<專案根目錄絕對路徑>" bash "${PLUGIN_ROOT}/core/scripts/promote-target.sh"
15
+ ```
16
+
17
+ - `NO_SOURCES`:告知目前沒有可以沉澱的來源(還沒 keep 過、沒有 lessons、也沒有 Checkpoint),可以先用 `/devlog-tracker:keep` 或開 Lessons Mode;結束。
18
+ - `FILE=<路徑> KIND=kept|lessons EXISTS=1`:要讀的檔。`EXISTS=0` 是索引還在但檔案已被刪掉,跳過,最後提一句。
19
+ - `CHECKPOINT=<檔案>:<行號>`:從該行的 `## Checkpoint` 標題往下讀到下一個 `## ` 標題為止,只看其中 `### 決策` 小節。
20
+ - `TARGET=<路徑>`:規則要寫進的檔。`EXISTING=<n>` 表示這個檔已經有 n 條沉澱過的規則。
21
+
22
+ ## 2. 挑候選
23
+
24
+ 用 Read 讀 `EXISTS=1` 的檔案、每個 Checkpoint 的 `### 決策`,以及 `TARGET` 檔(存在的話,看它整份內容,含 `<!-- devlog-tracker:rules:begin -->` 區塊內既有規則)。
25
+
26
+ 挑出讀起來像「規則、之後應該一直遵守」的內容:
27
+
28
+ - 要:跨任務都成立的約定、踩過坑後定下的做法、明確的「不要做 X」。
29
+ - 不要:單次任務細節、已經被後續內容推翻或取代的決定、`TARGET` 檔裡已經寫了(不論在不在規範區塊裡)意思相同的規則。
30
+
31
+ **不要把 `### User Input` 原文寫成規則候選**——kept 檔保留完整 Round,內容是單次任務的原文,不是沉澱過的規範。
32
+
33
+ 每條候選寫成一行、可以直接放進 CLAUDE.md 的條列句,必要時附一句原因,結尾標出處:`(來源:<檔名>「<標題>」)`。找不到夠格的就說沒有,不硬湊。
34
+
35
+ 在對話裡列出編號清單,並說明會寫進哪個檔(`TARGET`)。
36
+
37
+ ## 3. 等使用者選
38
+
39
+ **停下來。** 請使用者回覆要寫入的編號(例如 `1,3`),也可以要求改寫某條。使用者沒有明確選擇(例如只說「看起來不錯」)時,再問一次要哪幾條;不要自己決定全寫。
40
+
41
+ ## 4. 寫入
42
+
43
+ 1. 用 Write 把選定(改寫過就用改寫版)的規則寫到 `<專案根目錄>/.devlog/.promote-rules.tmp`,一行一條。
44
+ 2. 跑(這是新的一個 Bash call,`PLUGIN_ROOT` 要重新設一次——理由同步驟 1):
45
+
46
+ ```bash
47
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
48
+ DEVLOG_PROJECT_DIR="<專案根目錄絕對路徑>" bash "${PLUGIN_ROOT}/core/scripts/promote-write.sh" "<TARGET>" "<專案根目錄>/.devlog/.promote-rules.tmp" \
49
+ && rm -f "<專案根目錄>/.devlog/.promote-rules.tmp"
50
+ ```
51
+
52
+ 3. 依輸出 `ADDED=<n> SKIPPED_DUP=<m> TARGET=<路徑>` 回報:寫入了幾條、幾條因為一字不差已存在而跳過、寫到哪個檔。提醒使用者這個檔要不要 commit 由他決定。
53
+
54
+ 規範區塊只會被追加;要修改或刪除已寫入的規則,請使用者直接編輯該檔。`npx devlog-tracker init` 不會動這個區塊。
@@ -0,0 +1,23 @@
1
+ ---
2
+ description: 查看 devlog 統計:Round 數、各 Status、BLOCKED 比例、Checkpoint/keep/lessons 數量與時間範圍(純讀取)。
3
+ ---
4
+
5
+ 這是純讀取,不改任何檔。先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄),再跑:
6
+
7
+ ```bash
8
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
9
+ DEVLOG_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/core/scripts/report-devlog.sh"
10
+ ```
11
+
12
+ 使用者要看所有 branch 的合計時加 `--all-branches`。
13
+
14
+ - `NOT_STARTED`:告知還沒 `/devlog-tracker:start`,結束。
15
+ - 其他輸出是 `KEY=VALUE`,翻成幾行給人看:
16
+ - `BRANCH`:統計的是哪個 branch 的檔案(`--all-branches` 時仍印目前 branch)。
17
+ - `ROUNDS_TOTAL`/`ROUNDS_MAIN`/`ROUNDS_ARCHIVE`:Round 總數與主檔、archive 各自的數量。
18
+ - `STATUS_*`:各 Status 的 Round 數;`BLOCKED_RATIO` 是 BLOCKED 佔總數的百分比(整數)。
19
+ - `CHECKPOINTS`、`KEPT_TOPICS`、`LESSONS_TOPICS`:Checkpoint 區塊、已 keep 主題、Lessons 主題數。
20
+ - `LESSONS_ADVISORY`(有才印):Lessons Mode 機制性訊號累積次數/門檻。
21
+ - `FIRST_ROUND_AT`/`LAST_ROUND_AT`:第一輪與最後一輪的時間;`none` 表示沒有 Round。
22
+
23
+ 需要機器可讀的輸出(CI、儀表板)時,告訴使用者可以用 `npx devlog-tracker report --json`。
@@ -0,0 +1,18 @@
1
+ ---
2
+ description: 把 devlog 產生成一份離線可開的 HTML 時間軸(.devlog/timeline.html),可依 Status/branch/關鍵字篩選。
3
+ ---
4
+
5
+ 先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄),再跑:
6
+
7
+ ```bash
8
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
9
+ DEVLOG_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/core/scripts/timeline-devlog.sh"
10
+ ```
11
+
12
+ 使用者要看所有 branch 時加 `--all-branches`;要寫到別的位置時加 `--out <路徑>`。
13
+
14
+ - `NO_NODE`:告知這個功能需要 Node.js(≥18),裝好後再跑;結束。
15
+ - `NOT_STARTED`:告知還沒 `/devlog-tracker:start`,結束。
16
+ - `OUT=<路徑>`:告知檔案位置,並提示可以直接用瀏覽器開(macOS:`open <路徑>`)。不要自己打開瀏覽器,也不要把 HTML 內容貼進對話。
17
+
18
+ 時間軸不含 User Input 原文。`.devlog/timeline.html` 在 `.devlog/` 底下,每次重跑會覆寫;`.devlog/` 通常已被 gitignore,沒有的話別把它 commit。
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env bash
2
2
  # Sourced helper for serializing writes under .devlog.
3
+ #
4
+ # The holder exports DEVLOG_LOCK_OWNER=<its pid>, so a script it runs as a
5
+ # child (round-start.sh -> close-open-round.sh) finds the lock already its
6
+ # own and proceeds instead of waiting out the contention timeout on its
7
+ # parent. The child never holds it, so its release leaves the lock alone.
3
8
  devlog_lock_acquire() {
4
9
  local dir="${DEVLOG_DIR:-.}/.lock"
5
10
  local start now pid
@@ -14,12 +19,16 @@ devlog_lock_acquire() {
14
19
  if mkdir "$dir" 2>/dev/null; then
15
20
  LOCK_HELD=1
16
21
  printf '%s\n' "$$" > "$dir/pid" 2>/dev/null || true
22
+ export DEVLOG_LOCK_OWNER="$$"
17
23
  return 0
18
24
  fi
19
25
  pid="$(cat "$dir/pid" 2>/dev/null || true)"
20
26
  case "$pid" in
21
27
  ''|*[!0-9]*) pid='' ;;
22
28
  esac
29
+ if [ -n "$pid" ] && [ "$pid" = "${DEVLOG_LOCK_OWNER:-}" ]; then
30
+ return 0
31
+ fi
23
32
  # Stale lock: the pid that created it is no longer running (a crashed
24
33
  # session), so reclaim it immediately instead of waiting out the full
25
34
  # contention timeout below.
@@ -42,6 +51,7 @@ devlog_lock_release() {
42
51
  rm -f "${DEVLOG_DIR:-.}/.lock/pid" 2>/dev/null || true
43
52
  rmdir "${DEVLOG_DIR:-.}/.lock" 2>/dev/null || true
44
53
  LOCK_HELD=0
54
+ unset DEVLOG_LOCK_OWNER
45
55
  fi
46
56
  }
47
57
 
@@ -94,3 +94,19 @@ json_str_field() {
94
94
  slugify() {
95
95
  printf '%s' "$1" | tr ' ' '-' | sed -E 's/-+/-/g; s/^-//; s/-$//'
96
96
  }
97
+
98
+ json_escape() {
99
+ printf '%s' "$1" | awk '
100
+ BEGIN {
101
+ ORS = ""
102
+ # Same control-char handling as report-scan.awk esc(): \t \r get short
103
+ # escapes below, the rest of 0x01-0x1F (e.g. pasted ANSI \033) -> \u00XX.
104
+ for (ci = 1; ci <= 31; ci++) { cchar[ci] = sprintf("%c", ci); cesc[ci] = sprintf("\\u%04x", ci) }
105
+ }
106
+ {
107
+ gsub(/\\/, "\\\\"); gsub(/"/, "\\\""); gsub(/\t/, "\\t"); gsub(/\r/, "\\r")
108
+ for (ci = 1; ci <= 31; ci++) if (index($0, cchar[ci]) > 0) gsub(cchar[ci], cesc[ci])
109
+ if (NR > 1) print "\\n"
110
+ print
111
+ }'
112
+ }
@@ -10,6 +10,8 @@ set -uo pipefail
10
10
 
11
11
  _src="${BASH_SOURCE[0]}"
12
12
  HOOKS_DIR="$(cd "${_src%/*}" && pwd)"
13
+ # shellcheck source=json-field.sh
14
+ . "$HOOKS_DIR/json-field.sh"
13
15
  PROJECT_DIR="${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}"
14
16
  [ -f "$PROJECT_DIR/.devlog/.enabled" ] || exit 0
15
17
  [ -f "$PROJECT_DIR/.devlog/.lessons-enabled" ] || exit 0
@@ -22,15 +24,5 @@ DEVLOG_PROJECT_DIR='${PROJECT_DIR}' bash '${HOOKS_DIR}/lessons-append.sh' --topi
22
24
 
23
25
  路徑都是絕對路徑,在 worktree 裡也照原樣用,不要改成相對路徑。有記的話,在最終回報裡用一句話說明記了哪個主題;腳本回報 NEW_TOPIC 時可改用它列出的既有主題重跑。"
24
26
 
25
- json_escape() {
26
- printf '%s' "$1" | awk '
27
- BEGIN { ORS = "" }
28
- {
29
- gsub(/\\/, "\\\\"); gsub(/"/, "\\\""); gsub(/\t/, "\\t"); gsub(/\r/, "\\r")
30
- if (NR > 1) print "\\n"
31
- print
32
- }'
33
- }
34
-
35
27
  printf '{"hookSpecificOutput":{"hookEventName":"SubagentStart","additionalContext":"%s"}}\n' "$(json_escape "$MSG")"
36
28
  exit 0