devlog-tracker 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +43 -71
  2. package/claude/hooks.json +75 -0
  3. package/cli/agents-md.js +18 -12
  4. package/cli/agents-md.test.js +37 -9
  5. package/cli/init.js +6 -4
  6. package/cli/init.test.js +5 -4
  7. package/cli/merge-hooks.js +5 -2
  8. package/cli/merge-hooks.test.js +52 -0
  9. package/cli/platforms/claude.js +55 -0
  10. package/cli/platforms/claude.test.js +178 -0
  11. package/cli/platforms/codex.js +21 -14
  12. package/cli/skills-from-commands.js +32 -0
  13. package/cli/skills-from-commands.test.js +51 -0
  14. package/cli/vendor.js +8 -1
  15. package/cli/vendor.test.js +24 -5
  16. package/codex/hooks/on-pre-tool.sh +3 -3
  17. package/codex/hooks/on-session-end.sh +2 -2
  18. package/codex/hooks/on-session-start.sh +2 -2
  19. package/codex/hooks/on-stop.sh +3 -3
  20. package/codex/hooks/on-user-prompt-submit.sh +2 -2
  21. package/codex/hooks/test-adapters.sh +2 -2
  22. package/commands/checkpoint.md +2 -2
  23. package/commands/clean.md +3 -3
  24. package/commands/compact.md +3 -3
  25. package/commands/continue.md +4 -4
  26. package/commands/keep.md +3 -3
  27. package/commands/lessons-drift.md +2 -2
  28. package/commands/lessons-off.md +3 -3
  29. package/commands/lessons-on.md +3 -3
  30. package/commands/lessons.md +3 -3
  31. package/commands/overview.md +3 -3
  32. package/commands/pause.md +3 -3
  33. package/commands/resume.md +3 -3
  34. package/commands/segment-watch.md +3 -3
  35. package/commands/span.md +2 -2
  36. package/commands/start.md +3 -3
  37. package/commands/status.md +3 -3
  38. package/{hooks → core}/scripts/await-open.sh +1 -1
  39. package/{hooks → core}/scripts/checkpoint-set.sh +1 -1
  40. package/{hooks → core}/scripts/clean-devlog.sh +1 -1
  41. package/{hooks → core}/scripts/close-open-round.sh +1 -1
  42. package/{hooks → core}/scripts/compact-devlog.sh +1 -1
  43. package/{hooks → core}/scripts/enforce-devlog.sh +1 -1
  44. package/{hooks → core}/scripts/keep-move.sh +1 -1
  45. package/{hooks → core}/scripts/kept-list.sh +1 -1
  46. package/{hooks → core}/scripts/lessons-append.sh +2 -2
  47. package/{hooks → core}/scripts/lessons-drift-set.sh +1 -1
  48. package/{hooks → core}/scripts/lessons-off.sh +1 -1
  49. package/{hooks → core}/scripts/lessons-on.sh +1 -1
  50. package/{hooks → core}/scripts/lessons-read.sh +1 -1
  51. package/{hooks → core}/scripts/on-tool-failure.sh +1 -1
  52. package/{hooks → core}/scripts/pause-devlog.sh +1 -1
  53. package/core/scripts/project-dir.sh +19 -0
  54. package/{hooks → core}/scripts/resume-devlog.sh +1 -1
  55. package/{hooks → core}/scripts/round-start.sh +1 -1
  56. package/{hooks → core}/scripts/run-tests.sh +1 -1
  57. package/{hooks → core}/scripts/segment-watch-set.sh +1 -1
  58. package/{hooks → core}/scripts/segment-watch.sh +1 -1
  59. package/{hooks → core}/scripts/session-start-devlog.sh +1 -1
  60. package/{hooks → core}/scripts/span-close.sh +1 -1
  61. package/{hooks → core}/scripts/span-open.sh +1 -1
  62. package/{hooks → core}/scripts/start-devlog.sh +1 -1
  63. package/{hooks → core}/scripts/status-devlog.sh +1 -1
  64. package/core/scripts/tests/test-cli-init-e2e.sh +68 -0
  65. package/core/scripts/tests/test-project-dir.sh +30 -0
  66. package/{hooks → core}/scripts/tests/test-session-start-devlog.sh +2 -2
  67. package/{hooks → core}/scripts/workspace-snapshot.sh +2 -2
  68. package/cursor/hooks/on-pre-tool.sh +2 -2
  69. package/cursor/hooks/on-session-end.sh +2 -2
  70. package/cursor/hooks/on-session-start.sh +2 -2
  71. package/cursor/hooks/on-stop.sh +2 -2
  72. package/cursor/hooks/on-submit-prompt.sh +2 -2
  73. package/cursor/hooks/on-tool-failure.sh +2 -2
  74. package/package.json +4 -3
  75. package/skills/devlog-tracker/SKILL.md +9 -9
  76. package/skills/devlog-tracker/references/lessons-mode.md +1 -1
  77. package/skills/devlog-tracker/references/reply-fold.md +1 -1
  78. package/hooks/scripts/tests/test-cli-init-e2e.sh +0 -38
  79. /package/{hooks → core}/scripts/detect-pending-question.sh +0 -0
  80. /package/{hooks → core}/scripts/devlog-lock.sh +0 -0
  81. /package/{hooks → core}/scripts/devlog-md.sh +0 -0
  82. /package/{hooks → core}/scripts/devlog-path.sh +0 -0
  83. /package/{hooks → core}/scripts/files-snapshot.sh +0 -0
  84. /package/{hooks → core}/scripts/json-field.sh +0 -0
  85. /package/{hooks → core}/scripts/on-session-end.sh +0 -0
  86. /package/{hooks → core}/scripts/on-stop-failure.sh +0 -0
  87. /package/{hooks → core}/scripts/redact-prompt.sh +0 -0
  88. /package/{hooks → core}/scripts/tests/test-await-open.sh +0 -0
  89. /package/{hooks → core}/scripts/tests/test-branch-scoped-integration.sh +0 -0
  90. /package/{hooks → core}/scripts/tests/test-checkpoint-set.sh +0 -0
  91. /package/{hooks → core}/scripts/tests/test-clean-devlog.sh +0 -0
  92. /package/{hooks → core}/scripts/tests/test-close-open-round.sh +0 -0
  93. /package/{hooks → core}/scripts/tests/test-compact-devlog.sh +0 -0
  94. /package/{hooks → core}/scripts/tests/test-devlog-lock.sh +0 -0
  95. /package/{hooks → core}/scripts/tests/test-devlog-md.sh +0 -0
  96. /package/{hooks → core}/scripts/tests/test-devlog-path.sh +0 -0
  97. /package/{hooks → core}/scripts/tests/test-enforce-devlog-files.sh +0 -0
  98. /package/{hooks → core}/scripts/tests/test-enforce-devlog-handoff-order.sh +0 -0
  99. /package/{hooks → core}/scripts/tests/test-enforce-devlog-workspace.sh +0 -0
  100. /package/{hooks → core}/scripts/tests/test-enforce-devlog.sh +0 -0
  101. /package/{hooks → core}/scripts/tests/test-files-snapshot.sh +0 -0
  102. /package/{hooks → core}/scripts/tests/test-json-field.sh +0 -0
  103. /package/{hooks → core}/scripts/tests/test-keep-move.sh +0 -0
  104. /package/{hooks → core}/scripts/tests/test-kept-list.sh +0 -0
  105. /package/{hooks → core}/scripts/tests/test-lessons-append.sh +0 -0
  106. /package/{hooks → core}/scripts/tests/test-lessons-drift-set.sh +0 -0
  107. /package/{hooks → core}/scripts/tests/test-lessons-on-off.sh +0 -0
  108. /package/{hooks → core}/scripts/tests/test-lessons-read.sh +0 -0
  109. /package/{hooks → core}/scripts/tests/test-on-interrupt.sh +0 -0
  110. /package/{hooks → core}/scripts/tests/test-redact-prompt.sh +0 -0
  111. /package/{hooks → core}/scripts/tests/test-resume-devlog.sh +0 -0
  112. /package/{hooks → core}/scripts/tests/test-round-start.sh +0 -0
  113. /package/{hooks → core}/scripts/tests/test-segment-watch-set.sh +0 -0
  114. /package/{hooks → core}/scripts/tests/test-segment-watch.sh +0 -0
  115. /package/{hooks → core}/scripts/tests/test-start-pause-devlog.sh +0 -0
  116. /package/{hooks → core}/scripts/tests/test-status-span.sh +0 -0
  117. /package/{hooks → core}/scripts/tests/test-workspace-snapshot.sh +0 -0
