devlog-tracker 0.22.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/cli/agents-md.js +51 -0
- package/cli/agents-md.test.js +60 -0
- package/cli/platforms/codex.js +2 -0
- package/hooks/scripts/json-field.sh +22 -2
- package/hooks/scripts/tests/test-json-field.sh +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# devlog-tracker
|
|
2
2
|
|
|
3
|
-
**版本** 0.
|
|
3
|
+
**版本** 0.24.0
|
|
4
4
|
|
|
5
5
|
在專案中維護一份 `.devlog/devlog.md`,把每一輪對話的請求、決策與結果寫成永久紀錄。對話一 `/clear` 或換 session 就沒了;這份檔案取代那個缺口,讓工作可以中斷再接。沒下過 `/devlog-tracker:start` 時,裝著也不會動任何檔案。
|
|
6
6
|
|
|
@@ -46,6 +46,13 @@ npx devlog-tracker init --codex --cursor
|
|
|
46
46
|
其他工具已設定的 hook)。重新執行 `npx devlog-tracker init` 可以升級到套件目前的
|
|
47
47
|
版本;`npx devlog-tracker status` 可以查目前裝的版本是否落後。
|
|
48
48
|
|
|
49
|
+
裝 Codex 時,`init` 還會在專案根目錄的 `AGENTS.md` 加上(或更新)一段以
|
|
50
|
+
`<!-- devlog-tracker:begin/end -->` 包住的說明,告訴 Codex 沒有 slash 指令時該讀
|
|
51
|
+
`.devlog-tracker/commands/*.md`。區塊外的內容不會動,重跑 `init` 只會換掉區塊本身;
|
|
52
|
+
區塊裡只有相對路徑,可以 commit。
|
|
53
|
+
|
|
54
|
+
裝完 Codex 之後,第一次還要在 Codex 裡核准這些 hook,否則不會記錄(見下方〈Codex(選用)〉的「hook 需要審核」)。
|
|
55
|
+
|
|
49
56
|
`init` 會把這台機器專屬的絕對路徑寫進 `.codex/hooks.json`、`.cursor/hooks.json` 與
|
|
50
57
|
`.devlog-tracker/env.sh`。如果你把這些檔案 commit 進 git,每位隊友都要在自己的機器上
|
|
51
58
|
跑一次 `npx devlog-tracker init`(路徑每台機器不同);或者改成把 `.devlog-tracker/` 與
|
|
@@ -82,6 +89,18 @@ Claude Code 仍是主要安裝方式。若要在 Codex CLI 使用,先設定
|
|
|
82
89
|
`hooks` 合併進專案(或 `~/.codex/`)的 `hooks.json`。也可以把整個 `codex/hooks/` 與
|
|
83
90
|
`hooks/scripts/` vendoring 到專案,並調整 command 路徑;兩者的相對目錄必須維持可用。
|
|
84
91
|
|
|
92
|
+
**hook 需要審核才會執行。** Codex 對新增或有變動的 hook 要求先審核;沒核准的 hook 會被
|
|
93
|
+
直接略過,而且**沒有任何警告**,看起來就像 devlog 沒在記錄。這跟專案有沒有設成
|
|
94
|
+
`trust_level = "trusted"` 是兩回事,專案信任不會讓 hook 生效。
|
|
95
|
+
|
|
96
|
+
- 互動模式:第一次開啟時 Codex 會提示有 hook 需要審核,核准後才會執行。之後 hook 的設定
|
|
97
|
+
有變動(例如重跑 `init` 讓路徑或指令改變)也可能要再核准一次。
|
|
98
|
+
- 非互動的 `codex exec`(CI、腳本):未審核的 hook 會被靜默略過。`--dangerously-bypass-hook-trust`
|
|
99
|
+
可以讓它們跑起來,但那個旗標會略過所有 hook 的信任檢查,只適合已經自己確認過 hook 來源的
|
|
100
|
+
自動化環境。
|
|
101
|
+
- 想確認有沒有生效:`start` 之後送一則訊息,看 `.devlog/.round-current.md` 有沒有出現這一輪的
|
|
102
|
+
User Input skeleton;沒有就代表 hook 沒被執行。
|
|
103
|
+
|
|
85
104
|
Codex 目前沒有對應「使用者中斷」(Claude Code 的 `PostToolUseFailure`/
|
|
86
105
|
`is_interrupt`)與「這輪異常結束」(`StopFailure`)的事件,這兩種細節狀態在
|
|
87
106
|
Codex 上不會被標記成 `INTERRUPTED`;核心強制記錄機制(`Stop` 事件擋住未寫完的
|
package/cli/agents-md.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const BEGIN = '<!-- devlog-tracker:begin -->';
|
|
6
|
+
const END = '<!-- devlog-tracker:end -->';
|
|
7
|
+
|
|
8
|
+
const BLOCK = `${BEGIN}
|
|
9
|
+
## devlog-tracker
|
|
10
|
+
|
|
11
|
+
這個專案用 devlog-tracker 在 \`.devlog/devlog.md\` 維護逐輪紀錄(Claude Code 與 Codex 共用同一份)。Codex 沒有 \`/devlog-tracker:*\` slash 指令,請照下面對照做:
|
|
12
|
+
|
|
13
|
+
1. 先 \`source .devlog-tracker/env.sh\`(設定 \`DEVLOG_TRACKER_ROOT\`)。
|
|
14
|
+
2. 依使用者意圖讀對應的 \`.devlog-tracker/commands/<名稱>.md\`,照裡面的步驟做(腳本在 \`.devlog-tracker/hooks/scripts/\`,執行時 \`CLAUDE_PROJECT_DIR\` 設成專案根目錄)。
|
|
15
|
+
|
|
16
|
+
| 使用者說 | 讀這份 |
|
|
17
|
+
|---|---|
|
|
18
|
+
| 開始追蹤 / start | \`commands/start.md\` |
|
|
19
|
+
| 暫停 / pause | \`commands/pause.md\` |
|
|
20
|
+
| 狀態 / status | \`commands/status.md\` |
|
|
21
|
+
| 接續上一題 / continue(換過工具或 \`/clear\` 之後) | \`commands/continue.md\` |
|
|
22
|
+
| 歸檔 / compact | \`commands/compact.md\` |
|
|
23
|
+
| 清空重編 / clean(不可復原,先問使用者確認) | \`commands/clean.md\` |
|
|
24
|
+
| 保存主題 / keep、接續具名檔 / resume、跨主題總覽 / overview | \`commands/keep.md\`、\`commands/resume.md\`、\`commands/overview.md\` |
|
|
25
|
+
| 長任務定期記錄 / span | \`commands/span.md\` |
|
|
26
|
+
| 調整沉默門檻 / checkpoint、segment-watch | \`commands/checkpoint.md\`、\`commands/segment-watch.md\` |
|
|
27
|
+
| 開發歷程教訓 / lessons、lessons-on、lessons-off、lessons-drift | \`commands/lessons.md\`、\`commands/lessons-on.md\`、\`commands/lessons-off.md\`、\`commands/lessons-drift.md\` |
|
|
28
|
+
|
|
29
|
+
寫 devlog 的格式與規則見 \`.devlog-tracker/skills/devlog-tracker/SKILL.md\`。每輪結束前必須把當輪寫進 \`.devlog/\`;已 \`start\` 的專案,Stop hook 會擋沒寫完的輪次。
|
|
30
|
+
${END}
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
function upsertAgentsMd(targetDir) {
|
|
34
|
+
const filePath = path.join(targetDir, 'AGENTS.md');
|
|
35
|
+
const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
36
|
+
const begin = existing.indexOf(BEGIN);
|
|
37
|
+
const end = existing.indexOf(END);
|
|
38
|
+
|
|
39
|
+
let next;
|
|
40
|
+
if (begin !== -1 && end > begin) {
|
|
41
|
+
next = existing.slice(0, begin) + BLOCK + existing.slice(end + END.length).replace(/^\n/, '');
|
|
42
|
+
} else if (existing.trim() === '') {
|
|
43
|
+
next = BLOCK;
|
|
44
|
+
} else {
|
|
45
|
+
next = `${existing.replace(/\n*$/, '\n')}\n${BLOCK}`;
|
|
46
|
+
}
|
|
47
|
+
if (next !== existing) fs.writeFileSync(filePath, next);
|
|
48
|
+
return filePath;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { upsertAgentsMd, BEGIN, END };
|
|
@@ -0,0 +1,60 @@
|
|
|
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 { upsertAgentsMd, BEGIN, END } = require('./agents-md');
|
|
8
|
+
const { run } = require('./init');
|
|
9
|
+
|
|
10
|
+
function tmp() {
|
|
11
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'devlog-tracker-agents-'));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
test('creates AGENTS.md when missing', () => {
|
|
15
|
+
const dir = tmp();
|
|
16
|
+
const file = upsertAgentsMd(dir);
|
|
17
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
18
|
+
assert.ok(text.startsWith(BEGIN));
|
|
19
|
+
assert.ok(text.includes('.devlog-tracker/commands/'));
|
|
20
|
+
assert.ok(text.trimEnd().endsWith(END));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('appends after existing content without touching it', () => {
|
|
24
|
+
const dir = tmp();
|
|
25
|
+
fs.writeFileSync(path.join(dir, 'AGENTS.md'), '# My rules\n\nBe nice.\n');
|
|
26
|
+
const text = fs.readFileSync(upsertAgentsMd(dir), 'utf8');
|
|
27
|
+
assert.ok(text.startsWith('# My rules\n\nBe nice.\n\n'));
|
|
28
|
+
assert.ok(text.includes(BEGIN));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('is idempotent and replaces the block in place', () => {
|
|
32
|
+
const dir = tmp();
|
|
33
|
+
fs.writeFileSync(path.join(dir, 'AGENTS.md'), `before\n${BEGIN}\nold stale text\n${END}\nafter\n`);
|
|
34
|
+
const once = fs.readFileSync(upsertAgentsMd(dir), 'utf8');
|
|
35
|
+
const twice = fs.readFileSync(upsertAgentsMd(dir), 'utf8');
|
|
36
|
+
assert.equal(once, twice);
|
|
37
|
+
assert.ok(!once.includes('old stale text'));
|
|
38
|
+
assert.ok(once.startsWith('before\n'));
|
|
39
|
+
assert.ok(once.endsWith('after\n'));
|
|
40
|
+
assert.equal(once.split(BEGIN).length, 2);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('block mentions every command doc so Codex has an entry for each', () => {
|
|
44
|
+
const commandsDir = path.join(__dirname, '..', 'commands');
|
|
45
|
+
const text = fs.readFileSync(upsertAgentsMd(tmp()), 'utf8');
|
|
46
|
+
for (const file of fs.readdirSync(commandsDir).filter((f) => f.endsWith('.md'))) {
|
|
47
|
+
assert.ok(text.includes(`commands/${file}`), `AGENTS.md block is missing commands/${file}`);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('init --codex writes AGENTS.md; --cursor alone does not', async () => {
|
|
52
|
+
const repoRoot = path.join(__dirname, '..');
|
|
53
|
+
const withCodex = tmp();
|
|
54
|
+
await run(['--codex'], { repoRoot, targetDir: withCodex, version: '0.0.0' });
|
|
55
|
+
assert.ok(fs.existsSync(path.join(withCodex, 'AGENTS.md')));
|
|
56
|
+
|
|
57
|
+
const cursorOnly = tmp();
|
|
58
|
+
await run(['--cursor'], { repoRoot, targetDir: cursorOnly, version: '0.0.0' });
|
|
59
|
+
assert.ok(!fs.existsSync(path.join(cursorOnly, 'AGENTS.md')));
|
|
60
|
+
});
|
package/cli/platforms/codex.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { mergeHooksTemplate } = require('../merge-hooks');
|
|
4
|
+
const { upsertAgentsMd } = require('../agents-md');
|
|
4
5
|
|
|
5
6
|
function install({ repoRoot, targetDir, vendorRoot }) {
|
|
6
7
|
mergeHooksTemplate({
|
|
@@ -8,6 +9,7 @@ function install({ repoRoot, targetDir, vendorRoot }) {
|
|
|
8
9
|
targetPath: path.join(targetDir, '.codex', 'hooks.json'),
|
|
9
10
|
vendorRoot,
|
|
10
11
|
});
|
|
12
|
+
upsertAgentsMd(targetDir);
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
module.exports = { install };
|
|
@@ -66,8 +66,28 @@ json_str_field() {
|
|
|
66
66
|
echo
|
|
67
67
|
return 0
|
|
68
68
|
fi
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
# 值要吃得下 \" 這類跳脫序列,否則含引號的 prompt 會在第一個 \" 被截斷;
|
|
70
|
+
# 抓出來之後再還原 \n \t \r \" \\ \/(\uXXXX 不處理,原樣保留)。
|
|
71
|
+
local pat='"'"${key}"'"[[:space:]]*:[[:space:]]*"([^"\\]|\\.)*"'
|
|
72
|
+
raw="$(printf '%s' "$json" | grep -oE "$pat" 2>/dev/null | head -1 || echo '')"
|
|
73
|
+
printf '%s' "$raw" | sed -E "s/^\"${key}\"[[:space:]]*:[[:space:]]*\"//; s/\"$//" | awk '
|
|
74
|
+
BEGIN { ORS = "" }
|
|
75
|
+
{
|
|
76
|
+
s = $0; out = ""
|
|
77
|
+
while (length(s) > 0) {
|
|
78
|
+
c = substr(s, 1, 1)
|
|
79
|
+
if (c == "\\" && length(s) > 1) {
|
|
80
|
+
n = substr(s, 2, 1)
|
|
81
|
+
if (n == "n") out = out "\n"
|
|
82
|
+
else if (n == "t") out = out "\t"
|
|
83
|
+
else if (n == "r") out = out "\r"
|
|
84
|
+
else if (n == "u") out = out "\\u"
|
|
85
|
+
else out = out n
|
|
86
|
+
s = substr(s, 3)
|
|
87
|
+
} else { out = out c; s = substr(s, 2) }
|
|
88
|
+
}
|
|
89
|
+
print out
|
|
90
|
+
}'
|
|
71
91
|
echo
|
|
72
92
|
}
|
|
73
93
|
|
|
@@ -49,6 +49,23 @@ assert_eq "str field null" "" "$(json_str_field "$INPUT" reason)"
|
|
|
49
49
|
INPUT='not json'
|
|
50
50
|
assert_eq "str field malformed" "" "$(json_str_field "$INPUT" reason)"
|
|
51
51
|
|
|
52
|
+
# Escaped characters must survive with and without jq (the no-jq path is a
|
|
53
|
+
# grep/awk fallback). NOJQ_BIN is a PATH holding only the tools the fallback
|
|
54
|
+
# needs, so `command -v jq` fails inside it.
|
|
55
|
+
NOJQ_BIN="$(mktemp -d)"
|
|
56
|
+
trap 'rm -f "$TMP"; rm -rf "$NOJQ_BIN"' EXIT
|
|
57
|
+
for b in grep sed awk head tr cat; do
|
|
58
|
+
for d in /usr/bin /bin; do [ -x "$d/$b" ] && { ln -s "$d/$b" "$NOJQ_BIN/$b"; break; }; done
|
|
59
|
+
done
|
|
60
|
+
INPUT='{"session_id":"s","prompt":"say \"hi\" \\ back\/slash\ttab 你好","after":"ok"}'
|
|
61
|
+
EXPECT_PROMPT="$(printf 'say "hi" \\ back/slash\ttab 你好')"
|
|
62
|
+
for mode in jq nojq; do
|
|
63
|
+
if [ "$mode" = nojq ]; then P="$NOJQ_BIN"; else P="$PATH"; fi
|
|
64
|
+
[ "$mode" = jq ] && ! command -v jq >/dev/null 2>&1 && continue
|
|
65
|
+
assert_eq "[$mode] escaped quote/backslash/tab prompt" "$EXPECT_PROMPT" "$(PATH="$P" json_str_field "$INPUT" prompt)"
|
|
66
|
+
assert_eq "[$mode] field after escaped one" "ok" "$(PATH="$P" json_str_field "$INPUT" after)"
|
|
67
|
+
done
|
|
68
|
+
|
|
52
69
|
assert_eq "slugify basic" "foo-bar" "$(slugify 'foo bar')"
|
|
53
70
|
assert_eq "slugify collapses runs" "foo-bar" "$(slugify 'foo bar')"
|
|
54
71
|
assert_eq "slugify trims edges" "foo" "$(slugify ' foo ')"
|