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,72 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
|
|
4
|
+
_src="${BASH_SOURCE[0]}"
|
|
5
|
+
SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
|
|
6
|
+
. "$SCRIPT_DIR/devlog-md.sh"
|
|
7
|
+
. "$SCRIPT_DIR/devlog-lock.sh"
|
|
8
|
+
. "$SCRIPT_DIR/devlog-path.sh"
|
|
9
|
+
|
|
10
|
+
devlog_resolve_paths "${CLAUDE_PROJECT_DIR:-.}"
|
|
11
|
+
devlog_lock_acquire
|
|
12
|
+
trap 'devlog_lock_release' EXIT
|
|
13
|
+
MAIN="$DEVLOG_FILE"
|
|
14
|
+
ARCHIVE="$DEVLOG_DIR/devlog.archive.md"
|
|
15
|
+
[ -f "$MAIN" ] || { echo "devlog.md 不存在" >&2; exit 1; }
|
|
16
|
+
|
|
17
|
+
TMP="$(mktemp -d "${TMPDIR:-/tmp}/devlog-compact.XXXXXX")" || exit 1
|
|
18
|
+
trap 'rm -rf "$TMP"; devlog_lock_release' EXIT
|
|
19
|
+
STARTS="$TMP/starts"
|
|
20
|
+
MOVE="$TMP/move"
|
|
21
|
+
CANDIDATES="$TMP/candidates"
|
|
22
|
+
MOVED_BLOCKS="$TMP/moved"
|
|
23
|
+
NEW_MAIN="$TMP/main"
|
|
24
|
+
devlog_list_round_starts "$MAIN" > "$STARTS"
|
|
25
|
+
: > "$MOVE"
|
|
26
|
+
: > "$CANDIDATES"
|
|
27
|
+
|
|
28
|
+
TOTAL="$(wc -l < "$STARTS" | tr -d ' ')"
|
|
29
|
+
while read -r start _; do
|
|
30
|
+
[ -n "${start:-}" ] || continue
|
|
31
|
+
end="$(devlog_block_end "$MAIN" "$start")"
|
|
32
|
+
status="$(devlog_round_status "$MAIN" "$start" "$end")"
|
|
33
|
+
case "$status" in
|
|
34
|
+
IN_PROGRESS|BLOCKED|INTERRUPTED) ;;
|
|
35
|
+
*) printf '%s\n' "$start" >> "$CANDIDATES" ;;
|
|
36
|
+
esac
|
|
37
|
+
done < "$STARTS"
|
|
38
|
+
|
|
39
|
+
COMPLETED="$(wc -l < "$CANDIDATES" | tr -d ' ')"
|
|
40
|
+
TO_MOVE=$((COMPLETED > 5 ? COMPLETED - 5 : 0))
|
|
41
|
+
awk -v count="$TO_MOVE" 'NR <= count { print }' "$CANDIDATES" > "$MOVE"
|
|
42
|
+
MOVED="$(wc -l < "$MOVE" | tr -d ' ')"
|
|
43
|
+
if [ "$MOVED" -eq 0 ]; then
|
|
44
|
+
ARCHIVE_COUNT=0
|
|
45
|
+
[ ! -f "$ARCHIVE" ] || ARCHIVE_COUNT="$(devlog_list_round_starts "$ARCHIVE" | wc -l | tr -d ' ')"
|
|
46
|
+
printf 'MOVED=0 REMAINING=%s ARCHIVE=%s\n' "$TOTAL" "$ARCHIVE_COUNT"
|
|
47
|
+
exit 0
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
awk -v move_file="$MOVE" -v moved_file="$MOVED_BLOCKS" '
|
|
51
|
+
BEGIN {
|
|
52
|
+
while ((getline line < move_file) > 0) move[line] = 1
|
|
53
|
+
}
|
|
54
|
+
/^[ \t]*```/ { fence = !fence }
|
|
55
|
+
!fence && /^## / {
|
|
56
|
+
moving = (NR in move)
|
|
57
|
+
}
|
|
58
|
+
moving { print >> moved_file; next }
|
|
59
|
+
{ print }
|
|
60
|
+
' "$MAIN" > "$NEW_MAIN" || exit 1
|
|
61
|
+
|
|
62
|
+
while read -r start; do
|
|
63
|
+
heading="$(awk -v n="$start" 'NR == n { print; exit }' "$MAIN")"
|
|
64
|
+
grep -Fqx "$heading" "$MOVED_BLOCKS" || { echo "archive 驗證失敗" >&2; exit 1; }
|
|
65
|
+
done < "$MOVE"
|
|
66
|
+
|
|
67
|
+
cat "$MOVED_BLOCKS" >> "$ARCHIVE" || exit 1
|
|
68
|
+
mv "$NEW_MAIN" "$MAIN" || exit 1
|
|
69
|
+
|
|
70
|
+
REMAINING="$(devlog_list_round_starts "$MAIN" | wc -l | tr -d ' ')"
|
|
71
|
+
ARCHIVE_COUNT="$(devlog_list_round_starts "$ARCHIVE" | wc -l | tr -d ' ')"
|
|
72
|
+
printf 'MOVED=%s REMAINING=%s ARCHIVE=%s\n' "$MOVED" "$REMAINING" "$ARCHIVE_COUNT"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# shellcheck shell=bash
|
|
2
|
+
# sourced by hook scripts. Do not execute.
|
|
3
|
+
#
|
|
4
|
+
# Best-effort only, used to make the dangling-heal stub message more useful —
|
|
5
|
+
# not part of the enable/detection logic itself. 專案原則是不解析 transcript
|
|
6
|
+
# 來判斷「這輪有沒有記錄」(見 SKILL.md),這裡不違反:偵測不到/沒有 jq/
|
|
7
|
+
# 檔案讀不到,一律回傳空字串,退回原本的通用 stub 文字,不影響任何強制流程。
|
|
8
|
+
#
|
|
9
|
+
# detect_pending_question <transcript_path>
|
|
10
|
+
# 印出 "1" 代表:transcript 裡最後一筆 assistant/user 訊息,是一個還沒拿到
|
|
11
|
+
# 回答的 AskUserQuestion tool_use(also 是整份 transcript 的最後一筆有意義
|
|
12
|
+
# 紀錄)——最典型的訊號是「process 在等使用者選答案時被中斷」。其餘一律印出
|
|
13
|
+
# 空字串。
|
|
14
|
+
detect_pending_question() {
|
|
15
|
+
local transcript="$1"
|
|
16
|
+
[ -n "$transcript" ] || { echo ''; return 0; }
|
|
17
|
+
[ -f "$transcript" ] || { echo ''; return 0; }
|
|
18
|
+
command -v jq >/dev/null 2>&1 || { echo ''; return 0; }
|
|
19
|
+
tail -n 200 "$transcript" 2>/dev/null | jq -rs '
|
|
20
|
+
map(select(.type == "assistant" or .type == "user"))
|
|
21
|
+
| if length == 0 then ""
|
|
22
|
+
else
|
|
23
|
+
(.[-1]) as $last
|
|
24
|
+
| if $last.type == "assistant"
|
|
25
|
+
and ($last.message.content // [] | any(.type == "tool_use" and .name == "AskUserQuestion"))
|
|
26
|
+
then "1"
|
|
27
|
+
else ""
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
' 2>/dev/null || echo ''
|
|
31
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Sourced helper for serializing writes under .devlog.
|
|
3
|
+
devlog_lock_acquire() {
|
|
4
|
+
local dir="${DEVLOG_DIR:-.}/.lock"
|
|
5
|
+
local start now
|
|
6
|
+
LOCK_HELD=0
|
|
7
|
+
command -v mkdir >/dev/null 2>&1 || return 0
|
|
8
|
+
command -v rmdir >/dev/null 2>&1 || return 0
|
|
9
|
+
command -v date >/dev/null 2>&1 || return 0
|
|
10
|
+
command -v sleep >/dev/null 2>&1 || return 0
|
|
11
|
+
start="$(date +%s 2>/dev/null || echo 0)"
|
|
12
|
+
while true; do
|
|
13
|
+
if mkdir "$dir" 2>/dev/null; then
|
|
14
|
+
LOCK_HELD=1
|
|
15
|
+
return 0
|
|
16
|
+
fi
|
|
17
|
+
now="$(date +%s 2>/dev/null || echo 0)"
|
|
18
|
+
if [ $((now - start)) -ge 2 ]; then
|
|
19
|
+
return 0
|
|
20
|
+
fi
|
|
21
|
+
sleep 0.1 2>/dev/null || true
|
|
22
|
+
done
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
devlog_lock_release() {
|
|
26
|
+
if [ "${LOCK_HELD:-0}" -eq 1 ]; then
|
|
27
|
+
rmdir "${DEVLOG_DIR:-.}/.lock" 2>/dev/null || true
|
|
28
|
+
LOCK_HELD=0
|
|
29
|
+
fi
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
devlog_with_lock() {
|
|
33
|
+
devlog_lock_acquire
|
|
34
|
+
"$@"
|
|
35
|
+
local result=$?
|
|
36
|
+
devlog_lock_release
|
|
37
|
+
return "$result"
|
|
38
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
_DEVLOG_MD_DIR="$(cd "${BASH_SOURCE[0]%/*}" && pwd)"
|
|
4
|
+
|
|
5
|
+
# NOFENCE 防呆(跟 enforce-devlog.sh 同一套邏輯):如果 [start,end] 這個範圍
|
|
6
|
+
# 內 ``` 記號數量是奇數,代表某處圍欄沒有正常收尾(例如 User Input 貼了一段
|
|
7
|
+
# 忘記關閉 fence 的程式碼)——這是真的寫錯了,不是刻意的範例。此時呼叫端的
|
|
8
|
+
# fence 變數若照常 toggle,會在剩下的內容裡卡在 1,把明明存在的 #### 工作區
|
|
9
|
+
# / ### Status / ### 段落 誤判成「被吃掉、看起來是空的」,讓
|
|
10
|
+
# workspace_claim_state 靜默回報 NO_CLAIM(本來該是 MISMATCH),整個漂移
|
|
11
|
+
# 偵測形同虛設。回傳 1 時呼叫端要讓 fence 變數維持 0(退化回單純掃描),
|
|
12
|
+
# 圍欄成雙成對(含 0 個)時回傳 0,fence-aware 行為不變。
|
|
13
|
+
_devlog_fence_nofence() {
|
|
14
|
+
local file="$1" start="$2" end="$3" count
|
|
15
|
+
count="$(awk -v start="$start" -v end="$end" '
|
|
16
|
+
NR < start || NR > end { next }
|
|
17
|
+
/^[ \t]*```/ { c++ }
|
|
18
|
+
END { print c + 0 }
|
|
19
|
+
' "$file")"
|
|
20
|
+
if [ $((count % 2)) -eq 0 ]; then printf '0\n'; else printf '1\n'; fi
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
devlog_list_round_starts() {
|
|
24
|
+
awk '
|
|
25
|
+
/^[ \t]*```/ { fence = !fence; next }
|
|
26
|
+
!fence && /^## Round [0-9]+/ {
|
|
27
|
+
round = $0
|
|
28
|
+
sub(/^## Round /, "", round)
|
|
29
|
+
sub(/[^0-9].*$/, "", round)
|
|
30
|
+
print NR, round
|
|
31
|
+
}
|
|
32
|
+
' "$1"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
devlog_block_end() {
|
|
36
|
+
awk -v start="$2" '
|
|
37
|
+
NR < start { next }
|
|
38
|
+
/^[ \t]*```/ { fence = !fence }
|
|
39
|
+
NR > start && !fence && /^## / { print NR - 1; found = 1; exit }
|
|
40
|
+
END { if (!found) print NR }
|
|
41
|
+
' "$1"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
devlog_round_status() {
|
|
45
|
+
local nofence
|
|
46
|
+
nofence="$(_devlog_fence_nofence "$1" "$2" "$3")"
|
|
47
|
+
awk -v start="$2" -v end="$3" -v nofence="$nofence" '
|
|
48
|
+
NR < start || NR > end { next }
|
|
49
|
+
/^[ \t]*```/ { if (!nofence) fence = !fence; next }
|
|
50
|
+
!fence && /^### Status[[:space:]]*$/ { status = 1; value = ""; next }
|
|
51
|
+
!fence && /^### / && status { status = 0 }
|
|
52
|
+
status && $0 !~ /^[[:space:]]*$/ { value = $0 }
|
|
53
|
+
END { print value }
|
|
54
|
+
' "$1"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
devlog_count_segments() {
|
|
58
|
+
awk -v start="$2" -v end="$3" '
|
|
59
|
+
NR < start || NR > end { next }
|
|
60
|
+
/^[ \t]*```/ { fence = !fence; next }
|
|
61
|
+
!fence && /^### 段落 / { count++ }
|
|
62
|
+
END { print count + 0 }
|
|
63
|
+
' "$1"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
devlog_insert_before_summary() {
|
|
67
|
+
local file="$1" start="$2" end="$3" text_file="$4"
|
|
68
|
+
awk -v start="$start" -v end="$end" -v textfile="$text_file" '
|
|
69
|
+
BEGIN {
|
|
70
|
+
inserted = 0
|
|
71
|
+
while ((getline line < textfile) > 0) {
|
|
72
|
+
ins[ni++] = line
|
|
73
|
+
}
|
|
74
|
+
close(textfile)
|
|
75
|
+
}
|
|
76
|
+
{
|
|
77
|
+
if (!inserted && NR >= start && NR <= end && !fence && $0 ~ /^### Summary[[:space:]]*$/) {
|
|
78
|
+
for (i = 0; i < ni; i++) print ins[i]
|
|
79
|
+
inserted = 1
|
|
80
|
+
}
|
|
81
|
+
if ($0 ~ /^[ \t]*```/) fence = !fence
|
|
82
|
+
print
|
|
83
|
+
}
|
|
84
|
+
' "$file"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
devlog_kept_index_lines() {
|
|
88
|
+
awk '
|
|
89
|
+
/^[ \t]*```/ { fence = !fence; next }
|
|
90
|
+
!fence && /^## Kept 索引/ { grab = 1; found = 1; buf = ""; next }
|
|
91
|
+
!fence && grab && /^## / { grab = 0 }
|
|
92
|
+
grab { buf = buf $0 ORS }
|
|
93
|
+
END { if (found) printf "%s", buf }
|
|
94
|
+
' "$1"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
devlog_strip_kept_index() {
|
|
98
|
+
# Pipe through a second pass that buffers blank lines and only emits them
|
|
99
|
+
# once a non-blank line follows. Kept 索引 is always the trailing section
|
|
100
|
+
# (keep-move.sh appends it last), so any blank line(s) left as its
|
|
101
|
+
# separator are always at true EOF here and get dropped instead of
|
|
102
|
+
# surviving into the caller's rebuild — this is what stops the separator
|
|
103
|
+
# from growing by one line on every keep-move.sh call.
|
|
104
|
+
awk '
|
|
105
|
+
/^[ \t]*```/ { fence = !fence }
|
|
106
|
+
!fence && /^## Kept 索引/ { grab = 1; next }
|
|
107
|
+
!fence && grab && /^## / { grab = 0 }
|
|
108
|
+
grab { next }
|
|
109
|
+
{ print }
|
|
110
|
+
' "$1" | awk '
|
|
111
|
+
/^[ \t]*$/ { pending = pending $0 ORS; next }
|
|
112
|
+
{ printf "%s", pending; pending = ""; print }
|
|
113
|
+
' > "$2"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
devlog_lessons_index_lines() {
|
|
117
|
+
awk '
|
|
118
|
+
/^[ \t]*```/ { fence = !fence; next }
|
|
119
|
+
!fence && /^## Lessons 索引/ { grab = 1; found = 1; buf = ""; next }
|
|
120
|
+
!fence && grab && /^## / { grab = 0 }
|
|
121
|
+
grab { buf = buf $0 ORS }
|
|
122
|
+
END { if (found) printf "%s", buf }
|
|
123
|
+
' "$1"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
devlog_strip_lessons_index() {
|
|
127
|
+
# Same trailing-block assumption and blank-line-collapse behavior as
|
|
128
|
+
# devlog_strip_kept_index() above (docs/design/lessons-mode.md).
|
|
129
|
+
awk '
|
|
130
|
+
/^[ \t]*```/ { fence = !fence }
|
|
131
|
+
!fence && /^## Lessons 索引/ { grab = 1; next }
|
|
132
|
+
!fence && grab && /^## / { grab = 0 }
|
|
133
|
+
grab { next }
|
|
134
|
+
{ print }
|
|
135
|
+
' "$1" | awk '
|
|
136
|
+
/^[ \t]*$/ { pending = pending $0 ORS; next }
|
|
137
|
+
{ printf "%s", pending; pending = ""; print }
|
|
138
|
+
' > "$2"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
devlog_round_workspace_body() {
|
|
142
|
+
local nofence
|
|
143
|
+
nofence="$(_devlog_fence_nofence "$1" "$2" "$3")"
|
|
144
|
+
awk -v start="$2" -v end="$3" -v nofence="$nofence" '
|
|
145
|
+
NR < start || NR > end { next }
|
|
146
|
+
/^[ \t]*```/ { if (!nofence) fence = !fence; next }
|
|
147
|
+
!fence && /^#### 工作區[[:space:]]*$/ { grab = 1; next }
|
|
148
|
+
grab && !fence && /^#### / { grab = 0 }
|
|
149
|
+
grab && !fence && /^### / { grab = 0 }
|
|
150
|
+
grab && !fence && /^## / { grab = 0 }
|
|
151
|
+
grab { print }
|
|
152
|
+
' "$1" | sed -e '/^[[:space:]]*$/d'
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
devlog_round_segments_body() {
|
|
156
|
+
local nofence
|
|
157
|
+
nofence="$(_devlog_fence_nofence "$1" "$2" "$3")"
|
|
158
|
+
awk -v start="$2" -v end="$3" -v nofence="$nofence" '
|
|
159
|
+
NR < start || NR > end { next }
|
|
160
|
+
/^[ \t]*```/ {
|
|
161
|
+
if (segment) print
|
|
162
|
+
if (!nofence) fence = !fence
|
|
163
|
+
next
|
|
164
|
+
}
|
|
165
|
+
!fence && /^### 段落 / { segment = 1; next }
|
|
166
|
+
segment && !fence && /^### / { segment = 0 }
|
|
167
|
+
segment && !fence && /^## / { segment = 0 }
|
|
168
|
+
segment { print }
|
|
169
|
+
' "$1"
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
devlog_merge_round_current() {
|
|
173
|
+
# Appends $2's content onto $1 (one blank line separator, matching the
|
|
174
|
+
# existing "\n## Round N" append convention) and removes $2. No-op if $2
|
|
175
|
+
# is absent or empty — nothing to merge.
|
|
176
|
+
local devlog="$1" current="$2"
|
|
177
|
+
[ -s "$current" ] || return 0
|
|
178
|
+
{
|
|
179
|
+
printf '\n'
|
|
180
|
+
cat "$current"
|
|
181
|
+
} >> "$devlog" 2>/dev/null || return 1
|
|
182
|
+
rm -f "$current" 2>/dev/null || true
|
|
183
|
+
return 0
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
devlog_reopen_last_round() {
|
|
187
|
+
# Moves the last "## Round N ..." block out of $1 into $2 (creating $2),
|
|
188
|
+
# removing those lines from $1. Returns 1 and touches neither file if $1
|
|
189
|
+
# has no round to move.
|
|
190
|
+
local devlog="$1" current="$2" start end
|
|
191
|
+
start="$(devlog_list_round_starts "$devlog" | awk 'END { print $1 }')"
|
|
192
|
+
[ -n "$start" ] || return 1
|
|
193
|
+
end="$(devlog_block_end "$devlog" "$start")"
|
|
194
|
+
awk -v start="$start" -v end="$end" 'NR >= start && NR <= end' "$devlog" > "$current" 2>/dev/null || return 1
|
|
195
|
+
awk -v start="$start" -v end="$end" 'NR < start || NR > end' "$devlog" > "$devlog.tmp" 2>/dev/null \
|
|
196
|
+
&& mv "$devlog.tmp" "$devlog" 2>/dev/null || { rm -f "$devlog.tmp" "$current" 2>/dev/null; return 1; }
|
|
197
|
+
return 0
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
workspace_claim_state() {
|
|
201
|
+
local dir="$1" file="$2" start end status claimed live
|
|
202
|
+
if ! type workspace_snapshot >/dev/null 2>&1; then
|
|
203
|
+
# shellcheck source=workspace-snapshot.sh
|
|
204
|
+
. "$_DEVLOG_MD_DIR/workspace-snapshot.sh"
|
|
205
|
+
fi
|
|
206
|
+
start="$(devlog_list_round_starts "$file" | awk 'END { print $1 }')"
|
|
207
|
+
[ -n "$start" ] || { printf 'NO_CLAIM\n'; return 0; }
|
|
208
|
+
end="$(devlog_block_end "$file" "$start")"
|
|
209
|
+
status="$(devlog_round_status "$file" "$start" "$end")"
|
|
210
|
+
case "$status" in
|
|
211
|
+
'[reason:'*) status=INTERRUPTED ;;
|
|
212
|
+
esac
|
|
213
|
+
case "$status" in
|
|
214
|
+
IN_PROGRESS|BLOCKED|INTERRUPTED) ;;
|
|
215
|
+
*) printf 'NO_CLAIM\n'; return 0 ;;
|
|
216
|
+
esac
|
|
217
|
+
claimed="$(devlog_round_workspace_body "$file" "$start" "$end")"
|
|
218
|
+
[ -n "$claimed" ] || { printf 'NO_CLAIM\n'; return 0; }
|
|
219
|
+
live="$(workspace_snapshot "$dir")"
|
|
220
|
+
[ -n "$live" ] || { printf 'NO_CLAIM\n'; return 0; }
|
|
221
|
+
if [ "$claimed" = "$live" ]; then printf 'MATCH\n'; else printf 'MISMATCH\n'; fi
|
|
222
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Sourced helper: resolves the branch-scoped devlog file for the branch
|
|
3
|
+
# currently checked out in a working directory. See
|
|
4
|
+
# docs/design/branch-scoped-devlog.md.
|
|
5
|
+
|
|
6
|
+
_DEVLOG_PATH_DIR="$(cd "${BASH_SOURCE[0]%/*}" && pwd)"
|
|
7
|
+
# shellcheck source=devlog-lock.sh
|
|
8
|
+
. "$_DEVLOG_PATH_DIR/devlog-lock.sh"
|
|
9
|
+
|
|
10
|
+
# Turns an arbitrary branch/worktree name into a safe devlog.<name>.md
|
|
11
|
+
# filename segment: anything outside [A-Za-z0-9._-] becomes '-', repeats
|
|
12
|
+
# collapse, leading/trailing '-' trimmed.
|
|
13
|
+
_devlog_sanitize_name() {
|
|
14
|
+
printf '%s' "$1" | sed -E 's/[^A-Za-z0-9._-]/-/g; s/-+/-/g; s/^-+//; s/-+$//'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
# Sets DEVLOG_DIR and DEVLOG_FILE for the branch currently checked out in
|
|
18
|
+
# $1 (defaults to "."). main/master (any case) and anything that isn't a
|
|
19
|
+
# git repo or has no resolvable branch keep the shared devlog.md. Any
|
|
20
|
+
# other branch gets devlog.<sanitized-branch>.md; a detached HEAD (or an
|
|
21
|
+
# unborn branch, which git also reports as "HEAD" here) falls back to the
|
|
22
|
+
# working directory's own basename. The first time a branch resolves to a
|
|
23
|
+
# file that doesn't exist yet while devlog.md already has content, the
|
|
24
|
+
# existing devlog.md is renamed (not copied) into that branch's file.
|
|
25
|
+
devlog_resolve_paths() {
|
|
26
|
+
local dir="${1:-.}"
|
|
27
|
+
DEVLOG_DIR="$dir/.devlog"
|
|
28
|
+
DEVLOG_FILE="$DEVLOG_DIR/devlog.md"
|
|
29
|
+
|
|
30
|
+
local branch raw name
|
|
31
|
+
branch="$(git -C "$dir" rev-parse --abbrev-ref HEAD 2>/dev/null || echo '')"
|
|
32
|
+
|
|
33
|
+
case "$branch" in
|
|
34
|
+
''|[Mm][Aa][Ii][Nn]|[Mm][Aa][Ss][Tt][Ee][Rr])
|
|
35
|
+
return 0
|
|
36
|
+
;;
|
|
37
|
+
HEAD)
|
|
38
|
+
raw="$(basename "$(cd "$dir" 2>/dev/null && pwd)" 2>/dev/null || echo '')"
|
|
39
|
+
;;
|
|
40
|
+
*)
|
|
41
|
+
raw="$branch"
|
|
42
|
+
;;
|
|
43
|
+
esac
|
|
44
|
+
[ -n "$raw" ] || return 0
|
|
45
|
+
|
|
46
|
+
name="$(_devlog_sanitize_name "$raw")"
|
|
47
|
+
if [ -z "$name" ]; then
|
|
48
|
+
# Sanitizing stripped every character (e.g. an all-CJK branch name).
|
|
49
|
+
# Falling back to the shared devlog.md here would silently defeat this
|
|
50
|
+
# whole feature for exactly the case it's most likely to hit in a
|
|
51
|
+
# Chinese-language project. Derive a short, stable, ASCII-safe name
|
|
52
|
+
# from the original string instead.
|
|
53
|
+
name="b-$(printf '%s' "$raw" | cksum | awk '{print $1}')"
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Reserved namespace: devlog.archive.md and devlog.lessons.*.md already
|
|
57
|
+
# mean something else in this plugin. A branch name that sanitizes to
|
|
58
|
+
# one of these shares the plain devlog.md instead of colliding with them.
|
|
59
|
+
case "$name" in
|
|
60
|
+
archive|lessons.*) return 0 ;;
|
|
61
|
+
esac
|
|
62
|
+
|
|
63
|
+
local resolved="$DEVLOG_DIR/devlog.$name.md"
|
|
64
|
+
if [ -f "$DEVLOG_DIR/.enabled" ] && [ ! -f "$resolved" ] && [ -f "$DEVLOG_FILE" ]; then
|
|
65
|
+
devlog_lock_acquire
|
|
66
|
+
mv "$DEVLOG_FILE" "$resolved" 2>/dev/null || true
|
|
67
|
+
devlog_lock_release
|
|
68
|
+
fi
|
|
69
|
+
DEVLOG_FILE="$resolved"
|
|
70
|
+
}
|