devlog-tracker 0.22.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/LICENSE +201 -0
- package/README.md +234 -0
- package/bin/devlog-tracker.js +51 -0
- package/cli/init.js +55 -0
- package/cli/init.test.js +37 -0
- package/cli/merge-hooks.js +60 -0
- package/cli/merge-hooks.test.js +187 -0
- package/cli/platforms/codex.js +13 -0
- package/cli/platforms/cursor.js +13 -0
- package/cli/status.js +14 -0
- package/cli/status.test.js +33 -0
- package/cli/vendor.js +37 -0
- package/cli/vendor.test.js +84 -0
- package/codex/hooks/on-pre-tool.sh +19 -0
- package/codex/hooks/on-session-end.sh +31 -0
- package/codex/hooks/on-session-start.sh +37 -0
- package/codex/hooks/on-stop.sh +18 -0
- package/codex/hooks/on-user-prompt-submit.sh +15 -0
- package/codex/hooks/project-dir.sh +13 -0
- package/codex/hooks/test-adapters.sh +98 -0
- package/codex/hooks.json +54 -0
- package/commands/checkpoint.md +17 -0
- package/commands/clean.md +50 -0
- package/commands/compact.md +24 -0
- package/commands/continue.md +30 -0
- package/commands/keep.md +100 -0
- package/commands/lessons-drift.md +19 -0
- package/commands/lessons-off.md +16 -0
- package/commands/lessons-on.md +20 -0
- package/commands/lessons.md +20 -0
- package/commands/overview.md +32 -0
- package/commands/pause.md +20 -0
- package/commands/resume.md +18 -0
- package/commands/segment-watch.md +21 -0
- package/commands/span.md +13 -0
- package/commands/start.md +19 -0
- package/commands/status.md +14 -0
- package/cursor/hooks/on-pre-tool.sh +24 -0
- package/cursor/hooks/on-session-end.sh +11 -0
- package/cursor/hooks/on-session-start.sh +15 -0
- package/cursor/hooks/on-stop.sh +44 -0
- package/cursor/hooks/on-submit-prompt.sh +28 -0
- package/cursor/hooks/on-tool-failure.sh +16 -0
- package/cursor/hooks/project-dir.sh +10 -0
- package/cursor/hooks/test-adapters.sh +201 -0
- package/cursor/hooks.json +26 -0
- package/hooks/scripts/await-open.sh +23 -0
- package/hooks/scripts/checkpoint-set.sh +44 -0
- package/hooks/scripts/clean-devlog.sh +69 -0
- package/hooks/scripts/close-open-round.sh +186 -0
- package/hooks/scripts/compact-devlog.sh +72 -0
- package/hooks/scripts/detect-pending-question.sh +31 -0
- package/hooks/scripts/devlog-lock.sh +38 -0
- package/hooks/scripts/devlog-md.sh +222 -0
- package/hooks/scripts/devlog-path.sh +70 -0
- package/hooks/scripts/enforce-devlog.sh +629 -0
- package/hooks/scripts/files-snapshot.sh +70 -0
- package/hooks/scripts/json-field.sh +76 -0
- package/hooks/scripts/keep-move.sh +155 -0
- package/hooks/scripts/kept-list.sh +40 -0
- package/hooks/scripts/lessons-append.sh +93 -0
- package/hooks/scripts/lessons-drift-set.sh +42 -0
- package/hooks/scripts/lessons-off.sh +15 -0
- package/hooks/scripts/lessons-on.sh +21 -0
- package/hooks/scripts/lessons-read.sh +60 -0
- package/hooks/scripts/on-session-end.sh +14 -0
- package/hooks/scripts/on-stop-failure.sh +18 -0
- package/hooks/scripts/on-tool-failure.sh +25 -0
- package/hooks/scripts/pause-devlog.sh +13 -0
- package/hooks/scripts/redact-prompt.sh +19 -0
- package/hooks/scripts/resume-devlog.sh +41 -0
- package/hooks/scripts/round-start.sh +333 -0
- package/hooks/scripts/run-tests.sh +26 -0
- package/hooks/scripts/segment-watch-set.sh +47 -0
- package/hooks/scripts/segment-watch.sh +197 -0
- package/hooks/scripts/session-start-devlog.sh +157 -0
- package/hooks/scripts/span-close.sh +9 -0
- package/hooks/scripts/span-open.sh +21 -0
- package/hooks/scripts/start-devlog.sh +24 -0
- package/hooks/scripts/status-devlog.sh +43 -0
- package/hooks/scripts/tests/test-await-open.sh +112 -0
- package/hooks/scripts/tests/test-branch-scoped-integration.sh +108 -0
- package/hooks/scripts/tests/test-checkpoint-set.sh +36 -0
- package/hooks/scripts/tests/test-clean-devlog.sh +160 -0
- package/hooks/scripts/tests/test-cli-init-e2e.sh +37 -0
- package/hooks/scripts/tests/test-close-open-round.sh +325 -0
- package/hooks/scripts/tests/test-compact-devlog.sh +62 -0
- package/hooks/scripts/tests/test-devlog-lock.sh +37 -0
- package/hooks/scripts/tests/test-devlog-md.sh +530 -0
- package/hooks/scripts/tests/test-devlog-path.sh +118 -0
- package/hooks/scripts/tests/test-enforce-devlog-files.sh +233 -0
- package/hooks/scripts/tests/test-enforce-devlog-handoff-order.sh +223 -0
- package/hooks/scripts/tests/test-enforce-devlog-workspace.sh +333 -0
- package/hooks/scripts/tests/test-enforce-devlog.sh +1290 -0
- package/hooks/scripts/tests/test-files-snapshot.sh +142 -0
- package/hooks/scripts/tests/test-json-field.sh +57 -0
- package/hooks/scripts/tests/test-keep-move.sh +127 -0
- package/hooks/scripts/tests/test-kept-list.sh +80 -0
- package/hooks/scripts/tests/test-lessons-append.sh +138 -0
- package/hooks/scripts/tests/test-lessons-drift-set.sh +36 -0
- package/hooks/scripts/tests/test-lessons-on-off.sh +75 -0
- package/hooks/scripts/tests/test-lessons-read.sh +97 -0
- package/hooks/scripts/tests/test-on-interrupt.sh +186 -0
- package/hooks/scripts/tests/test-redact-prompt.sh +27 -0
- package/hooks/scripts/tests/test-resume-devlog.sh +35 -0
- package/hooks/scripts/tests/test-round-start.sh +825 -0
- package/hooks/scripts/tests/test-segment-watch-set.sh +75 -0
- package/hooks/scripts/tests/test-segment-watch.sh +468 -0
- package/hooks/scripts/tests/test-session-start-devlog.sh +508 -0
- package/hooks/scripts/tests/test-start-pause-devlog.sh +84 -0
- package/hooks/scripts/tests/test-status-span.sh +50 -0
- package/hooks/scripts/tests/test-workspace-snapshot.sh +130 -0
- package/hooks/scripts/workspace-snapshot.sh +69 -0
- package/package.json +37 -0
- package/skills/devlog-tracker/SKILL.md +411 -0
- package/skills/devlog-tracker/references/checkpoint-mode.md +42 -0
- package/skills/devlog-tracker/references/lessons-mode.md +32 -0
- package/skills/devlog-tracker/references/reply-fold.md +129 -0
- package/skills/devlog-tracker/references/round-segments.md +55 -0
- package/skills/devlog-tracker/references/span-mode.md +65 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SessionStart hook:在 startup / resume / clear / compact / fork 時執行。
|
|
3
|
+
# startup / resume / compact / fork:讀 .devlog/devlog.md 最後 N 輪 + 開頭摘要,
|
|
4
|
+
# 印到 stdout 讓 Claude Code 當成 additionalContext 注入。
|
|
5
|
+
# source=clear:只做 dangling heal,不注入、不印 span 提醒——/clear 必須是真的空
|
|
6
|
+
# context,接續改由 /devlog-tracker:continue。找不到檔案就 exit 0。
|
|
7
|
+
#
|
|
8
|
+
# Span Mode:.span-open 還開著時,在注入內容最前面加一段提醒(clear 除外)。
|
|
9
|
+
# 讀不到/格式壞掉就靜默跳過,fail-open。
|
|
10
|
+
#
|
|
11
|
+
# Dangling heal:startup / resume / clear / fork 把殘留的 .round-open 標成
|
|
12
|
+
# INTERRUPTED。source=compact(以及 source 缺失/讀不到)跳過 heal,避免
|
|
13
|
+
# mid-turn auto-compact 改雜湊讓 Stop 靜默放行;compact 仍注入 excerpt
|
|
14
|
+
#(最後一個 Checkpoint + 最近兩輪 Summary/Handoff/Status),不 heal。
|
|
15
|
+
# close-open-round.sh 必須對 stdout 保持沉默。
|
|
16
|
+
#
|
|
17
|
+
# .round-current.md excerpt:因為 compact 不 heal,這一輪真的可能還開著、
|
|
18
|
+
# 內容只在 .devlog/.round-current.md 裡(尚未併回 devlog.md)——起始時的
|
|
19
|
+
# devlog.md 摘要看不到它,會讓 mid-turn auto-compact 的注入內容悄悄漏掉
|
|
20
|
+
# Claude 正在寫的這一輪。devlog.md 摘要印完之後,只要 .round-current.md
|
|
21
|
+
# 非空就整份印出來、清楚標示「尚未收尾」,跟上面的歷史摘要分開。startup /
|
|
22
|
+
# resume / fork 這幾個 source 因為上面已經先 heal 過,這裡通常是空的、印出
|
|
23
|
+
# 來是 no-op,不特別排除,寫法比較單純。讀不到就跳過,fail-open。
|
|
24
|
+
|
|
25
|
+
set -uo pipefail
|
|
26
|
+
|
|
27
|
+
_src="${BASH_SOURCE[0]}"
|
|
28
|
+
HOOKS_DIR="$(cd "${_src%/*}" && pwd)"
|
|
29
|
+
# shellcheck source=json-field.sh
|
|
30
|
+
. "$HOOKS_DIR/json-field.sh"
|
|
31
|
+
# shellcheck source=detect-pending-question.sh
|
|
32
|
+
. "$HOOKS_DIR/detect-pending-question.sh"
|
|
33
|
+
# shellcheck source=devlog-path.sh
|
|
34
|
+
. "$HOOKS_DIR/devlog-path.sh"
|
|
35
|
+
|
|
36
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
|
37
|
+
devlog_resolve_paths "$PROJECT_DIR"
|
|
38
|
+
ROUND_CURRENT="$DEVLOG_DIR/.round-current.md"
|
|
39
|
+
SPAN_FILE="$DEVLOG_DIR/.span-open"
|
|
40
|
+
|
|
41
|
+
INPUT="$(cat 2>/dev/null || true)"
|
|
42
|
+
SOURCE="$(json_str_field "$INPUT" source)"
|
|
43
|
+
|
|
44
|
+
case "$SOURCE" in
|
|
45
|
+
startup|resume|clear|fork)
|
|
46
|
+
if [ -f "$DEVLOG_DIR/.round-open" ]; then
|
|
47
|
+
DANGLING_DETAIL=""
|
|
48
|
+
if [ -n "$(detect_pending_question "$(json_str_field "$INPUT" transcript_path)")" ]; then
|
|
49
|
+
DANGLING_DETAIL="awaiting_question"
|
|
50
|
+
fi
|
|
51
|
+
bash "$HOOKS_DIR/close-open-round.sh" "dangling:session_start" "$DANGLING_DETAIL" || true
|
|
52
|
+
fi
|
|
53
|
+
;;
|
|
54
|
+
esac
|
|
55
|
+
|
|
56
|
+
# /clear 清空對話;不要把 devlog 或 span 提醒打回 context。
|
|
57
|
+
if [ "$SOURCE" = "clear" ]; then
|
|
58
|
+
exit 0
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
if [ -f "$SPAN_FILE" ]; then
|
|
62
|
+
SPAN_ROUND="$(json_int_get "$SPAN_FILE" round)"
|
|
63
|
+
SPAN_OPENED_AT="$(json_str_get "$SPAN_FILE" opened_at)"
|
|
64
|
+
if [ -n "$SPAN_ROUND" ] && [ -n "$SPAN_OPENED_AT" ]; then
|
|
65
|
+
echo "⚠️ 有一個開啟中的 span:Round ${SPAN_ROUND},從 ${SPAN_OPENED_AT} 開始,"
|
|
66
|
+
echo "還沒有正式結束。請先確認要繼續這個自動化任務,還是要明確關閉它"
|
|
67
|
+
echo "(刪除 .devlog/.span-open 並補寫收尾的 Round)。"
|
|
68
|
+
echo ""
|
|
69
|
+
fi
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
if [ -f "$DEVLOG_FILE" ]; then
|
|
73
|
+
echo "以下是本專案 .devlog/${DEVLOG_FILE##*/} 的接手摘要(不是全文;完整紀錄請自行讀取原檔):"
|
|
74
|
+
echo ""
|
|
75
|
+
awk '
|
|
76
|
+
/^[ \t]*```/ { fence = !fence }
|
|
77
|
+
{
|
|
78
|
+
lines[NR] = $0
|
|
79
|
+
infence[NR] = fence
|
|
80
|
+
}
|
|
81
|
+
END {
|
|
82
|
+
n = NR
|
|
83
|
+
last_cp = 0
|
|
84
|
+
last_kept = 0
|
|
85
|
+
last_lessons = 0
|
|
86
|
+
rc = 0
|
|
87
|
+
for (i = 1; i <= n; i++) {
|
|
88
|
+
if (infence[i]) continue
|
|
89
|
+
if (lines[i] ~ /^## Checkpoint/) last_cp = i
|
|
90
|
+
if (lines[i] ~ /^## Kept 索引/) last_kept = i
|
|
91
|
+
if (lines[i] ~ /^## Lessons 索引/) last_lessons = i
|
|
92
|
+
if (lines[i] ~ /^## Round /) { rc++; round_at[rc] = i }
|
|
93
|
+
}
|
|
94
|
+
if (last_cp > 0) {
|
|
95
|
+
cp_end = n
|
|
96
|
+
for (j = last_cp + 1; j <= n; j++) {
|
|
97
|
+
if (!infence[j] && lines[j] ~ /^## /) { cp_end = j - 1; break }
|
|
98
|
+
}
|
|
99
|
+
for (j = last_cp; j <= cp_end; j++) print lines[j]
|
|
100
|
+
print ""
|
|
101
|
+
}
|
|
102
|
+
if (last_kept > 0) {
|
|
103
|
+
kp_end = n
|
|
104
|
+
for (j = last_kept + 1; j <= n; j++) {
|
|
105
|
+
if (!infence[j] && lines[j] ~ /^## /) { kp_end = j - 1; break }
|
|
106
|
+
}
|
|
107
|
+
for (j = last_kept; j <= kp_end; j++) print lines[j]
|
|
108
|
+
print ""
|
|
109
|
+
}
|
|
110
|
+
if (last_lessons > 0) {
|
|
111
|
+
ls_end = n
|
|
112
|
+
for (j = last_lessons + 1; j <= n; j++) {
|
|
113
|
+
if (!infence[j] && lines[j] ~ /^## /) { ls_end = j - 1; break }
|
|
114
|
+
}
|
|
115
|
+
for (j = last_lessons; j <= ls_end; j++) print lines[j]
|
|
116
|
+
print ""
|
|
117
|
+
}
|
|
118
|
+
start_i = (rc > 2) ? rc - 1 : 1
|
|
119
|
+
if (rc == 0) exit 0
|
|
120
|
+
for (r = start_i; r <= rc; r++) {
|
|
121
|
+
rs = round_at[r]
|
|
122
|
+
re = n
|
|
123
|
+
for (j = rs + 1; j <= n; j++) {
|
|
124
|
+
if (!infence[j] && lines[j] ~ /^## /) { re = j - 1; break }
|
|
125
|
+
}
|
|
126
|
+
print lines[rs]
|
|
127
|
+
print ""
|
|
128
|
+
has_summary = 0
|
|
129
|
+
for (j = rs; j <= re; j++) if (!infence[j] && lines[j] ~ /^### Summary/) has_summary = 1
|
|
130
|
+
keep = 0
|
|
131
|
+
for (j = rs + 1; j <= re; j++) {
|
|
132
|
+
if (infence[j]) {
|
|
133
|
+
if (keep) print lines[j]
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
if (lines[j] ~ /^### Summary/ || lines[j] ~ /^### Handoff/ || lines[j] ~ /^### Status/) { keep = 1; print lines[j]; continue }
|
|
137
|
+
if (lines[j] ~ /^### User Input/) { keep = (has_summary ? 0 : 1); if (keep) print lines[j]; continue }
|
|
138
|
+
if (lines[j] ~ /^### /) { keep = 0; continue }
|
|
139
|
+
if (keep) print lines[j]
|
|
140
|
+
}
|
|
141
|
+
print ""
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
' "$DEVLOG_FILE" 2>/dev/null || true
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
# See header comment: this round may still be genuinely open (compact skips
|
|
148
|
+
# heal) with its real content sitting only in .round-current.md. Surface it
|
|
149
|
+
# separately from the devlog.md excerpt above so it isn't silently dropped.
|
|
150
|
+
if [ -s "$ROUND_CURRENT" ]; then
|
|
151
|
+
echo "以下是目前還沒收尾、仍在 .devlog/.round-current.md 裡的這一輪內容(跟上面的歷史摘要分開,尚未併入 devlog.md):"
|
|
152
|
+
echo ""
|
|
153
|
+
cat "$ROUND_CURRENT" 2>/dev/null || true
|
|
154
|
+
echo ""
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
exit 0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
_src="${BASH_SOURCE[0]}"
|
|
4
|
+
SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
|
|
5
|
+
. "$SCRIPT_DIR/json-field.sh"
|
|
6
|
+
. "$SCRIPT_DIR/devlog-md.sh"
|
|
7
|
+
. "$SCRIPT_DIR/devlog-path.sh"
|
|
8
|
+
|
|
9
|
+
devlog_resolve_paths "${CLAUDE_PROJECT_DIR:-.}"
|
|
10
|
+
[ -f "$DEVLOG_DIR/.enabled" ] || { echo "NOT_ENABLED" >&2; exit 1; }
|
|
11
|
+
[ ! -e "$DEVLOG_DIR/.span-open" ] || { echo "ALREADY_OPEN" >&2; exit 1; }
|
|
12
|
+
ROUND=""
|
|
13
|
+
[ ! -f "$DEVLOG_DIR/.round-open" ] || ROUND="$(json_int_get "$DEVLOG_DIR/.round-open" round)"
|
|
14
|
+
if [ -z "$ROUND" ] && [ -f "$DEVLOG_FILE" ]; then
|
|
15
|
+
ROUND="$(devlog_list_round_starts "$DEVLOG_FILE" | awk 'END { print $2 }')"
|
|
16
|
+
fi
|
|
17
|
+
[ -n "$ROUND" ] || { echo "NO_ROUND" >&2; exit 1; }
|
|
18
|
+
OPENED_AT="$(date -Iseconds 2>/dev/null || date '+%Y-%m-%dT%H:%M:%S%z')"
|
|
19
|
+
printf '{"round": %s, "opened_at": "%s", "ticks_since_checkin": 0, "max_silent_ticks": 5}\n' \
|
|
20
|
+
"$ROUND" "$OPENED_AT" > "$DEVLOG_DIR/.span-open" || exit 1
|
|
21
|
+
printf 'OPENED=%s\n' "$ROUND"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# User-invoked: /devlog-tracker:start filesystem side.
|
|
3
|
+
set -uo pipefail
|
|
4
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
|
5
|
+
DEVLOG_DIR="$PROJECT_DIR/.devlog"
|
|
6
|
+
mkdir -p "$DEVLOG_DIR" || exit 1
|
|
7
|
+
if [ ! -f "$DEVLOG_DIR/.enabled" ]; then
|
|
8
|
+
date -Iseconds > "$DEVLOG_DIR/.enabled" 2>/dev/null || echo enabled > "$DEVLOG_DIR/.enabled"
|
|
9
|
+
fi
|
|
10
|
+
if [ ! -f "$DEVLOG_DIR/.checkpoint-state" ]; then
|
|
11
|
+
printf '%s\n' '{"rounds_since_checkpoint": 0, "max_silent_rounds": 20, "checkpoint_marker_count": 0}' \
|
|
12
|
+
> "$DEVLOG_DIR/.checkpoint-state" || exit 1
|
|
13
|
+
fi
|
|
14
|
+
if [ ! -f "$DEVLOG_DIR/.segment-state" ]; then
|
|
15
|
+
printf '%s\n' '{"last_change_epoch": 0, "last_seen_cksum": "", "max_silent_seconds": 600, "session_id": ""}' \
|
|
16
|
+
> "$DEVLOG_DIR/.segment-state" || exit 1
|
|
17
|
+
fi
|
|
18
|
+
GITIGNORE_DEVLOG=no
|
|
19
|
+
if [ -f "$PROJECT_DIR/.gitignore" ] && grep -E -q '(^|/)\.devlog(/|$)' "$PROJECT_DIR/.gitignore"; then
|
|
20
|
+
GITIGNORE_DEVLOG=yes
|
|
21
|
+
fi
|
|
22
|
+
echo "ENABLED=$DEVLOG_DIR/.enabled"
|
|
23
|
+
echo "GITIGNORE_DEVLOG=$GITIGNORE_DEVLOG"
|
|
24
|
+
exit 0
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
_src="${BASH_SOURCE[0]}"
|
|
4
|
+
SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
|
|
5
|
+
. "$SCRIPT_DIR/json-field.sh"
|
|
6
|
+
. "$SCRIPT_DIR/devlog-md.sh"
|
|
7
|
+
. "$SCRIPT_DIR/devlog-path.sh"
|
|
8
|
+
|
|
9
|
+
devlog_resolve_paths "${CLAUDE_PROJECT_DIR:-.}"
|
|
10
|
+
[ -d "$DEVLOG_DIR" ] || { echo "NOT_STARTED"; exit 0; }
|
|
11
|
+
|
|
12
|
+
[ -f "$DEVLOG_DIR/.enabled" ] && echo "ENABLED=yes" || echo "ENABLED=no"
|
|
13
|
+
[ -f "$DEVLOG_DIR/.lessons-enabled" ] && echo "LESSONS=yes" || echo "LESSONS=no"
|
|
14
|
+
if [ -f "$DEVLOG_DIR/.lessons-drift-state" ]; then
|
|
15
|
+
drift_count="$(json_int_get "$DEVLOG_DIR/.lessons-drift-state" mismatch_count)"
|
|
16
|
+
drift_max="$(json_int_get "$DEVLOG_DIR/.lessons-drift-state" threshold)"
|
|
17
|
+
printf 'LESSONS_DRIFT=%s/%s\n' "${drift_count:-0}" "${drift_max:-3}"
|
|
18
|
+
fi
|
|
19
|
+
if [ -f "$DEVLOG_DIR/.span-open" ]; then
|
|
20
|
+
r="$(json_int_get "$DEVLOG_DIR/.span-open" round)"
|
|
21
|
+
opened="$(json_str_get "$DEVLOG_DIR/.span-open" opened_at)"
|
|
22
|
+
ticks="$(json_int_get "$DEVLOG_DIR/.span-open" ticks_since_checkin)"
|
|
23
|
+
max="$(json_int_get "$DEVLOG_DIR/.span-open" max_silent_ticks)"
|
|
24
|
+
printf 'SPAN=%s,%s,%s,%s\n' "${r:-?}" "${opened:-?}" "${ticks:-?}" "${max:-5}"
|
|
25
|
+
else
|
|
26
|
+
echo "SPAN=closed"
|
|
27
|
+
fi
|
|
28
|
+
rounds="$(json_int_get "$DEVLOG_DIR/.checkpoint-state" rounds_since_checkpoint)"
|
|
29
|
+
checkpoint_max="$(json_int_get "$DEVLOG_DIR/.checkpoint-state" max_silent_rounds)"
|
|
30
|
+
printf 'CHECKPOINT=%s/%s\n' "${rounds:-0}" "${checkpoint_max:-20}"
|
|
31
|
+
seconds="$(json_int_get "$DEVLOG_DIR/.segment-state" max_silent_seconds)"
|
|
32
|
+
printf 'SEGMENT=max_silent_seconds=%s\n' "${seconds:-600}"
|
|
33
|
+
|
|
34
|
+
status="none"
|
|
35
|
+
if [ -f "$DEVLOG_FILE" ]; then
|
|
36
|
+
last="$(devlog_list_round_starts "$DEVLOG_FILE" | awk 'END { print $1 }')"
|
|
37
|
+
if [ -n "$last" ]; then
|
|
38
|
+
end="$(devlog_block_end "$DEVLOG_FILE" "$last")"
|
|
39
|
+
found="$(devlog_round_status "$DEVLOG_FILE" "$last" "$end")"
|
|
40
|
+
[ -z "$found" ] || status="$found"
|
|
41
|
+
fi
|
|
42
|
+
fi
|
|
43
|
+
printf 'LAST_STATUS=%s\n' "$status"
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Self-check for await-open.sh. Run:
|
|
3
|
+
# bash hooks/scripts/test-await-open.sh
|
|
4
|
+
set -uo pipefail
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
7
|
+
TMP_ROOT="$(mktemp -d)"
|
|
8
|
+
trap 'rm -rf "$TMP_ROOT"' EXIT
|
|
9
|
+
|
|
10
|
+
export CLAUDE_PROJECT_DIR="$TMP_ROOT"
|
|
11
|
+
DEVLOG_DIR="$TMP_ROOT/.devlog"
|
|
12
|
+
mkdir -p "$DEVLOG_DIR"
|
|
13
|
+
|
|
14
|
+
FAIL=0
|
|
15
|
+
assert_exit() {
|
|
16
|
+
local desc="$1" expected="$2" actual="$3"
|
|
17
|
+
if [ "$actual" -eq "$expected" ]; then
|
|
18
|
+
echo "PASS: $desc"
|
|
19
|
+
else
|
|
20
|
+
echo "FAIL: $desc (expected exit $expected, got $actual)"
|
|
21
|
+
FAIL=1
|
|
22
|
+
fi
|
|
23
|
+
}
|
|
24
|
+
assert_contains() {
|
|
25
|
+
local desc="$1" needle="$2" haystack="$3"
|
|
26
|
+
case "$haystack" in
|
|
27
|
+
*"$needle"*) echo "PASS: $desc" ;;
|
|
28
|
+
*) echo "FAIL: $desc (missing: $needle)"; FAIL=1 ;;
|
|
29
|
+
esac
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# --- 1: not enabled -> NOT_ENABLED, no file --------------------------------
|
|
33
|
+
OUT="$(bash "$SCRIPT_DIR/await-open.sh")"
|
|
34
|
+
assert_exit "not enabled -> exit 1" 1 $?
|
|
35
|
+
assert_contains "reports NOT_ENABLED" "NOT_ENABLED" "$OUT"
|
|
36
|
+
if [ -f "$DEVLOG_DIR/.awaiting-reply" ]; then
|
|
37
|
+
echo "FAIL: .awaiting-reply should not exist when disabled"
|
|
38
|
+
FAIL=1
|
|
39
|
+
else
|
|
40
|
+
echo "PASS: no .awaiting-reply when disabled"
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
touch "$DEVLOG_DIR/.enabled"
|
|
44
|
+
|
|
45
|
+
# --- 2: resolves round from .round-open when present -----------------------
|
|
46
|
+
printf '%s\n' '{"round": 4, "opened_at": "2026-09-09T12:00:00+08:00"}' > "$DEVLOG_DIR/.round-open"
|
|
47
|
+
OUT="$(bash "$SCRIPT_DIR/await-open.sh")"
|
|
48
|
+
assert_exit "opens -> exit 0" 0 $?
|
|
49
|
+
assert_contains "reports OPENED=4" "OPENED=4" "$OUT"
|
|
50
|
+
ROUND_FIELD="$(grep -o '"round"[[:space:]]*:[[:space:]]*[0-9]\+' "$DEVLOG_DIR/.awaiting-reply" | grep -o '[0-9]\+$')"
|
|
51
|
+
if [ "$ROUND_FIELD" = "4" ]; then
|
|
52
|
+
echo "PASS: .awaiting-reply round=4 (from .round-open)"
|
|
53
|
+
else
|
|
54
|
+
echo "FAIL: expected round=4, got $ROUND_FIELD"
|
|
55
|
+
FAIL=1
|
|
56
|
+
fi
|
|
57
|
+
grep -q '"opened_at"' "$DEVLOG_DIR/.awaiting-reply" \
|
|
58
|
+
&& echo "PASS: opened_at field present" || { echo "FAIL: opened_at missing"; FAIL=1; }
|
|
59
|
+
rm -f "$DEVLOG_DIR/.awaiting-reply" "$DEVLOG_DIR/.round-open"
|
|
60
|
+
|
|
61
|
+
# --- 3: falls back to last ## Round in devlog.md when .round-open absent ---
|
|
62
|
+
cat > "$DEVLOG_DIR/devlog.md" <<'EOF'
|
|
63
|
+
## Round 1 — 2026-09-09T12:00:00+08:00
|
|
64
|
+
|
|
65
|
+
### Summary
|
|
66
|
+
done
|
|
67
|
+
|
|
68
|
+
### Reply
|
|
69
|
+
fixture reply.
|
|
70
|
+
|
|
71
|
+
### Handoff
|
|
72
|
+
#### 現況
|
|
73
|
+
done
|
|
74
|
+
|
|
75
|
+
### Status
|
|
76
|
+
DONE
|
|
77
|
+
|
|
78
|
+
## Round 2 — 2026-09-09T13:00:00+08:00
|
|
79
|
+
|
|
80
|
+
### Summary
|
|
81
|
+
done
|
|
82
|
+
|
|
83
|
+
### Reply
|
|
84
|
+
fixture reply.
|
|
85
|
+
|
|
86
|
+
### Handoff
|
|
87
|
+
#### 現況
|
|
88
|
+
done
|
|
89
|
+
|
|
90
|
+
### Status
|
|
91
|
+
DONE
|
|
92
|
+
EOF
|
|
93
|
+
OUT="$(bash "$SCRIPT_DIR/await-open.sh")"
|
|
94
|
+
assert_contains "falls back to last Round in devlog.md" "OPENED=2" "$OUT"
|
|
95
|
+
rm -f "$DEVLOG_DIR/.awaiting-reply"
|
|
96
|
+
|
|
97
|
+
# --- 4: already open -> ALREADY_OPEN, does not overwrite --------------------
|
|
98
|
+
printf '%s\n' '{"round": 99, "opened_at": "sentinel"}' > "$DEVLOG_DIR/.awaiting-reply"
|
|
99
|
+
OUT="$(bash "$SCRIPT_DIR/await-open.sh")"
|
|
100
|
+
assert_exit "already open -> exit 1" 1 $?
|
|
101
|
+
assert_contains "reports ALREADY_OPEN" "ALREADY_OPEN" "$OUT"
|
|
102
|
+
grep -q 'sentinel' "$DEVLOG_DIR/.awaiting-reply" \
|
|
103
|
+
&& echo "PASS: existing marker left untouched" || { echo "FAIL: existing marker was overwritten"; FAIL=1; }
|
|
104
|
+
rm -f "$DEVLOG_DIR/.awaiting-reply" "$DEVLOG_DIR/devlog.md"
|
|
105
|
+
|
|
106
|
+
if [ "$FAIL" -eq 0 ]; then
|
|
107
|
+
echo "All checks passed."
|
|
108
|
+
exit 0
|
|
109
|
+
else
|
|
110
|
+
echo "Some checks FAILED."
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
4
|
+
TMP="$(mktemp -d)"
|
|
5
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
6
|
+
FAIL=0
|
|
7
|
+
|
|
8
|
+
REPO="$TMP/repo"
|
|
9
|
+
mkdir -p "$REPO"
|
|
10
|
+
git -C "$REPO" init -q
|
|
11
|
+
git -C "$REPO" -c user.email=t@t.t -c user.name=t commit -q --allow-empty -m init
|
|
12
|
+
git -C "$REPO" branch -M main
|
|
13
|
+
mkdir -p "$REPO/.devlog"
|
|
14
|
+
: > "$REPO/.devlog/.enabled"
|
|
15
|
+
export CLAUDE_PROJECT_DIR="$REPO"
|
|
16
|
+
|
|
17
|
+
git -C "$REPO" checkout -q -b fix-issue-123
|
|
18
|
+
|
|
19
|
+
# --- round-start.sh opens a Round in .round-current.md -------------------
|
|
20
|
+
# Round-current split: the open round lives in the shared, non-branch-scoped
|
|
21
|
+
# .devlog/.round-current.md until Stop merges it — it is NOT written directly
|
|
22
|
+
# into the branch-scoped file (that only happens once the round closes).
|
|
23
|
+
OUT="$(echo '{"prompt":"hello"}' | bash "$SCRIPT_DIR/round-start.sh" 2>&1)"
|
|
24
|
+
BRANCH_FILE="$REPO/.devlog/devlog.fix-issue-123.md"
|
|
25
|
+
ROUND_CURRENT="$REPO/.devlog/.round-current.md"
|
|
26
|
+
if [ -f "$ROUND_CURRENT" ] && grep -q '^## Round 1' "$ROUND_CURRENT" && [ ! -f "$BRANCH_FILE" ] && [ ! -f "$REPO/.devlog/devlog.md" ]; then
|
|
27
|
+
echo "PASS: round-start.sh opens Round 1 in .round-current.md, not yet in devlog.fix-issue-123.md"
|
|
28
|
+
else
|
|
29
|
+
echo "FAIL: round-start.sh output=$OUT branch_file_exists=$([ -f "$BRANCH_FILE" ] && echo y || echo n)"
|
|
30
|
+
FAIL=1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# --- segment-watch.sh: Read of .round-current.md is allowed ---------------
|
|
34
|
+
INPUT='{"tool_name":"Read","tool_input":{"file_path":".devlog/.round-current.md"}}'
|
|
35
|
+
if echo "$INPUT" | bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1; then
|
|
36
|
+
echo "PASS: segment-watch.sh allows Read of .round-current.md"
|
|
37
|
+
else
|
|
38
|
+
echo "FAIL: segment-watch.sh denied Read of .round-current.md"
|
|
39
|
+
FAIL=1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# --- segment-watch.sh: its own deny message names .round-current.md, not the
|
|
43
|
+
# branch-scoped file (that's only the merge target, not what's tracked
|
|
44
|
+
# while the round is still open) -----------------------------------------
|
|
45
|
+
# Force a stale segment-state so the silence-timeout path fires. The stored
|
|
46
|
+
# cksum must MATCH .round-current.md as it is right now: a mismatch means
|
|
47
|
+
# "the devlog just changed", which resets the timer and exits 0 instead of
|
|
48
|
+
# ever reaching the deny message we're trying to assert on.
|
|
49
|
+
SEEN_SUM="$(cksum < "$ROUND_CURRENT" | tr -d '\n')"
|
|
50
|
+
cat > "$REPO/.devlog/.segment-state" <<JSON
|
|
51
|
+
{"last_change_epoch": 1, "last_seen_cksum": "$SEEN_SUM", "max_silent_seconds": 1}
|
|
52
|
+
JSON
|
|
53
|
+
DENY_INPUT='{"tool_name":"Bash","tool_input":{"command":"echo hi"}}'
|
|
54
|
+
DENY_MSG="$(echo "$DENY_INPUT" | bash "$SCRIPT_DIR/segment-watch.sh" 2>&1 1>/dev/null)"
|
|
55
|
+
DENY_RC=0
|
|
56
|
+
echo "$DENY_INPUT" | bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>/dev/null || DENY_RC=$?
|
|
57
|
+
if [ "$DENY_RC" -eq 2 ] && printf '%s' "$DENY_MSG" | grep -q '\.round-current\.md' \
|
|
58
|
+
&& ! printf '%s' "$DENY_MSG" | grep -q 'devlog.fix-issue-123.md'; then
|
|
59
|
+
echo "PASS: segment-watch.sh deny message names .round-current.md, not the branch-scoped file"
|
|
60
|
+
else
|
|
61
|
+
echo "FAIL: segment-watch.sh deny message rc=$DENY_RC msg=$DENY_MSG"
|
|
62
|
+
FAIL=1
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# --- enforce-devlog.sh: Stop hook checks the branch-scoped file's hash ----
|
|
66
|
+
echo '{}' | bash "$SCRIPT_DIR/enforce-devlog.sh" >/dev/null 2>&1
|
|
67
|
+
ENFORCE_RC=$?
|
|
68
|
+
if [ "$ENFORCE_RC" -eq 2 ]; then
|
|
69
|
+
echo "PASS: enforce-devlog.sh blocks when the branch-scoped file's Round has no Summary/Handoff yet"
|
|
70
|
+
else
|
|
71
|
+
echo "FAIL: enforce-devlog.sh unexpected rc=$ENFORCE_RC (expected 2, unchanged branch file with only User Input)"
|
|
72
|
+
FAIL=1
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# --- devlog_resolve_paths: reserved name and CJK fallback (Fix 2 / Fix 3) ---
|
|
76
|
+
. "$SCRIPT_DIR/devlog-path.sh"
|
|
77
|
+
|
|
78
|
+
ARCHREPO="$TMP/archrepo"
|
|
79
|
+
mkdir -p "$ARCHREPO"
|
|
80
|
+
git -C "$ARCHREPO" init -q
|
|
81
|
+
git -C "$ARCHREPO" -c user.email=t@t.t -c user.name=t commit -q --allow-empty -m init
|
|
82
|
+
git -C "$ARCHREPO" checkout -q -b archive
|
|
83
|
+
devlog_resolve_paths "$ARCHREPO"
|
|
84
|
+
if [ "$DEVLOG_FILE" = "$ARCHREPO/.devlog/devlog.md" ]; then
|
|
85
|
+
echo "PASS: branch named 'archive' falls back to shared devlog.md, doesn't collide with devlog.archive.md"
|
|
86
|
+
else
|
|
87
|
+
echo "FAIL: 'archive' branch got $DEVLOG_FILE"
|
|
88
|
+
FAIL=1
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
CJKREPO="$TMP/cjkrepo"
|
|
92
|
+
mkdir -p "$CJKREPO"
|
|
93
|
+
git -C "$CJKREPO" init -q
|
|
94
|
+
git -C "$CJKREPO" -c user.email=t@t.t -c user.name=t commit -q --allow-empty -m init
|
|
95
|
+
git -C "$CJKREPO" checkout -q -b 修復登入問題
|
|
96
|
+
devlog_resolve_paths "$CJKREPO"
|
|
97
|
+
case "$DEVLOG_FILE" in
|
|
98
|
+
"$CJKREPO"/.devlog/devlog.b-*.md)
|
|
99
|
+
echo "PASS: all-CJK branch name gets a stable hash-derived file, not shared devlog.md"
|
|
100
|
+
;;
|
|
101
|
+
*)
|
|
102
|
+
echo "FAIL: CJK branch got $DEVLOG_FILE"
|
|
103
|
+
FAIL=1
|
|
104
|
+
;;
|
|
105
|
+
esac
|
|
106
|
+
|
|
107
|
+
if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
|
|
108
|
+
else echo "Some checks FAILED."; exit 1; fi
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
4
|
+
FAIL=0
|
|
5
|
+
TMP="$(mktemp -d)"
|
|
6
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
7
|
+
|
|
8
|
+
OUT="$(CLAUDE_PROJECT_DIR="$TMP" bash "$SCRIPT_DIR/checkpoint-set.sh" 25)"
|
|
9
|
+
[ "$OUT" = "NOT_STARTED" ] && echo "PASS: not started" || { echo "FAIL: $OUT"; FAIL=1; }
|
|
10
|
+
|
|
11
|
+
mkdir -p "$TMP/.devlog"
|
|
12
|
+
touch "$TMP/.devlog/.enabled"
|
|
13
|
+
printf '%s\n' '{"rounds_since_checkpoint": 7, "max_silent_rounds": 20, "checkpoint_marker_count": 2}' \
|
|
14
|
+
> "$TMP/.devlog/.checkpoint-state"
|
|
15
|
+
|
|
16
|
+
OUT="$(CLAUDE_PROJECT_DIR="$TMP" bash "$SCRIPT_DIR/checkpoint-set.sh" 30)"
|
|
17
|
+
case "$OUT" in *CHECKPOINT_MAX_SILENT_ROUNDS=30*) echo "PASS: set 30" ;; *) echo "FAIL: $OUT"; FAIL=1 ;; esac
|
|
18
|
+
grep -q '"rounds_since_checkpoint": 7' "$TMP/.devlog/.checkpoint-state" \
|
|
19
|
+
&& echo "PASS: rounds preserved" || { echo "FAIL: rounds reset"; FAIL=1; }
|
|
20
|
+
grep -q '"checkpoint_marker_count": 2' "$TMP/.devlog/.checkpoint-state" \
|
|
21
|
+
&& echo "PASS: markers preserved" || { echo "FAIL: markers reset"; FAIL=1; }
|
|
22
|
+
grep -q '"max_silent_rounds": 30' "$TMP/.devlog/.checkpoint-state" \
|
|
23
|
+
&& echo "PASS: max written" || { echo "FAIL: max"; FAIL=1; }
|
|
24
|
+
|
|
25
|
+
CLAUDE_PROJECT_DIR="$TMP" bash "$SCRIPT_DIR/checkpoint-set.sh" 0 >/dev/null 2>&1
|
|
26
|
+
[ $? -eq 1 ] && echo "PASS: reject 0" || { echo "FAIL: accepted 0"; FAIL=1; }
|
|
27
|
+
|
|
28
|
+
# create when missing
|
|
29
|
+
rm -f "$TMP/.devlog/.checkpoint-state"
|
|
30
|
+
OUT="$(CLAUDE_PROJECT_DIR="$TMP" bash "$SCRIPT_DIR/checkpoint-set.sh" 15)"
|
|
31
|
+
case "$OUT" in *CHECKPOINT_MAX_SILENT_ROUNDS=15*) echo "PASS: create 15" ;; *) echo "FAIL: create $OUT"; FAIL=1 ;; esac
|
|
32
|
+
grep -q '"rounds_since_checkpoint": 0' "$TMP/.devlog/.checkpoint-state" \
|
|
33
|
+
&& echo "PASS: create rounds 0" || { echo "FAIL: create rounds"; FAIL=1; }
|
|
34
|
+
|
|
35
|
+
if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
|
|
36
|
+
else echo "Some checks FAILED."; exit 1; fi
|