@@ -0,0 +1,178 @@
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 { install } = require('./claude');
8
+
9
+ function tmpdir() {
10
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'devlog-tracker-claude-'));
11
+ }
12
+
13
+ function makeFakeRepo(dir) {
14
+ fs.mkdirSync(path.join(dir, 'claude'), { recursive: true });
15
+ fs.writeFileSync(
16
+ path.join(dir, 'claude', 'hooks.json'),
17
+ JSON.stringify({ hooks: { Stop: [{ hooks: [{ type: 'command', command: 'bash "${DEVLOG_TRACKER_ROOT}/core/scripts/enforce-devlog.sh"' }] }] } }, null, 2)
18
+ );
19
+ fs.mkdirSync(path.join(dir, 'commands'), { recursive: true });
20
+ fs.writeFileSync(path.join(dir, 'commands', 'start.md'), '---\ndescription: 啟動追蹤\n---\n\n步驟一。\n');
21
+ }
22
+
23
+ function makeVendorRoot(dir) {
24
+ // Real vendorRoot is targetDir/.devlog-tracker, which contains the marker in its path
25
+ const vendorRoot = path.join(dir, '.devlog-tracker');
26
+ fs.mkdirSync(path.join(vendorRoot, 'commands'), { recursive: true });
27
+ fs.writeFileSync(path.join(vendorRoot, 'commands', 'start.md'), '---\ndescription: 啟動追蹤\n---\n\n步驟一。\n');
28
+ return vendorRoot;
29
+ }
30
+
31
+ test('install merges hooks into .claude/settings.local.json without touching other keys', () => {
32
+ const repoRoot = tmpdir();
33
+ const vendorParent = tmpdir();
34
+ const targetDir = tmpdir();
35
+ const vendorRoot = makeVendorRoot(vendorParent);
36
+ makeFakeRepo(repoRoot);
37
+ fs.mkdirSync(path.join(targetDir, '.claude'), { recursive: true });
38
+ fs.writeFileSync(
39
+ path.join(targetDir, '.claude', 'settings.local.json'),
40
+ JSON.stringify({ permissions: { allow: ['Bash(ls:*)'] } }, null, 2)
41
+ );
42
+
43
+ install({ repoRoot, targetDir, vendorRoot });
44
+
45
+ const settings = JSON.parse(fs.readFileSync(path.join(targetDir, '.claude', 'settings.local.json'), 'utf8'));
46
+ assert.deepEqual(settings.permissions, { allow: ['Bash(ls:*)'] });
47
+ assert.equal(settings.hooks.Stop[0].hooks[0].command, `bash "${vendorRoot}/core/scripts/enforce-devlog.sh"`);
48
+ });
49
+
50
+ test('install writes .claude/skills/devlog-<name>/SKILL.md from vendored commands', () => {
51
+ const repoRoot = tmpdir();
52
+ const vendorParent = tmpdir();
53
+ const targetDir = tmpdir();
54
+ const vendorRoot = makeVendorRoot(vendorParent);
55
+ makeFakeRepo(repoRoot);
56
+
57
+ install({ repoRoot, targetDir, vendorRoot });
58
+
59
+ const skillPath = path.join(targetDir, '.claude', 'skills', 'devlog-start', 'SKILL.md');
60
+ assert.ok(fs.existsSync(skillPath));
61
+ assert.match(fs.readFileSync(skillPath, 'utf8'), /步驟一。/);
62
+ });
63
+
64
+ test('install upserts the devlog-tracker block into CLAUDE.md, leaving other content alone', () => {
65
+ const repoRoot = tmpdir();
66
+ const vendorParent = tmpdir();
67
+ const targetDir = tmpdir();
68
+ const vendorRoot = makeVendorRoot(vendorParent);
69
+ makeFakeRepo(repoRoot);
70
+ fs.writeFileSync(path.join(targetDir, 'CLAUDE.md'), '# My project\n\nSome existing notes.\n');
71
+
72
+ install({ repoRoot, targetDir, vendorRoot });
73
+
74
+ const content = fs.readFileSync(path.join(targetDir, 'CLAUDE.md'), 'utf8');
75
+ assert.match(content, /# My project/);
76
+ assert.match(content, /Some existing notes\./);
77
+ assert.match(content, /<!-- devlog-tracker:begin -->/);
78
+ assert.match(content, /<!-- devlog-tracker:end -->/);
79
+ assert.match(content, /devlog-tracker/);
80
+ });
81
+
82
+ test('re-running install is idempotent: no duplicate hook entries or skill directories', () => {
83
+ const repoRoot = tmpdir();
84
+ const vendorParent = tmpdir();
85
+ const targetDir = tmpdir();
86
+ const vendorRoot = makeVendorRoot(vendorParent);
87
+ makeFakeRepo(repoRoot);
88
+
89
+ install({ repoRoot, targetDir, vendorRoot });
90
+ const skillsAfterFirst = fs.readdirSync(path.join(targetDir, '.claude', 'skills'));
91
+ install({ repoRoot, targetDir, vendorRoot });
92
+ const skillsAfterSecond = fs.readdirSync(path.join(targetDir, '.claude', 'skills'));
93
+
94
+ const settings = JSON.parse(fs.readFileSync(path.join(targetDir, '.claude', 'settings.local.json'), 'utf8'));
95
+ assert.equal(settings.hooks.Stop.length, 1);
96
+ assert.deepEqual(skillsAfterFirst, skillsAfterSecond, 'skill directories should not duplicate');
97
+ });
98
+
99
+ test('re-running install twice on existing CLAUDE.md keeps exactly one marker pair', () => {
100
+ const repoRoot = tmpdir();
101
+ const vendorParent = tmpdir();
102
+ const targetDir = tmpdir();
103
+ const vendorRoot = makeVendorRoot(vendorParent);
104
+ makeFakeRepo(repoRoot);
105
+ fs.writeFileSync(path.join(targetDir, 'CLAUDE.md'), '# My project\n\nSome existing notes.\n');
106
+
107
+ install({ repoRoot, targetDir, vendorRoot });
108
+ install({ repoRoot, targetDir, vendorRoot });
109
+
110
+ const content = fs.readFileSync(path.join(targetDir, 'CLAUDE.md'), 'utf8');
111
+ const beginCount = (content.match(/<!-- devlog-tracker:begin -->/g) || []).length;
112
+ const endCount = (content.match(/<!-- devlog-tracker:end -->/g) || []).length;
113
+ assert.equal(beginCount, 1, 'Should have exactly one begin marker');
114
+ assert.equal(endCount, 1, 'Should have exactly one end marker');
115
+ assert.match(content, /# My project/);
116
+ assert.match(content, /Some existing notes\./);
117
+ });
118
+
119
+ test('install with real claude/hooks.json substitutes all placeholders and is idempotent', () => {
120
+ const repoRoot = path.join(__dirname, '..', '..');
121
+ const vendorParent = tmpdir();
122
+ const targetDir = tmpdir();
123
+ const vendorRoot = makeVendorRoot(vendorParent);
124
+
125
+ // First run
126
+ install({ repoRoot, targetDir, vendorRoot });
127
+
128
+ const settingsPath = path.join(targetDir, '.claude', 'settings.local.json');
129
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
130
+
131
+ // Collect all commands from the merged hooks
132
+ const commands = [];
133
+ for (const event of Object.keys(settings.hooks)) {
134
+ const entries = settings.hooks[event];
135
+ for (const entry of entries) {
136
+ if (entry.hooks) {
137
+ for (const hook of entry.hooks) {
138
+ if (hook.command) {
139
+ commands.push(hook.command);
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
145
+
146
+ // Verify no unsubstituted placeholders remain
147
+ for (const command of commands) {
148
+ assert.ok(!command.includes('${'), `Command should not contain unsubstituted placeholder: ${command}`);
149
+ }
150
+
151
+ // Verify every command contains the vendorRoot and /core/scripts/
152
+ for (const command of commands) {
153
+ assert.ok(command.includes(vendorRoot), `Command should contain vendorRoot path: ${command}`);
154
+ assert.ok(command.includes('/core/scripts/'), `Command should reference core/scripts: ${command}`);
155
+ }
156
+
157
+ // Now read the template to count expected entries per event
158
+ const templatePath = path.join(repoRoot, 'claude', 'hooks.json');
159
+ const template = JSON.parse(fs.readFileSync(templatePath, 'utf8'));
160
+
161
+ // Before second run, get the current entry counts
162
+ const entriesBeforeSecond = {};
163
+ for (const event of Object.keys(template.hooks)) {
164
+ entriesBeforeSecond[event] = settings.hooks[event] ? settings.hooks[event].length : 0;
165
+ }
166
+
167
+ // Second run
168
+ install({ repoRoot, targetDir, vendorRoot });
169
+
170
+ const settingsAfterSecond = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
171
+
172
+ // Verify no duplication: each event should have same count as template (1 entry each)
173
+ for (const event of Object.keys(template.hooks)) {
174
+ const expectedCount = template.hooks[event].length;
175
+ const actualCount = settingsAfterSecond.hooks[event] ? settingsAfterSecond.hooks[event].length : 0;
176
+ assert.equal(actualCount, expectedCount, `Event ${event} should have ${expectedCount} entry (template count), got ${actualCount}`);
177
+ }
178
+ });
@@ -3,21 +3,28 @@ const fs = require('fs');
3
3
  const path = require('path');
4
4
  const { mergeHooksTemplate } = require('../merge-hooks');
5
5
  const { upsertAgentsMd } = require('../agents-md');
6
+ const { generateSkillsFromCommands } = require('../skills-from-commands');
6
7
 
7
- function installPrompts({ targetDir, vendorRoot }) {
8
- const commandsDir = path.join(vendorRoot, 'commands');
8
+ // Codex 只從 ~/.codex/prompts/ 讀 custom prompts(且已棄用),不會讀專案內的
9
+ // .codex/prompts/;專案層級的指令要做成 .agents/skills/<name>/SKILL.md,以 $<name> 叫用。
10
+
11
+ // 0.25.0 曾把 prompts 寫到 .codex/prompts/devlog-*.md,Codex 讀不到;升級時清掉。
12
+ function removeLegacyPrompts(targetDir) {
9
13
  const promptsDir = path.join(targetDir, '.codex', 'prompts');
10
- fs.mkdirSync(promptsDir, { recursive: true });
11
- for (const file of fs.readdirSync(commandsDir)) {
12
- if (!file.endsWith('.md')) continue;
13
- const command = path.basename(file, '.md');
14
- const source = path.join(commandsDir, file);
15
- const destination = path.join(promptsDir, `devlog-${command}.md`);
16
- fs.writeFileSync(
17
- destination,
18
- `${fs.readFileSync(source, 'utf8').trimEnd()}\n\n使用者提供的額外參數:$ARGUMENTS\n`
19
- );
14
+ if (!fs.existsSync(promptsDir)) return;
15
+ for (const file of fs.readdirSync(promptsDir)) {
16
+ if (/^devlog-.*\.md$/.test(file)) fs.rmSync(path.join(promptsDir, file));
20
17
  }
18
+ if (fs.readdirSync(promptsDir).length === 0) fs.rmdirSync(promptsDir);
19
+ }
20
+
21
+ function installSkills({ targetDir, vendorRoot }) {
22
+ generateSkillsFromCommands({
23
+ commandsDir: path.join(vendorRoot, 'commands'),
24
+ targetSkillsDir: path.join(targetDir, '.agents', 'skills'),
25
+ namePrefix: 'devlog',
26
+ });
27
+ removeLegacyPrompts(targetDir);
21
28
  }
22
29
 
23
30
  function install({ repoRoot, targetDir, vendorRoot }) {
@@ -26,8 +33,8 @@ function install({ repoRoot, targetDir, vendorRoot }) {
26
33
  targetPath: path.join(targetDir, '.codex', 'hooks.json'),
27
34
  vendorRoot,
28
35
  });
29
- installPrompts({ targetDir, vendorRoot });
36
+ installSkills({ targetDir, vendorRoot });
30
37
  upsertAgentsMd(targetDir);
31
38
  }
32
39
 
33
- module.exports = { install, installPrompts };
40
+ module.exports = { install, installSkills };
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ function parseCommand(text) {
6
+ const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(text);
7
+ if (!match) return { description: '', body: text };
8
+ const description = /^description:\s*(.*)$/m.exec(match[1]);
9
+ return { description: description ? description[1].trim() : '', body: match[2] };
10
+ }
11
+
12
+ // 把 commandsDir 底下每個 *.md 轉成 targetSkillsDir/<namePrefix>-<檔名>/SKILL.md。
13
+ // 回傳寫入的 SKILL.md 絕對路徑陣列。
14
+ function generateSkillsFromCommands({ commandsDir, targetSkillsDir, namePrefix }) {
15
+ const written = [];
16
+ for (const file of fs.readdirSync(commandsDir)) {
17
+ if (!file.endsWith('.md')) continue;
18
+ const name = `${namePrefix}-${path.basename(file, '.md')}`;
19
+ const { description, body } = parseCommand(fs.readFileSync(path.join(commandsDir, file), 'utf8'));
20
+ const skillDir = path.join(targetSkillsDir, name);
21
+ fs.mkdirSync(skillDir, { recursive: true });
22
+ const skillPath = path.join(skillDir, 'SKILL.md');
23
+ fs.writeFileSync(
24
+ skillPath,
25
+ `---\nname: ${name}\ndescription: ${JSON.stringify(description)}\n---\n\n${body.trim()}\n\n使用者提供的額外參數:請看觸發這個 skill 的使用者訊息。\n`
26
+ );
27
+ written.push(skillPath);
28
+ }
29
+ return written;
30
+ }
31
+
32
+ module.exports = { generateSkillsFromCommands, parseCommand };
@@ -0,0 +1,51 @@
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 { generateSkillsFromCommands } = require('./skills-from-commands');
8
+
9
+ function tmpdir() {
10
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'devlog-tracker-skills-'));
11
+ }
12
+
13
+ test('generateSkillsFromCommands writes one SKILL.md per command file, using namePrefix', () => {
14
+ const commandsDir = tmpdir();
15
+ const targetSkillsDir = tmpdir();
16
+ fs.writeFileSync(
17
+ path.join(commandsDir, 'start.md'),
18
+ '---\ndescription: 啟動追蹤\n---\n\n請執行以下步驟:\n1. 做事\n'
19
+ );
20
+
21
+ const written = generateSkillsFromCommands({ commandsDir, targetSkillsDir, namePrefix: 'devlog' });
22
+
23
+ assert.equal(written.length, 1);
24
+ const skillPath = path.join(targetSkillsDir, 'devlog-start', 'SKILL.md');
25
+ assert.equal(written[0], skillPath);
26
+ const content = fs.readFileSync(skillPath, 'utf8');
27
+ assert.match(content, /^---\nname: devlog-start\ndescription: "啟動追蹤"\n---\n\n/);
28
+ assert.match(content, /請執行以下步驟:/);
29
+ assert.match(content, /使用者提供的額外參數:請看觸發這個 skill 的使用者訊息。\n$/);
30
+ });
31
+
32
+ test('generateSkillsFromCommands ignores non-.md files', () => {
33
+ const commandsDir = tmpdir();
34
+ const targetSkillsDir = tmpdir();
35
+ fs.writeFileSync(path.join(commandsDir, 'start.md'), '---\ndescription: x\n---\nbody');
36
+ fs.writeFileSync(path.join(commandsDir, 'README.txt'), 'not a command');
37
+
38
+ const written = generateSkillsFromCommands({ commandsDir, targetSkillsDir, namePrefix: 'devlog' });
39
+
40
+ assert.equal(written.length, 1);
41
+ });
42
+
43
+ test('generateSkillsFromCommands uses a different prefix and target dir independently', () => {
44
+ const commandsDir = tmpdir();
45
+ const targetSkillsDir = tmpdir();
46
+ fs.writeFileSync(path.join(commandsDir, 'status.md'), '---\ndescription: 查狀態\n---\nbody');
47
+
48
+ generateSkillsFromCommands({ commandsDir, targetSkillsDir, namePrefix: 'claude-devlog' });
49
+
50
+ assert.ok(fs.existsSync(path.join(targetSkillsDir, 'claude-devlog-status', 'SKILL.md')));
51
+ });
package/cli/vendor.js CHANGED
@@ -2,7 +2,11 @@
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
 
5
- const VENDOR_ENTRIES = ['hooks/scripts', 'codex/hooks', 'cursor/hooks', 'skills', 'commands'];
5
+ const VENDOR_ENTRIES = ['core/scripts', 'claude/hooks.json', 'codex/hooks', 'cursor/hooks', 'skills', 'commands'];
6
+
7
+ // 0.25.x及更早版本 vendor 到 .devlog-tracker/hooks/scripts/;升級後改放
8
+ // core/scripts/,這裡清掉舊目錄避免孤兒檔案殘留。
9
+ const STALE_ENTRIES = ['hooks'];
6
10
 
7
11
  function copyRecursive(src, dest) {
8
12
  const stat = fs.statSync(src);
@@ -29,6 +33,9 @@ function vendor({ repoRoot, targetDir, version }) {
29
33
  if (!fs.existsSync(src)) continue;
30
34
  copyRecursive(src, path.join(vendorRoot, entry));
31
35
  }
36
+ for (const entry of STALE_ENTRIES) {
37
+ fs.rmSync(path.join(vendorRoot, entry), { recursive: true, force: true });
38
+ }
32
39
  fs.writeFileSync(path.join(vendorRoot, 'VERSION'), `${version}\n`);
33
40
  fs.writeFileSync(path.join(vendorRoot, 'env.sh'), envShContent(vendorRoot));
34
41
  return vendorRoot;
@@ -11,8 +11,10 @@ function tmpdir() {
11
11
  }
12
12
 
13
13
  function makeFakeRepo(dir) {
14
- fs.mkdirSync(path.join(dir, 'hooks', 'scripts'), { recursive: true });
15
- fs.writeFileSync(path.join(dir, 'hooks', 'scripts', 'enforce-devlog.sh'), '#!/usr/bin/env bash\necho hi\n');
14
+ fs.mkdirSync(path.join(dir, 'core', 'scripts'), { recursive: true });
15
+ fs.writeFileSync(path.join(dir, 'core', 'scripts', 'enforce-devlog.sh'), '#!/usr/bin/env bash\necho hi\n');
16
+ fs.mkdirSync(path.join(dir, 'claude'), { recursive: true });
17
+ fs.writeFileSync(path.join(dir, 'claude', 'hooks.json'), '{"hooks":{}}\n');
16
18
  fs.mkdirSync(path.join(dir, 'codex', 'hooks'), { recursive: true });
17
19
  fs.writeFileSync(path.join(dir, 'codex', 'hooks', 'on-stop.sh'), '#!/usr/bin/env bash\necho codex\n');
18
20
  fs.mkdirSync(path.join(dir, 'cursor', 'hooks'), { recursive: true });
@@ -31,7 +33,8 @@ test('vendor copies the expected directories and stamps VERSION + env.sh', () =>
31
33
  const vendorRoot = vendor({ repoRoot, targetDir, version: '0.21.0' });
32
34
 
33
35
  assert.equal(vendorRoot, path.join(targetDir, '.devlog-tracker'));
34
- assert.ok(fs.existsSync(path.join(vendorRoot, 'hooks', 'scripts', 'enforce-devlog.sh')));
36
+ assert.ok(fs.existsSync(path.join(vendorRoot, 'core', 'scripts', 'enforce-devlog.sh')));
37
+ assert.ok(fs.existsSync(path.join(vendorRoot, 'claude', 'hooks.json')));
35
38
  assert.ok(fs.existsSync(path.join(vendorRoot, 'codex', 'hooks', 'on-stop.sh')));
36
39
  assert.ok(fs.existsSync(path.join(vendorRoot, 'cursor', 'hooks', 'on-stop.sh')));
37
40
  assert.ok(fs.existsSync(path.join(vendorRoot, 'skills', 'devlog-tracker', 'SKILL.md')));
@@ -43,14 +46,14 @@ test('vendor copies the expected directories and stamps VERSION + env.sh', () =>
43
46
  );
44
47
  });
45
48
 
46
- test('vendor preserves the ../../hooks/scripts relative layout codex/cursor wrappers rely on', () => {
49
+ test('vendor preserves the ../../core/scripts relative layout codex/cursor wrappers rely on', () => {
47
50
  const repoRoot = tmpdir();
48
51
  const targetDir = tmpdir();
49
52
  makeFakeRepo(repoRoot);
50
53
 
51
54
  const vendorRoot = vendor({ repoRoot, targetDir, version: '0.21.0' });
52
55
 
53
- const resolvedFromCodex = path.join(vendorRoot, 'codex', 'hooks', '..', '..', 'hooks', 'scripts', 'enforce-devlog.sh');
56
+ const resolvedFromCodex = path.join(vendorRoot, 'codex', 'hooks', '..', '..', 'core', 'scripts', 'enforce-devlog.sh');
54
57
  assert.ok(fs.existsSync(resolvedFromCodex));
55
58
  });
56
59
 
@@ -82,3 +85,19 @@ test('env.sh escapes shell metacharacters in the vendor path', () => {
82
85
  );
83
86
  assert.ok(fs.readFileSync(path.join(vendorRoot, 'env.sh'), 'utf8').includes('a\\$(x)\\"b\\`c\\\\d'));
84
87
  });
88
+
89
+ test('re-running vendor after upgrade removes the stale hooks/scripts directory', () => {
90
+ const repoRoot = tmpdir();
91
+ const targetDir = tmpdir();
92
+ makeFakeRepo(repoRoot);
93
+
94
+ // Simulate a pre-upgrade install that used the old hooks/scripts layout.
95
+ const vendorRoot = path.join(targetDir, '.devlog-tracker');
96
+ fs.mkdirSync(path.join(vendorRoot, 'hooks', 'scripts'), { recursive: true });
97
+ fs.writeFileSync(path.join(vendorRoot, 'hooks', 'scripts', 'enforce-devlog.sh'), '#!/usr/bin/env bash\necho old\n');
98
+
99
+ vendor({ repoRoot, targetDir, version: '0.26.0' });
100
+
101
+ assert.ok(!fs.existsSync(path.join(vendorRoot, 'hooks')), 'stale hooks/ directory should be removed');
102
+ assert.ok(fs.existsSync(path.join(vendorRoot, 'core', 'scripts', 'enforce-devlog.sh')));
103
+ });
@@ -3,16 +3,16 @@
3
3
  # contract as Claude Code (0 = allow, 2 = block), so this only needs the
4
4
  # cwd->CLAUDE_PROJECT_DIR translation; segment-watch.sh's stderr message
5
5
  # propagates unchanged. Fail-open like the Cursor adapter: only an exit code of
6
- # exactly 2 blocks; anything else (including a missing hooks/scripts dir or a
6
+ # exactly 2 blocks; anything else (including a missing core/scripts dir or a
7
7
  # crash) allows.
8
8
  set -uo pipefail
9
9
  _src="${BASH_SOURCE[0]}"
10
10
  SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
11
- PLUGIN_SCRIPTS="$(cd "$SCRIPT_DIR/../../hooks/scripts" 2>/dev/null && pwd)"
11
+ PLUGIN_SCRIPTS="$(cd "$SCRIPT_DIR/../../core/scripts" 2>/dev/null && pwd)"
12
12
  [ -d "$PLUGIN_SCRIPTS" ] || exit 0
13
13
  INPUT="$(cat 2>/dev/null || true)"
14
14
  ROOT="$(printf '%s' "$INPUT" | bash "$SCRIPT_DIR/project-dir.sh")"
15
- export CLAUDE_PROJECT_DIR="$ROOT"
15
+ export DEVLOG_PROJECT_DIR="$ROOT"
16
16
  printf '%s' "$INPUT" | bash "$PLUGIN_SCRIPTS/segment-watch.sh"
17
17
  RC=$?
18
18
  [ "$RC" -eq 2 ] && exit 2
@@ -7,10 +7,10 @@
7
7
  set -uo pipefail
8
8
  _src="${BASH_SOURCE[0]}"
9
9
  SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
10
- PLUGIN_SCRIPTS="$(cd "$SCRIPT_DIR/../../hooks/scripts" && pwd)"
10
+ PLUGIN_SCRIPTS="$(cd "$SCRIPT_DIR/../../core/scripts" && pwd)"
11
11
  INPUT="$(cat 2>/dev/null || true)"
12
12
  ROOT="$(printf '%s' "$INPUT" | bash "$SCRIPT_DIR/project-dir.sh")"
13
- export CLAUDE_PROJECT_DIR="$ROOT"
13
+ export DEVLOG_PROJECT_DIR="$ROOT"
14
14
 
15
15
  REASON=""
16
16
  if command -v jq >/dev/null 2>&1; then
@@ -13,10 +13,10 @@
13
13
  set -uo pipefail
14
14
  _src="${BASH_SOURCE[0]}"
15
15
  SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
16
- PLUGIN_SCRIPTS="$(cd "$SCRIPT_DIR/../../hooks/scripts" && pwd)"
16
+ PLUGIN_SCRIPTS="$(cd "$SCRIPT_DIR/../../core/scripts" && pwd)"
17
17
  INPUT="$(cat 2>/dev/null || true)"
18
18
  ROOT="$(printf '%s' "$INPUT" | bash "$SCRIPT_DIR/project-dir.sh")"
19
- export CLAUDE_PROJECT_DIR="$ROOT"
19
+ export DEVLOG_PROJECT_DIR="$ROOT"
20
20
 
21
21
  SRC=""
22
22
  if command -v jq >/dev/null 2>&1; then
@@ -3,15 +3,15 @@
3
3
  # reads "stop_hook_active", which is Claude Code's own standard field name
4
4
  # and not one of the documented Codex naming gaps, so it is forwarded as-is.
5
5
  # Fail-open like the Cursor adapter: only an exit code of exactly 2 blocks;
6
- # anything else (including a missing hooks/scripts dir or a crash) allows.
6
+ # anything else (including a missing core/scripts dir or a crash) allows.
7
7
  set -uo pipefail
8
8
  _src="${BASH_SOURCE[0]}"
9
9
  SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
10
- PLUGIN_SCRIPTS="$(cd "$SCRIPT_DIR/../../hooks/scripts" 2>/dev/null && pwd)"
10
+ PLUGIN_SCRIPTS="$(cd "$SCRIPT_DIR/../../core/scripts" 2>/dev/null && pwd)"
11
11
  [ -d "$PLUGIN_SCRIPTS" ] || exit 0
12
12
  INPUT="$(cat 2>/dev/null || true)"
13
13
  ROOT="$(printf '%s' "$INPUT" | bash "$SCRIPT_DIR/project-dir.sh")"
14
- export CLAUDE_PROJECT_DIR="$ROOT"
14
+ export DEVLOG_PROJECT_DIR="$ROOT"
15
15
  printf '%s' "$INPUT" | bash "$PLUGIN_SCRIPTS/enforce-devlog.sh"
16
16
  RC=$?
17
17
  [ "$RC" -eq 2 ] && exit 2
@@ -7,9 +7,9 @@
7
7
  set -uo pipefail
8
8
  _src="${BASH_SOURCE[0]}"
9
9
  SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
10
- PLUGIN_SCRIPTS="$(cd "$SCRIPT_DIR/../../hooks/scripts" && pwd)"
10
+ PLUGIN_SCRIPTS="$(cd "$SCRIPT_DIR/../../core/scripts" && pwd)"
11
11
  INPUT="$(cat 2>/dev/null || true)"
12
12
  ROOT="$(printf '%s' "$INPUT" | bash "$SCRIPT_DIR/project-dir.sh")"
13
- export CLAUDE_PROJECT_DIR="$ROOT"
13
+ export DEVLOG_PROJECT_DIR="$ROOT"
14
14
  printf '%s' "$INPUT" | bash "$PLUGIN_SCRIPTS/round-start.sh"
15
15
  exit 0
@@ -77,7 +77,7 @@ else
77
77
  echo "FAIL: sessionEnd 'why' fallback"; FAIL=1
78
78
  fi
79
79
 
80
- # fail-open: when ../../hooks/scripts is missing, preTool/stop wrappers must allow (exit 0)
80
+ # fail-open: when ../../core/scripts is missing, preTool/stop wrappers must allow (exit 0)
81
81
  ORPHAN="$TMP/orphan"
82
82
  mkdir -p "$ORPHAN/codex/hooks"
83
83
  cp "$SCRIPT_DIR/on-pre-tool.sh" "$SCRIPT_DIR/on-stop.sh" "$SCRIPT_DIR/project-dir.sh" "$ORPHAN/codex/hooks/"
@@ -86,7 +86,7 @@ for W in on-pre-tool on-stop; do
86
86
  printf '{"cwd":"%s"}' "$ORPHAN" | bash "$ORPHAN/codex/hooks/$W.sh" >/dev/null 2>/dev/null
87
87
  RC=$?
88
88
  set -e
89
- if [ "$RC" -eq 0 ]; then echo "PASS: $W fails open without hooks/scripts (exit 0)"; else echo "FAIL: $W orphan exit [$RC]"; FAIL=1; fi
89
+ if [ "$RC" -eq 0 ]; then echo "PASS: $W fails open without core/scripts (exit 0)"; else echo "FAIL: $W orphan exit [$RC]"; FAIL=1; fi
90
90
  done
91
91
 
92
92
  # sessionStart: payload with neither "how" nor "source" falls back to startup and still injects
@@ -6,8 +6,8 @@ description: 調整 Checkpoint Mode 的沉默門檻——累積多少輪沒寫 #
6
6
  跑:
7
7
 
8
8
  ```bash
9
- PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${DEVLOG_TRACKER_ROOT:-}}"
10
- CLAUDE_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/hooks/scripts/checkpoint-set.sh" <rounds>
9
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
10
+ DEVLOG_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/core/scripts/checkpoint-set.sh" <rounds>
11
11
  ```
12
12
 
13
13
  不要自己手改 `.devlog/.checkpoint-state`。
package/commands/clean.md CHANGED
@@ -30,11 +30,11 @@ description: 無條件清空 .devlog/devlog.md(含專案摘要與所有 Round
30
30
  ## 3. 跑清空腳本
31
31
 
32
32
  ```bash
33
- 先決定 plugin 根目錄(有 `CLAUDE_PLUGIN_ROOT` 用它;否則用 `DEVLOG_TRACKER_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
33
+ 先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
34
34
 
35
35
  ```bash
36
- PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${DEVLOG_TRACKER_ROOT:-}}"
37
- CLAUDE_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/hooks/scripts/clean-devlog.sh" --confirmed
36
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
37
+ DEVLOG_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/core/scripts/clean-devlog.sh" --confirmed
38
38
  ```
39
39
  ```
40
40
 
@@ -13,10 +13,10 @@ description: 把 .devlog/devlog.md 裡已完成且較舊的紀錄搬到 devlog.a
13
13
  - 所有 `## Checkpoint` 區塊,永遠留在 devlog.md、不搬到 archive——它們是
14
14
  checkpoint 機制存在的目的:翻閱時的摘要路標,搬走就失去了作用
15
15
  4. 跑(不要自己搬檔):
16
- 先決定 plugin 根目錄(有 `CLAUDE_PLUGIN_ROOT` 用它;否則用 `DEVLOG_TRACKER_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
16
+ 先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
17
17
  ```bash
18
- PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${DEVLOG_TRACKER_ROOT:-}}"
19
- CLAUDE_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/hooks/scripts/compact-devlog.sh"
18
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
19
+ DEVLOG_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/core/scripts/compact-devlog.sh"
20
20
  ```
21
21
  檔案不存在時腳本 exit 1:告知沒有東西可壓縮。
22
22
  5. 用 stdout 的 `MOVED` / `REMAINING` / `ARCHIVE` 回報一句話。
@@ -9,12 +9,12 @@ description: 讀取 .devlog/devlog.md,核對最後一輪 Handoff 的工作區
9
9
  3. 讀最近的 Round(不夠再往前讀;有 `## Checkpoint` 就一併看最後一個)。不要讀 `devlog.archive.md` 或 `devlog.<name>.md`,除非 Handoff 下一步明確指向它們。
10
10
  4. 依**最後一個歷史 Round**(不是這一輪 continue 自己的 skeleton)的 Status 行動。`DONE`:告訴使用者上一題已經結束,等新需求。不要核對、不要自己找下一件工作。
11
11
  5. `IN_PROGRESS`、`INTERRUPTED`、`BLOCKED`:先核對,再行動。不要先問「上次做到哪」。不要改歷史 Round。
12
- 1. 跑與寫「工作區」相同的生產者,不要自己跑 git、不要手編成 SKILL 的七種格式。先決定 plugin 根目錄(有 `CLAUDE_PLUGIN_ROOT` 用它;否則用 `DEVLOG_TRACKER_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
12
+ 1. 跑與寫「工作區」相同的生產者,不要自己跑 git、不要手編成 SKILL 的七種格式。先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
13
13
  ```bash
14
- PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${DEVLOG_TRACKER_ROOT:-}}"
15
- CLAUDE_PROJECT_DIR="<步驟 1 記下的專案根目錄絕對路徑,不要用 $(pwd) 重新推>" bash "${PLUGIN_ROOT}/hooks/scripts/workspace-snapshot.sh"
14
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
15
+ DEVLOG_PROJECT_DIR="<步驟 1 記下的專案根目錄絕對路徑,不要用 $(pwd) 重新推>" bash "${PLUGIN_ROOT}/core/scripts/workspace-snapshot.sh"
16
16
  ```
17
- 把 `CLAUDE_PROJECT_DIR` 換成實際的絕對路徑字串再執行,不要真的呼叫 `pwd`。stdout 就是實際快照(1 行或 2 行)。腳本檔找不到時才退回 `skills/devlog-tracker/SKILL.md` `#### 工作區` 的七種格式手編。不要重跑測試套件,除非「下一步」本身就是跑測試。
17
+ 把 `DEVLOG_PROJECT_DIR` 換成實際的絕對路徑字串再執行,不要真的呼叫 `pwd`。stdout 就是實際快照(1 行或 2 行)。腳本檔找不到時才退回 `skills/devlog-tracker/SKILL.md` `#### 工作區` 的七種格式手編。不要重跑測試套件,除非「下一步」本身就是跑測試。
18
18
  2. 把編成的實際快照對照該歷史 Round 的 `#### 工作區` 正文。
19
19
  - 沒有這一節(舊 Round、`INTERRUPTED` stub):沒有宣稱可對,不算「不符」——不用寫 `### 段落`,直接以剛才編成的實際快照為準。
20
20
  - 有這一節但跟編成的實際快照不符:在**這一輪**先追加一段 `### 段落`,寫宣稱 vs 實際(用剛才腳本的 stdout 當實際快照)。
package/commands/keep.md CHANGED
@@ -75,11 +75,11 @@ description: 掃描整份 devlog.md,把值得留名的主題段落一次分別
75
75
 
76
76
  把步驟 4 驗證通過的段落,依 `from` 由小到大排序。對每一段依序跑(一段一次,不要自己搬檔,也不要一次塞多段給腳本):
77
77
 
78
- 先決定 plugin 根目錄(有 `CLAUDE_PLUGIN_ROOT` 用它;否則用 `DEVLOG_TRACKER_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
78
+ 先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
79
79
 
80
80
  ```bash
81
- PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${DEVLOG_TRACKER_ROOT:-}}"
82
- CLAUDE_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/hooks/scripts/keep-move.sh" \
81
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
82
+ DEVLOG_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/core/scripts/keep-move.sh" \
83
83
  --from <from> --to <to> --name "<name>" --desc "<這段在做什麼的一句話>"
84
84
  ```
85
85
 
@@ -5,8 +5,8 @@ description: 調整 Lessons Mode「工作區漂移重複發生」機制性提醒
5
5
  取得使用者要設定的次數(正整數);沒帶就先問,不要用預設值硬猜。跑:
6
6
 
7
7
  ```bash
8
- PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${DEVLOG_TRACKER_ROOT:-}}"
9
- CLAUDE_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/hooks/scripts/lessons-drift-set.sh" <次數>
8
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
9
+ DEVLOG_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/core/scripts/lessons-drift-set.sh" <次數>
10
10
  ```
11
11
 
12
12
  不要自己手改 `.devlog/.lessons-drift-state`。
@@ -4,10 +4,10 @@ description: 關閉 Lessons Mode。不會刪除任何已寫的 devlog.lessons.*.
4
4
 
5
5
  請執行:
6
6
 
7
- 1. 先決定 plugin 根目錄(有 `CLAUDE_PLUGIN_ROOT` 用它;否則用 `DEVLOG_TRACKER_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
7
+ 1. 先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
8
8
  ```bash
9
- PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${DEVLOG_TRACKER_ROOT:-}}"
10
- CLAUDE_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/hooks/scripts/lessons-off.sh"
9
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
10
+ DEVLOG_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/core/scripts/lessons-off.sh"
11
11
  ```
12
12
  不要自己刪 `.lessons-enabled`。
13
13
  2. stdout 是 `NOT_ENABLED`:告知 Lessons Mode 本來就沒開,結束。
@@ -4,10 +4,10 @@ description: 開啟 Lessons Mode(開發歷程教訓,預設關閉)。隸屬
4
4
 
5
5
  請執行:
6
6
 
7
- 1. 先決定 plugin 根目錄(有 `CLAUDE_PLUGIN_ROOT` 用它;否則用 `DEVLOG_TRACKER_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
7
+ 1. 先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
8
8
  ```bash
9
- PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${DEVLOG_TRACKER_ROOT:-}}"
10
- CLAUDE_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/hooks/scripts/lessons-on.sh"
9
+ PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
10
+ DEVLOG_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/core/scripts/lessons-on.sh"
11
11
  ```
12
12
  不要自己用手建 `.lessons-enabled`。
13
13
  2. stdout 是 `NOT_ENABLED`:告知這個專案還沒下過 `/devlog-tracker:start`,Lessons Mode 隸屬主開關,沒有 Round/Status 紀錄可判斷「BLOCKED→解開」,請先 `/devlog-tracker:start` 再開這個。