devlog-tracker 0.25.1 → 0.29.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -151
- package/README.zh-TW.md +235 -0
- package/claude/hooks.json +75 -0
- package/cli/agents-md.js +15 -8
- package/cli/agents-md.test.js +21 -1
- package/cli/init.js +6 -4
- package/cli/init.test.js +5 -4
- package/cli/merge-hooks.js +5 -2
- package/cli/merge-hooks.test.js +52 -0
- package/cli/platforms/claude.js +55 -0
- package/cli/platforms/claude.test.js +178 -0
- package/cli/platforms/codex.js +6 -18
- package/cli/skills-from-commands.js +32 -0
- package/cli/skills-from-commands.test.js +51 -0
- package/cli/vendor.js +8 -1
- package/cli/vendor.test.js +24 -5
- package/codex/hooks/on-pre-tool.sh +3 -3
- package/codex/hooks/on-session-end.sh +2 -2
- package/codex/hooks/on-session-start.sh +2 -2
- package/codex/hooks/on-stop.sh +3 -3
- package/codex/hooks/on-user-prompt-submit.sh +2 -2
- package/codex/hooks/test-adapters.sh +2 -2
- package/commands/checkpoint.md +2 -2
- package/commands/clean.md +3 -3
- package/commands/compact.md +3 -3
- package/commands/continue.md +4 -4
- package/commands/keep.md +21 -5
- package/commands/lessons-drift.md +4 -4
- package/commands/lessons-off.md +3 -3
- package/commands/lessons-on.md +3 -3
- package/commands/lessons.md +3 -3
- package/commands/overview.md +3 -3
- package/commands/pause.md +3 -3
- package/commands/resume.md +3 -3
- package/commands/search.md +22 -0
- package/commands/segment-watch.md +3 -3
- package/commands/span.md +2 -2
- package/commands/start.md +3 -3
- package/commands/status.md +5 -4
- package/{hooks → core}/scripts/await-open.sh +1 -1
- package/{hooks → core}/scripts/checkpoint-set.sh +1 -1
- package/{hooks → core}/scripts/clean-devlog.sh +2 -1
- package/{hooks → core}/scripts/close-open-round.sh +1 -1
- package/{hooks → core}/scripts/compact-devlog.sh +1 -1
- package/{hooks → core}/scripts/devlog-lock.sh +17 -1
- package/{hooks → core}/scripts/devlog-path.sh +15 -8
- package/{hooks → core}/scripts/enforce-devlog.sh +26 -1
- package/core/scripts/handoff-file.sh +106 -0
- package/{hooks → core}/scripts/keep-move.sh +1 -1
- package/{hooks → core}/scripts/kept-list.sh +1 -1
- package/core/scripts/lessons-advisory-state.sh +51 -0
- package/{hooks → core}/scripts/lessons-append.sh +18 -2
- package/core/scripts/lessons-drift-set.sh +46 -0
- package/{hooks → core}/scripts/lessons-off.sh +1 -1
- package/{hooks → core}/scripts/lessons-on.sh +9 -4
- package/{hooks → core}/scripts/lessons-read.sh +1 -1
- package/{hooks → core}/scripts/on-tool-failure.sh +1 -1
- package/{hooks → core}/scripts/pause-devlog.sh +1 -1
- package/core/scripts/project-dir.sh +19 -0
- package/{hooks → core}/scripts/resume-devlog.sh +1 -1
- package/{hooks → core}/scripts/round-start.sh +52 -17
- package/{hooks → core}/scripts/run-tests.sh +1 -1
- package/core/scripts/search-devlog.sh +63 -0
- package/{hooks → core}/scripts/segment-watch-set.sh +1 -1
- package/{hooks → core}/scripts/segment-watch.sh +1 -1
- package/{hooks → core}/scripts/session-start-devlog.sh +8 -1
- package/{hooks → core}/scripts/span-close.sh +1 -1
- package/{hooks → core}/scripts/span-open.sh +1 -1
- package/{hooks → core}/scripts/start-devlog.sh +1 -1
- package/{hooks → core}/scripts/status-devlog.sh +5 -5
- package/{hooks → core}/scripts/tests/test-clean-devlog.sh +12 -0
- package/core/scripts/tests/test-cli-init-e2e.sh +68 -0
- package/core/scripts/tests/test-devlog-lock.sh +76 -0
- package/{hooks → core}/scripts/tests/test-devlog-path.sh +44 -0
- package/{hooks → core}/scripts/tests/test-enforce-devlog-files.sh +11 -0
- package/core/scripts/tests/test-enforce-devlog-session-handoff.sh +178 -0
- package/{hooks → core}/scripts/tests/test-enforce-devlog-workspace.sh +35 -0
- package/{hooks → core}/scripts/tests/test-enforce-devlog.sh +174 -0
- package/core/scripts/tests/test-handoff-file.sh +98 -0
- package/core/scripts/tests/test-lessons-advisory-state.sh +55 -0
- package/{hooks → core}/scripts/tests/test-lessons-append.sh +18 -0
- package/{hooks → core}/scripts/tests/test-lessons-drift-set.sh +14 -5
- package/{hooks → core}/scripts/tests/test-lessons-on-off.sh +16 -6
- package/core/scripts/tests/test-project-dir.sh +30 -0
- package/{hooks → core}/scripts/tests/test-round-start.sh +247 -13
- package/core/scripts/tests/test-search-devlog.sh +126 -0
- package/{hooks → core}/scripts/tests/test-session-start-devlog.sh +61 -2
- package/{hooks → core}/scripts/tests/test-status-span.sh +3 -3
- package/{hooks → core}/scripts/workspace-snapshot.sh +2 -2
- package/cursor/hooks/on-pre-tool.sh +2 -2
- package/cursor/hooks/on-session-end.sh +2 -2
- package/cursor/hooks/on-session-start.sh +2 -2
- package/cursor/hooks/on-stop.sh +2 -2
- package/cursor/hooks/on-submit-prompt.sh +2 -2
- package/cursor/hooks/on-tool-failure.sh +2 -2
- package/package.json +5 -4
- package/skills/devlog-tracker/SKILL.md +90 -22
- package/skills/devlog-tracker/references/checkpoint-mode.md +21 -6
- package/skills/devlog-tracker/references/contract.md +83 -0
- package/skills/devlog-tracker/references/lessons-mode.md +33 -9
- package/skills/devlog-tracker/references/reply-fold.md +1 -1
- package/hooks/scripts/lessons-drift-set.sh +0 -42
- package/hooks/scripts/tests/test-cli-init-e2e.sh +0 -38
- package/hooks/scripts/tests/test-devlog-lock.sh +0 -37
- /package/{hooks → core}/scripts/detect-pending-question.sh +0 -0
- /package/{hooks → core}/scripts/devlog-md.sh +0 -0
- /package/{hooks → core}/scripts/files-snapshot.sh +0 -0
- /package/{hooks → core}/scripts/json-field.sh +0 -0
- /package/{hooks → core}/scripts/on-session-end.sh +0 -0
- /package/{hooks → core}/scripts/on-stop-failure.sh +0 -0
- /package/{hooks → core}/scripts/redact-prompt.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-await-open.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-branch-scoped-integration.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-checkpoint-set.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-close-open-round.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-compact-devlog.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-devlog-md.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-enforce-devlog-handoff-order.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-files-snapshot.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-json-field.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-keep-move.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-kept-list.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-lessons-read.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-on-interrupt.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-redact-prompt.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-resume-devlog.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-segment-watch-set.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-segment-watch.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-start-pause-devlog.sh +0 -0
- /package/{hooks → core}/scripts/tests/test-workspace-snapshot.sh +0 -0
|
@@ -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
|
+
});
|
package/cli/platforms/codex.js
CHANGED
|
@@ -3,15 +3,10 @@ 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
8
|
// Codex 只從 ~/.codex/prompts/ 讀 custom prompts(且已棄用),不會讀專案內的
|
|
8
9
|
// .codex/prompts/;專案層級的指令要做成 .agents/skills/<name>/SKILL.md,以 $<name> 叫用。
|
|
9
|
-
function parseCommand(text) {
|
|
10
|
-
const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(text);
|
|
11
|
-
if (!match) return { description: '', body: text };
|
|
12
|
-
const description = /^description:\s*(.*)$/m.exec(match[1]);
|
|
13
|
-
return { description: description ? description[1].trim() : '', body: match[2] };
|
|
14
|
-
}
|
|
15
10
|
|
|
16
11
|
// 0.25.0 曾把 prompts 寫到 .codex/prompts/devlog-*.md,Codex 讀不到;升級時清掉。
|
|
17
12
|
function removeLegacyPrompts(targetDir) {
|
|
@@ -24,18 +19,11 @@ function removeLegacyPrompts(targetDir) {
|
|
|
24
19
|
}
|
|
25
20
|
|
|
26
21
|
function installSkills({ targetDir, vendorRoot }) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const skillDir = path.join(targetDir, '.agents', 'skills', name);
|
|
33
|
-
fs.mkdirSync(skillDir, { recursive: true });
|
|
34
|
-
fs.writeFileSync(
|
|
35
|
-
path.join(skillDir, 'SKILL.md'),
|
|
36
|
-
`---\nname: ${name}\ndescription: ${JSON.stringify(description)}\n---\n\n${body.trim()}\n\n使用者提供的額外參數:請看觸發這個 skill 的使用者訊息。\n`
|
|
37
|
-
);
|
|
38
|
-
}
|
|
22
|
+
generateSkillsFromCommands({
|
|
23
|
+
commandsDir: path.join(vendorRoot, 'commands'),
|
|
24
|
+
targetSkillsDir: path.join(targetDir, '.agents', 'skills'),
|
|
25
|
+
namePrefix: 'devlog',
|
|
26
|
+
});
|
|
39
27
|
removeLegacyPrompts(targetDir);
|
|
40
28
|
}
|
|
41
29
|
|
|
@@ -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 = ['
|
|
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;
|
package/cli/vendor.test.js
CHANGED
|
@@ -11,8 +11,10 @@ function tmpdir() {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function makeFakeRepo(dir) {
|
|
14
|
-
fs.mkdirSync(path.join(dir, '
|
|
15
|
-
fs.writeFileSync(path.join(dir, '
|
|
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, '
|
|
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 ../../
|
|
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', '..', '..', '
|
|
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
|
|
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/../../
|
|
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
|
|
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/../../
|
|
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
|
|
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/../../
|
|
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
|
|
19
|
+
export DEVLOG_PROJECT_DIR="$ROOT"
|
|
20
20
|
|
|
21
21
|
SRC=""
|
|
22
22
|
if command -v jq >/dev/null 2>&1; then
|
package/codex/hooks/on-stop.sh
CHANGED
|
@@ -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
|
|
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/../../
|
|
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
|
|
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/../../
|
|
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
|
|
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 ../../
|
|
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
|
|
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
|
package/commands/checkpoint.md
CHANGED
|
@@ -6,8 +6,8 @@ description: 調整 Checkpoint Mode 的沉默門檻——累積多少輪沒寫 #
|
|
|
6
6
|
跑:
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
|
-
PLUGIN_ROOT="${
|
|
10
|
-
|
|
9
|
+
PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
|
|
10
|
+
DEVLOG_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/core/scripts/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 根目錄(有 `
|
|
33
|
+
先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
PLUGIN_ROOT="${
|
|
37
|
-
|
|
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
|
|
package/commands/compact.md
CHANGED
|
@@ -13,10 +13,10 @@ description: 把 .devlog/devlog.md 裡已完成且較舊的紀錄搬到 devlog.a
|
|
|
13
13
|
- 所有 `## Checkpoint` 區塊,永遠留在 devlog.md、不搬到 archive——它們是
|
|
14
14
|
checkpoint 機制存在的目的:翻閱時的摘要路標,搬走就失去了作用
|
|
15
15
|
4. 跑(不要自己搬檔):
|
|
16
|
-
先決定 plugin 根目錄(有 `
|
|
16
|
+
先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
|
|
17
17
|
```bash
|
|
18
|
-
PLUGIN_ROOT="${
|
|
19
|
-
|
|
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` 回報一句話。
|
package/commands/continue.md
CHANGED
|
@@ -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 根目錄(有 `
|
|
12
|
+
1. 跑與寫「工作區」相同的生產者,不要自己跑 git、不要手編成 SKILL 的七種格式。先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
|
|
13
13
|
```bash
|
|
14
|
-
PLUGIN_ROOT="${
|
|
15
|
-
|
|
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
|
-
把 `
|
|
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
|
@@ -34,16 +34,19 @@ description: 掃描整份 devlog.md,把值得留名的主題段落一次分別
|
|
|
34
34
|
回覆:
|
|
35
35
|
採用 → 全部照上面寫入
|
|
36
36
|
改第 N 段範圍 <from>-<to> / 改第 N 段檔名 <name> / 移除第 N 段
|
|
37
|
+
摘要第 N 段 → 這一段搬完後,把敘事部分改寫得更精簡(見下方說明)
|
|
37
38
|
全部歷史合併成一個檔 <name> → 放棄分段,整份歷史存成一檔
|
|
38
39
|
取消
|
|
39
40
|
```
|
|
40
41
|
|
|
41
|
-
同一則回覆可以合併多條修改,例如「改第 2 段檔名 foo,移除第 3 段」。`N` 一律對應上面列出的原始編號,修改不會讓其他段落重新編號。
|
|
42
|
+
同一則回覆可以合併多條修改,例如「改第 2 段檔名 foo,移除第 3 段,摘要第 1 段」。`N` 一律對應上面列出的原始編號,修改不會讓其他段落重新編號。
|
|
43
|
+
|
|
44
|
+
「摘要第 N 段」是選配、預設關閉:不特別指定就跟現在一樣,逐字搬移。指定的段落記下來,留到步驟 5.5 使用;跟「改範圍」「改檔名」同一則回覆可以合併,不影響步驟 4 的驗證。
|
|
42
45
|
|
|
43
46
|
然後停止。使用者還沒回覆前不要寫任何檔。
|
|
44
47
|
|
|
45
48
|
- 取消,或套用修改後一段都不剩 → 不改檔,結束。
|
|
46
|
-
- 「全部歷史合併成一個檔 `<name>`」→ 放棄前面列出的分段,改成單一段:`from` = 最早的歷史 Round、`to` = 最晚的歷史 Round(仍不含開著的 Round),`<name>` 用使用者這裡給的名稱。之後只處理這一段,跳過步驟 4
|
|
49
|
+
- 「全部歷史合併成一個檔 `<name>`」→ 放棄前面列出的分段,改成單一段:`from` = 最早的歷史 Round、`to` = 最晚的歷史 Round(仍不含開著的 Round),`<name>` 用使用者這裡給的名稱。之後只處理這一段,跳過步驟 4 的「重疊」與「批次撞名」檢查(只有一段);「摘要」仍可套用在這唯一一段上。
|
|
47
50
|
- 其他回覆(採用 / 各種修改組合)→ 帶著套用修改後的段落清單,進入步驟 4。
|
|
48
51
|
|
|
49
52
|
## 4. 驗證每一段的範圍與檔名(不通過就再問一次,仍不寫檔)
|
|
@@ -75,11 +78,11 @@ description: 掃描整份 devlog.md,把值得留名的主題段落一次分別
|
|
|
75
78
|
|
|
76
79
|
把步驟 4 驗證通過的段落,依 `from` 由小到大排序。對每一段依序跑(一段一次,不要自己搬檔,也不要一次塞多段給腳本):
|
|
77
80
|
|
|
78
|
-
先決定 plugin 根目錄(有 `
|
|
81
|
+
先決定 plugin 根目錄(有 `DEVLOG_TRACKER_ROOT` 用它;否則用 `CLAUDE_PLUGIN_ROOT`;兩者都空就用含 `.claude-plugin/plugin.json` 的本 plugin 根目錄):
|
|
79
82
|
|
|
80
83
|
```bash
|
|
81
|
-
PLUGIN_ROOT="${
|
|
82
|
-
|
|
84
|
+
PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
|
|
85
|
+
DEVLOG_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/core/scripts/keep-move.sh" \
|
|
83
86
|
--from <from> --to <to> --name "<name>" --desc "<這段在做什麼的一句話>"
|
|
84
87
|
```
|
|
85
88
|
|
|
@@ -89,6 +92,19 @@ CLAUDE_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/hooks/scripts/keep-move.sh" \
|
|
|
89
92
|
|
|
90
93
|
如果這批段落最終涵蓋了 `devlog.md` 目前剩下的所有歷史 Round(沒有瑣碎段落留下),跑到最後一段(Round 號碼最新的那一段)時,腳本會自動判定為 full keep:專案摘要併入那一檔、開著的 Round 重編號成 1、`.span-open` 刪除、checkpoint counter 歸零。這是預期行為,不用特別處理,也不用另外判斷「這是不是 full keep」。
|
|
91
94
|
|
|
95
|
+
## 5.5 選配:摘要標記為「摘要第 N 段」的段落
|
|
96
|
+
|
|
97
|
+
只處理步驟 3 使用者明確標記要摘要、且步驟 5 已成功搬移的段落。沒有任何段落被標記時,整節跳過。
|
|
98
|
+
|
|
99
|
+
這一步是 Claude 自己改寫,不是另外呼叫 LLM API、不是腳本邏輯——`keep-move.sh` 在步驟 5 已經把該段逐字搬進 `devlog.<name>.md`,機器可核對的欄位跟這一步無關。對每個被標記的段落:
|
|
100
|
+
|
|
101
|
+
1. 用 Read 讀出該段剛寫入的 `devlog.<name>.md` 全文。
|
|
102
|
+
2. 只能改寫每個 `## Round` 裡的敘事文字:`### Summary`、`### Reply`、`#### 決策`、`#### 現況`。把同一輪裡這幾節的內容改寫得更精簡,可以合併重複的敘述,但不要跨 Round 合併——Round 的數量與編號必須維持原樣。
|
|
103
|
+
3. 絕對不要動:`## Round <n> — <時間戳>` 標題本身、`### User Input`、`#### 工作區`、`#### 檔案`、`#### 完成條件`、`#### 下一步`、`### Status`,以及 `## Kept 索引`。這些是機器核對過的事實或 `resume`/`continue` 接手時要逐字比對的欄位,摘要只能動無法被 git 或 Stop 驗證的敘事散文。
|
|
104
|
+
4. 用 Edit 把改寫後的內容寫回同一個檔案(不要用 Write 整檔覆寫,避免不小心動到上面列的欄位)。
|
|
105
|
+
|
|
106
|
+
改寫完成後才進入步驟 6/7 回報;回報時註明哪幾段套用了摘要。
|
|
107
|
+
|
|
92
108
|
## 6. 處理腳本結果
|
|
93
109
|
|
|
94
110
|
每段成功時記下 stdout 給的具名檔路徑、`ROUNDS=<from>-<to>` 與 `REMAINING=<int>`,等這批全部段落都跑完(或某段失敗中止)後,一次在步驟 7 回報。
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: 調整 Lessons Mode
|
|
2
|
+
description: 調整 Lessons Mode 機制性提醒的共用門檻——工作區漂移或 BLOCKED 輪次累積達幾次才印一次建議。
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
取得使用者要設定的次數(正整數);沒帶就先問,不要用預設值硬猜。跑:
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
PLUGIN_ROOT="${
|
|
9
|
-
|
|
8
|
+
PLUGIN_ROOT="${DEVLOG_TRACKER_ROOT:-${CLAUDE_PLUGIN_ROOT:-}}"
|
|
9
|
+
DEVLOG_PROJECT_DIR="$(pwd)" bash "${PLUGIN_ROOT}/core/scripts/lessons-drift-set.sh" <次數>
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
不要自己手改 `.devlog/.lessons-
|
|
12
|
+
不要自己手改 `.devlog/.lessons-advisory-state`。
|
|
13
13
|
|
|
14
14
|
- stdout 是 `NOT_STARTED`:告知這個專案還沒 `/devlog-tracker:start`,問要不要現在
|
|
15
15
|
`/devlog-tracker:start`,不要自己跑 start。
|