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.
Files changed (120) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +234 -0
  3. package/bin/devlog-tracker.js +51 -0
  4. package/cli/init.js +55 -0
  5. package/cli/init.test.js +37 -0
  6. package/cli/merge-hooks.js +60 -0
  7. package/cli/merge-hooks.test.js +187 -0
  8. package/cli/platforms/codex.js +13 -0
  9. package/cli/platforms/cursor.js +13 -0
  10. package/cli/status.js +14 -0
  11. package/cli/status.test.js +33 -0
  12. package/cli/vendor.js +37 -0
  13. package/cli/vendor.test.js +84 -0
  14. package/codex/hooks/on-pre-tool.sh +19 -0
  15. package/codex/hooks/on-session-end.sh +31 -0
  16. package/codex/hooks/on-session-start.sh +37 -0
  17. package/codex/hooks/on-stop.sh +18 -0
  18. package/codex/hooks/on-user-prompt-submit.sh +15 -0
  19. package/codex/hooks/project-dir.sh +13 -0
  20. package/codex/hooks/test-adapters.sh +98 -0
  21. package/codex/hooks.json +54 -0
  22. package/commands/checkpoint.md +17 -0
  23. package/commands/clean.md +50 -0
  24. package/commands/compact.md +24 -0
  25. package/commands/continue.md +30 -0
  26. package/commands/keep.md +100 -0
  27. package/commands/lessons-drift.md +19 -0
  28. package/commands/lessons-off.md +16 -0
  29. package/commands/lessons-on.md +20 -0
  30. package/commands/lessons.md +20 -0
  31. package/commands/overview.md +32 -0
  32. package/commands/pause.md +20 -0
  33. package/commands/resume.md +18 -0
  34. package/commands/segment-watch.md +21 -0
  35. package/commands/span.md +13 -0
  36. package/commands/start.md +19 -0
  37. package/commands/status.md +14 -0
  38. package/cursor/hooks/on-pre-tool.sh +24 -0
  39. package/cursor/hooks/on-session-end.sh +11 -0
  40. package/cursor/hooks/on-session-start.sh +15 -0
  41. package/cursor/hooks/on-stop.sh +44 -0
  42. package/cursor/hooks/on-submit-prompt.sh +28 -0
  43. package/cursor/hooks/on-tool-failure.sh +16 -0
  44. package/cursor/hooks/project-dir.sh +10 -0
  45. package/cursor/hooks/test-adapters.sh +201 -0
  46. package/cursor/hooks.json +26 -0
  47. package/hooks/scripts/await-open.sh +23 -0
  48. package/hooks/scripts/checkpoint-set.sh +44 -0
  49. package/hooks/scripts/clean-devlog.sh +69 -0
  50. package/hooks/scripts/close-open-round.sh +186 -0
  51. package/hooks/scripts/compact-devlog.sh +72 -0
  52. package/hooks/scripts/detect-pending-question.sh +31 -0
  53. package/hooks/scripts/devlog-lock.sh +38 -0
  54. package/hooks/scripts/devlog-md.sh +222 -0
  55. package/hooks/scripts/devlog-path.sh +70 -0
  56. package/hooks/scripts/enforce-devlog.sh +629 -0
  57. package/hooks/scripts/files-snapshot.sh +70 -0
  58. package/hooks/scripts/json-field.sh +76 -0
  59. package/hooks/scripts/keep-move.sh +155 -0
  60. package/hooks/scripts/kept-list.sh +40 -0
  61. package/hooks/scripts/lessons-append.sh +93 -0
  62. package/hooks/scripts/lessons-drift-set.sh +42 -0
  63. package/hooks/scripts/lessons-off.sh +15 -0
  64. package/hooks/scripts/lessons-on.sh +21 -0
  65. package/hooks/scripts/lessons-read.sh +60 -0
  66. package/hooks/scripts/on-session-end.sh +14 -0
  67. package/hooks/scripts/on-stop-failure.sh +18 -0
  68. package/hooks/scripts/on-tool-failure.sh +25 -0
  69. package/hooks/scripts/pause-devlog.sh +13 -0
  70. package/hooks/scripts/redact-prompt.sh +19 -0
  71. package/hooks/scripts/resume-devlog.sh +41 -0
  72. package/hooks/scripts/round-start.sh +333 -0
  73. package/hooks/scripts/run-tests.sh +26 -0
  74. package/hooks/scripts/segment-watch-set.sh +47 -0
  75. package/hooks/scripts/segment-watch.sh +197 -0
  76. package/hooks/scripts/session-start-devlog.sh +157 -0
  77. package/hooks/scripts/span-close.sh +9 -0
  78. package/hooks/scripts/span-open.sh +21 -0
  79. package/hooks/scripts/start-devlog.sh +24 -0
  80. package/hooks/scripts/status-devlog.sh +43 -0
  81. package/hooks/scripts/tests/test-await-open.sh +112 -0
  82. package/hooks/scripts/tests/test-branch-scoped-integration.sh +108 -0
  83. package/hooks/scripts/tests/test-checkpoint-set.sh +36 -0
  84. package/hooks/scripts/tests/test-clean-devlog.sh +160 -0
  85. package/hooks/scripts/tests/test-cli-init-e2e.sh +37 -0
  86. package/hooks/scripts/tests/test-close-open-round.sh +325 -0
  87. package/hooks/scripts/tests/test-compact-devlog.sh +62 -0
  88. package/hooks/scripts/tests/test-devlog-lock.sh +37 -0
  89. package/hooks/scripts/tests/test-devlog-md.sh +530 -0
  90. package/hooks/scripts/tests/test-devlog-path.sh +118 -0
  91. package/hooks/scripts/tests/test-enforce-devlog-files.sh +233 -0
  92. package/hooks/scripts/tests/test-enforce-devlog-handoff-order.sh +223 -0
  93. package/hooks/scripts/tests/test-enforce-devlog-workspace.sh +333 -0
  94. package/hooks/scripts/tests/test-enforce-devlog.sh +1290 -0
  95. package/hooks/scripts/tests/test-files-snapshot.sh +142 -0
  96. package/hooks/scripts/tests/test-json-field.sh +57 -0
  97. package/hooks/scripts/tests/test-keep-move.sh +127 -0
  98. package/hooks/scripts/tests/test-kept-list.sh +80 -0
  99. package/hooks/scripts/tests/test-lessons-append.sh +138 -0
  100. package/hooks/scripts/tests/test-lessons-drift-set.sh +36 -0
  101. package/hooks/scripts/tests/test-lessons-on-off.sh +75 -0
  102. package/hooks/scripts/tests/test-lessons-read.sh +97 -0
  103. package/hooks/scripts/tests/test-on-interrupt.sh +186 -0
  104. package/hooks/scripts/tests/test-redact-prompt.sh +27 -0
  105. package/hooks/scripts/tests/test-resume-devlog.sh +35 -0
  106. package/hooks/scripts/tests/test-round-start.sh +825 -0
  107. package/hooks/scripts/tests/test-segment-watch-set.sh +75 -0
  108. package/hooks/scripts/tests/test-segment-watch.sh +468 -0
  109. package/hooks/scripts/tests/test-session-start-devlog.sh +508 -0
  110. package/hooks/scripts/tests/test-start-pause-devlog.sh +84 -0
  111. package/hooks/scripts/tests/test-status-span.sh +50 -0
  112. package/hooks/scripts/tests/test-workspace-snapshot.sh +130 -0
  113. package/hooks/scripts/workspace-snapshot.sh +69 -0
  114. package/package.json +37 -0
  115. package/skills/devlog-tracker/SKILL.md +411 -0
  116. package/skills/devlog-tracker/references/checkpoint-mode.md +42 -0
  117. package/skills/devlog-tracker/references/lessons-mode.md +32 -0
  118. package/skills/devlog-tracker/references/reply-fold.md +129 -0
  119. package/skills/devlog-tracker/references/round-segments.md +55 -0
  120. package/skills/devlog-tracker/references/span-mode.md +65 -0
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bash
2
+ # shellcheck shell=bash
3
+ # Sourced by enforce-devlog.sh (docs/design/files-verify.md, devlog ssot
4
+ # Phase 4). Computes the canonical #### 檔案 category lines — 新增/修改/刪除,
5
+ # comma-joined per category, omitting empty ones — for either the current
6
+ # uncommitted tree state or a single commit, so enforce-devlog.sh can
7
+ # compare Claude's claim against something computed independently.
8
+ #
9
+ # Usage:
10
+ # files_snapshot "$PROJECT_DIR" # uncommitted branch (git status)
11
+ # files_snapshot "$PROJECT_DIR" "$HASH" # committed branch (git diff-tree)
12
+ #
13
+ # Echoes up to three lines in fixed order (新增, then 修改, then 刪除).
14
+ # Echoes nothing if there's nothing to report outside .devlog/, or if $dir
15
+ # isn't a git repo, or (committed branch) $HASH doesn't resolve to a commit
16
+ # — all fail-open, matching this hook suite's design.
17
+ #
18
+ # Renames are never detected: no -M/--find-renames anywhere below (per
19
+ # docs/design/files-verify.md Decision 5), so git reports a rename as a
20
+ # plain delete+add pair, which is exactly the desired 刪除+新增 split.
21
+ # A status code this function doesn't specifically recognize (e.g. a copy,
22
+ # git status's `C`) is categorized conservatively as 修改 — see
23
+ # docs/design/files-verify.md's Known Limitations.
24
+ #
25
+ # Known limitation: a changed path containing a space is not exactly
26
+ # reversible from the comma-joined output below — same limitation
27
+ # workspace-snapshot.sh's 未提交 list already has.
28
+ files_snapshot() {
29
+ local dir="${1:-.}" hash="${2:-}"
30
+ if ! git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
31
+ return 0
32
+ fi
33
+
34
+ local raw
35
+ if [ -n "$hash" ]; then
36
+ git -C "$dir" rev-parse --verify -q "${hash}^{commit}" >/dev/null 2>&1 || return 0
37
+ raw="$(git -C "$dir" diff-tree --no-commit-id --name-status -r --root "$hash" 2>/dev/null || true)"
38
+ else
39
+ raw="$(git -C "$dir" status --short --no-renames 2>/dev/null || true)"
40
+ fi
41
+ [ -n "$raw" ] || return 0
42
+
43
+ local added='' modified='' deleted='' line code path
44
+ while IFS= read -r line; do
45
+ [ -n "$line" ] || continue
46
+ if [ -n "$hash" ]; then
47
+ # diff-tree --name-status: "<code>\t<path>" (single-letter code)
48
+ code="${line%%$'\t'*}"
49
+ path="${line#*$'\t'}"
50
+ else
51
+ # git status --short: "<XY> <path>" (XY is always exactly 2 chars)
52
+ code="${line:0:2}"
53
+ path="${line:3}"
54
+ fi
55
+ case "$path" in
56
+ .devlog/*) continue ;;
57
+ esac
58
+ case "$code" in
59
+ '??') added="${added:+$added, }$path" ;;
60
+ *A*) added="${added:+$added, }$path" ;;
61
+ *D*) deleted="${deleted:+$deleted, }$path" ;;
62
+ *) modified="${modified:+$modified, }$path" ;;
63
+ esac
64
+ done <<< "$raw"
65
+
66
+ [ -n "$added" ] && printf '新增:%s\n' "$added"
67
+ [ -n "$modified" ] && printf '修改:%s\n' "$modified"
68
+ [ -n "$deleted" ] && printf '刪除:%s\n' "$deleted"
69
+ return 0
70
+ }
@@ -0,0 +1,76 @@
1
+ # shellcheck shell=bash
2
+ # sourced by hook scripts. Do not execute.
3
+ json_int_get() {
4
+ local file="$1" key="$2" raw=""
5
+ [ -f "$file" ] || { echo ''; return 0; }
6
+ # Read digits from the file text first so leading-zero values like 08
7
+ # stay visible (jq would coerce them and bash would treat them as octal).
8
+ raw="$(grep -o "\"${key}\"[[:space:]]*:[[:space:]]*[0-9]\+" "$file" 2>/dev/null | grep -o '[0-9]\+$' || echo '')"
9
+ if [ -z "$raw" ] && command -v jq >/dev/null 2>&1; then
10
+ raw="$(jq -r --arg k "$key" '.[$k] | tostring' "$file" 2>/dev/null || echo '')"
11
+ [ "$raw" = "null" ] && raw=""
12
+ fi
13
+ case "$raw" in
14
+ ''|*[!0-9]*) echo ''; return 0 ;;
15
+ 0) echo 0; return 0 ;;
16
+ 0*) echo ''; return 0 ;;
17
+ *) echo "$raw"; return 0 ;;
18
+ esac
19
+ }
20
+
21
+ json_str_get() {
22
+ local file="$1" key="$2" raw=""
23
+ [ -f "$file" ] || { echo ''; return 0; }
24
+ if command -v jq >/dev/null 2>&1; then
25
+ raw="$(jq -r --arg k "$key" '.[$k] // empty' "$file" 2>/dev/null || echo '')"
26
+ [ "$raw" = "null" ] && raw=""
27
+ printf '%s' "$raw"
28
+ echo
29
+ return 0
30
+ fi
31
+ raw="$(grep -o "\"${key}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$file" 2>/dev/null | head -1 || echo '')"
32
+ printf '%s' "$raw" | sed -E "s/^.*\"${key}\"[[:space:]]*:[[:space:]]*\"//; s/\"$//"
33
+ echo
34
+ }
35
+
36
+ json_int_set() {
37
+ local file="$1" key="$2" val="$3"
38
+ [ -f "$file" ] || return 0
39
+ case "$val" in ''|*[!0-9]*) return 0 ;; esac
40
+ awk -v key="$key" -v val="$val" '{
41
+ pat = "\"" key "\"[[:space:]]*:[[:space:]]*[0-9]+"
42
+ repl = "\"" key "\": " val
43
+ gsub(pat, repl)
44
+ print
45
+ }' "$file" > "$file.tmp" 2>/dev/null && mv "$file.tmp" "$file" 2>/dev/null || true
46
+ }
47
+
48
+ json_str_set() {
49
+ local file="$1" key="$2" val="$3"
50
+ [ -f "$file" ] || return 0
51
+ case "$val" in *\"*|*[$'\n']*) return 0 ;; esac
52
+ awk -v key="$key" -v val="$val" '{
53
+ pat = "\"" key "\"[[:space:]]*:[[:space:]]*\"[^\"]*\""
54
+ repl = "\"" key "\": \"" val "\""
55
+ gsub(pat, repl)
56
+ print
57
+ }' "$file" > "$file.tmp" 2>/dev/null && mv "$file.tmp" "$file" 2>/dev/null || true
58
+ }
59
+
60
+ json_str_field() {
61
+ local json="$1" key="$2" raw=""
62
+ if command -v jq >/dev/null 2>&1; then
63
+ raw="$(printf '%s' "$json" | jq -r --arg k "$key" '.[$k] // empty' 2>/dev/null || echo '')"
64
+ [ "$raw" = "null" ] && raw=""
65
+ printf '%s' "$raw"
66
+ echo
67
+ return 0
68
+ fi
69
+ raw="$(printf '%s' "$json" | grep -o "\"${key}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" 2>/dev/null | head -1 || echo '')"
70
+ printf '%s' "$raw" | sed -E "s/^.*\"${key}\"[[:space:]]*:[[:space:]]*\"//; s/\"$//"
71
+ echo
72
+ }
73
+
74
+ slugify() {
75
+ printf '%s' "$1" | tr ' ' '-' | sed -E 's/-+/-/g; s/^-//; s/-$//'
76
+ }
@@ -0,0 +1,155 @@
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/json-field.sh"
8
+ . "$SCRIPT_DIR/devlog-lock.sh"
9
+ . "$SCRIPT_DIR/devlog-path.sh"
10
+
11
+ FROM=""
12
+ TO=""
13
+ NAME=""
14
+ DESC=""
15
+ while [ "$#" -gt 0 ]; do
16
+ case "$1" in
17
+ --from) FROM="${2:-}"; shift 2 ;;
18
+ --to) TO="${2:-}"; shift 2 ;;
19
+ --name) NAME="${2:-}"; shift 2 ;;
20
+ --desc) DESC="${2:-}"; shift 2 ;;
21
+ *) echo "未知參數:$1" >&2; exit 1 ;;
22
+ esac
23
+ done
24
+ DESC="$(printf '%s' "$DESC" | tr '\n' ' ')"
25
+ case "$FROM:$TO" in *[!0-9:]*|:*) echo "範圍無效" >&2; exit 1 ;; esac
26
+ [ "$FROM" -le "$TO" ] || { echo "範圍起點不可大於終點" >&2; exit 1; }
27
+
28
+ case "$NAME" in devlog.md|*'/'*|*'\'*|*'..'*) echo "檔名無效" >&2; exit 1 ;; esac
29
+ case "$NAME" in devlog.*) NAME="${NAME#devlog.}" ;; esac
30
+ case "$NAME" in *.md) NAME="${NAME%.md}" ;; esac
31
+ NAME="$(slugify "$NAME")"
32
+ [ -n "$NAME" ] && [ "$NAME" != "archive" ] && [ "$NAME" != "devlog.md" ] \
33
+ || { echo "檔名無效" >&2; exit 1; }
34
+ [ "${#NAME}" -le 64 ] || { echo "檔名超過 64 字元" >&2; exit 1; }
35
+
36
+ devlog_resolve_paths "${CLAUDE_PROJECT_DIR:-.}"
37
+ devlog_lock_acquire
38
+ trap 'devlog_lock_release' EXIT
39
+ MAIN="$DEVLOG_FILE"
40
+ TARGET="$DEVLOG_DIR/devlog.$NAME.md"
41
+ [ -f "$MAIN" ] || { echo "devlog.md 不存在" >&2; exit 1; }
42
+ [ ! -e "$TARGET" ] || { echo "目標檔案已存在:$TARGET" >&2; exit 1; }
43
+
44
+ OPEN=""
45
+ [ ! -f "$DEVLOG_DIR/.round-open" ] || OPEN="$(json_int_get "$DEVLOG_DIR/.round-open" round)"
46
+ TMP="$(mktemp -d "${TMPDIR:-/tmp}/devlog-keep.XXXXXX")" || exit 1
47
+ trap 'rm -rf "$TMP"; devlog_lock_release' EXIT
48
+ STARTS="$TMP/starts"
49
+ MOVE_ROUNDS="$TMP/move-rounds"
50
+ MOVE_BLOCKS="$TMP/move-blocks"
51
+ H2="$TMP/h2"
52
+ NEW_MAIN="$TMP/main"
53
+ devlog_list_round_starts "$MAIN" > "$STARTS"
54
+ : > "$MOVE_ROUNDS"
55
+ : > "$MOVE_BLOCKS"
56
+
57
+ HISTORICAL=0
58
+ FIRST_ROUND=""
59
+ LAST_MOVED_END=""
60
+ while read -r start round; do
61
+ [ -n "${start:-}" ] || continue
62
+ [ -n "$FIRST_ROUND" ] || FIRST_ROUND="$start"
63
+ [ "$round" = "$OPEN" ] && continue
64
+ HISTORICAL=$((HISTORICAL + 1))
65
+ if [ "$round" -ge "$FROM" ] && [ "$round" -le "$TO" ]; then
66
+ printf '%s %s\n' "$start" "$round" >> "$MOVE_ROUNDS"
67
+ printf '%s\n' "$start" >> "$MOVE_BLOCKS"
68
+ LAST_MOVED_END="$(devlog_block_end "$MAIN" "$start")"
69
+ fi
70
+ done < "$STARTS"
71
+
72
+ EXPECTED=$((TO - FROM + 1))
73
+ MOVED="$(wc -l < "$MOVE_ROUNDS" | tr -d ' ')"
74
+ [ "$MOVED" -eq "$EXPECTED" ] || { echo "範圍內有不存在或非歷史的 Round" >&2; exit 1; }
75
+ [ "$HISTORICAL" -gt 0 ] || { echo "目前沒有東西可 keep" >&2; exit 1; }
76
+ FULL=0
77
+ [ "$MOVED" -eq "$HISTORICAL" ] && FULL=1
78
+ FIRST_MOVED="$(awk 'NR == 1 { print $1 }' "$MOVE_ROUNDS")"
79
+
80
+ awk '
81
+ /^[ \t]*```/ { fence = !fence }
82
+ !fence && /^## / { print NR "\t" $0 }
83
+ ' "$MAIN" > "$H2"
84
+ while IFS="$(printf '\t')" read -r start heading; do
85
+ case "$heading" in
86
+ "## Checkpoint"*)
87
+ span="$(printf '%s' "$heading" | sed -nE 's/.*Round ([0-9]+)-([0-9]+).*/\1 \2/p')"
88
+ if [ -n "$span" ]; then
89
+ x="${span%% *}"; y="${span##* }"
90
+ if [ "$x" -ge "$FROM" ] && [ "$y" -le "$TO" ]; then
91
+ printf '%s\n' "$start" >> "$MOVE_BLOCKS"
92
+ fi
93
+ elif [ "$start" -gt "$FIRST_MOVED" ] && [ "$start" -le "$LAST_MOVED_END" ]; then
94
+ printf '%s\n' "$start" >> "$MOVE_BLOCKS"
95
+ fi
96
+ ;;
97
+ esac
98
+ done < "$H2"
99
+
100
+ KEPT_AT="$(date -Iseconds 2>/dev/null || date '+%Y-%m-%dT%H:%M:%S%z')"
101
+ {
102
+ printf '# Kept log\n\n- source: `.devlog/devlog.md`\n- rounds: %s-%s\n- kept_at: %s\n\n' \
103
+ "$FROM" "$TO" "$KEPT_AT"
104
+ awk -v selected="$MOVE_BLOCKS" -v full="$FULL" -v first_round="$FIRST_ROUND" '
105
+ BEGIN { while ((getline n < selected) > 0) move[n] = 1 }
106
+ /^[ \t]*```/ { fence = !fence }
107
+ !fence && /^## / { moving = (NR in move) }
108
+ full && NR < first_round { print; next }
109
+ moving { print }
110
+ ' "$MAIN"
111
+ } > "$TARGET" || exit 1
112
+
113
+ while read -r start round; do
114
+ heading="$(awk -v n="$start" 'NR == n { print; exit }' "$MAIN")"
115
+ grep -Fqx "$heading" "$TARGET" || { echo "具名檔驗證失敗" >&2; exit 1; }
116
+ done < "$MOVE_ROUNDS"
117
+
118
+ awk -v selected="$MOVE_BLOCKS" -v full="$FULL" -v first_round="$FIRST_ROUND" -v open="$OPEN" '
119
+ BEGIN { while ((getline n < selected) > 0) move[n] = 1 }
120
+ /^[ \t]*```/ { fence = !fence }
121
+ !fence && /^## / { moving = (NR in move) }
122
+ full && NR < first_round { next }
123
+ moving { next }
124
+ full && open != "" && $0 ~ ("^## Round " open "([^0-9]|$)") {
125
+ sub("^## Round " open, "## Round 1")
126
+ }
127
+ { print }
128
+ ' "$MAIN" > "$NEW_MAIN" || exit 1
129
+ mv "$NEW_MAIN" "$MAIN" || exit 1
130
+
131
+ KEPT_LINE="- \`devlog.${NAME}.md\`:Round ${FROM}-${TO},kept_at ${KEPT_AT}"
132
+ [ -z "$DESC" ] || KEPT_LINE="${KEPT_LINE},${DESC}"
133
+ KEPT_STRIPPED="$TMP/kept-stripped"
134
+ devlog_strip_kept_index "$MAIN" "$KEPT_STRIPPED"
135
+ EXISTING_KEPT_LINES="$(devlog_kept_index_lines "$MAIN")"
136
+ {
137
+ cat "$KEPT_STRIPPED"
138
+ printf '\n## Kept 索引\n'
139
+ [ -n "$EXISTING_KEPT_LINES" ] && printf '%s\n' "$EXISTING_KEPT_LINES"
140
+ printf '%s\n' "$KEPT_LINE"
141
+ } > "$KEPT_STRIPPED.new" && mv "$KEPT_STRIPPED.new" "$MAIN" || exit 1
142
+
143
+ if [ "$FULL" -eq 1 ]; then
144
+ rm -f "$DEVLOG_DIR/.span-open"
145
+ [ ! -f "$DEVLOG_DIR/.round-open" ] || json_int_set "$DEVLOG_DIR/.round-open" round 1
146
+ [ ! -f "$DEVLOG_DIR/.checkpoint-state" ] || json_int_set "$DEVLOG_DIR/.checkpoint-state" rounds_since_checkpoint 0
147
+ elif [ -f "$DEVLOG_DIR/.span-open" ]; then
148
+ SPAN_ROUND="$(json_int_get "$DEVLOG_DIR/.span-open" round)"
149
+ if [ -n "$SPAN_ROUND" ] && [ "$SPAN_ROUND" -ge "$FROM" ] && [ "$SPAN_ROUND" -le "$TO" ]; then
150
+ rm -f "$DEVLOG_DIR/.span-open"
151
+ fi
152
+ fi
153
+
154
+ REMAINING="$(devlog_list_round_starts "$MAIN" | wc -l | tr -d ' ')"
155
+ printf 'KEPT=%s ROUNDS=%s-%s REMAINING=%s\n' "$TARGET" "$FROM" "$TO" "$REMAINING"
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env bash
2
+ # Filesystem side of /devlog-tracker:overview (docs/design/keep.md "Kept
3
+ # index"). Plain read only -- parses the `## Kept 索引` block for the list
4
+ # of devlog.<name>.md files kept so far, checking each against disk so a
5
+ # manually-deleted file is reported as a ghost row instead of silently
6
+ # dropped (same tolerance as the index itself).
7
+ set -uo pipefail
8
+
9
+ _src="${BASH_SOURCE[0]}"
10
+ SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
11
+ # shellcheck source=devlog-md.sh
12
+ . "$SCRIPT_DIR/devlog-md.sh"
13
+ # shellcheck source=devlog-path.sh
14
+ . "$SCRIPT_DIR/devlog-path.sh"
15
+
16
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
17
+ devlog_resolve_paths "$PROJECT_DIR"
18
+ MAIN="$DEVLOG_FILE"
19
+
20
+ if [ ! -f "$MAIN" ]; then
21
+ echo "NO_INDEX"
22
+ exit 0
23
+ fi
24
+
25
+ LINES="$(devlog_kept_index_lines "$MAIN")"
26
+ if [ -z "$LINES" ]; then
27
+ echo "NO_INDEX"
28
+ exit 0
29
+ fi
30
+
31
+ printf '%s\n' "$LINES" | while IFS= read -r line; do
32
+ name="$(printf '%s' "$line" | sed -nE 's/.*`devlog\.([^`]+)\.md`.*/\1/p')"
33
+ [ -n "$name" ] || continue
34
+ file="$DEVLOG_DIR/devlog.$name.md"
35
+ if [ -f "$file" ]; then
36
+ printf 'FILE=%s EXISTS=1\n' "$file"
37
+ else
38
+ printf 'FILE=%s EXISTS=0\n' "$file"
39
+ fi
40
+ done
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env bash
2
+ # Claude-invoked only (docs/design/lessons-mode.md), never wired into
3
+ # hooks/hooks.json — same posture as keep-move.sh. Appends one free-prose
4
+ # entry to .devlog/devlog.lessons.<topic>.md (creating it if new), then
5
+ # rebuilds the trailing "## Lessons 索引" block in devlog.md.
6
+ set -uo pipefail
7
+
8
+ _src="${BASH_SOURCE[0]}"
9
+ SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
10
+ # shellcheck source=json-field.sh
11
+ . "$SCRIPT_DIR/json-field.sh"
12
+ # shellcheck source=devlog-md.sh
13
+ . "$SCRIPT_DIR/devlog-md.sh"
14
+ # shellcheck source=devlog-lock.sh
15
+ . "$SCRIPT_DIR/devlog-lock.sh"
16
+ # shellcheck source=devlog-path.sh
17
+ . "$SCRIPT_DIR/devlog-path.sh"
18
+
19
+ TOPIC=""
20
+ TEXT=""
21
+ while [ "$#" -gt 0 ]; do
22
+ case "$1" in
23
+ --topic) TOPIC="${2:-}"; shift 2 ;;
24
+ --text) TEXT="${2:-}"; shift 2 ;;
25
+ *) echo "未知參數:$1" >&2; exit 1 ;;
26
+ esac
27
+ done
28
+
29
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
30
+ devlog_resolve_paths "$PROJECT_DIR"
31
+ MAIN="$DEVLOG_FILE"
32
+
33
+ [ -f "$DEVLOG_DIR/.enabled" ] || { echo "NOT_ENABLED" >&2; exit 1; }
34
+ [ -f "$DEVLOG_DIR/.lessons-enabled" ] || { echo "LESSONS_NOT_ENABLED" >&2; exit 1; }
35
+ [ -n "$TEXT" ] || { echo "缺少 --text" >&2; exit 1; }
36
+ [ -f "$MAIN" ] || { echo "devlog.md 不存在" >&2; exit 1; }
37
+
38
+ # --- Normalize <topic>,跟 keep/resume 的 <name> 同一套規則
39
+ # (docs/design/keep.md「Filenames」/ docs/design/lessons-mode.md「Storage」),
40
+ # 外加保留 lessons 這個 infix 本身當名字。
41
+ case "$TOPIC" in *'/'*|*'\'*|*'..'*) echo "TOPIC 無效" >&2; exit 1 ;; esac
42
+ case "$TOPIC" in devlog.lessons.*) TOPIC="${TOPIC#devlog.lessons.}" ;; esac
43
+ case "$TOPIC" in devlog.*) TOPIC="${TOPIC#devlog.}" ;; esac
44
+ case "$TOPIC" in *.md) TOPIC="${TOPIC%.md}" ;; esac
45
+ TOPIC="$(slugify "$TOPIC")"
46
+ [ -n "$TOPIC" ] && [ "$TOPIC" != "archive" ] && [ "$TOPIC" != "lessons" ] && [ "${#TOPIC}" -le 64 ] \
47
+ || { echo "TOPIC 無效" >&2; exit 1; }
48
+
49
+ devlog_lock_acquire
50
+ trap 'devlog_lock_release' EXIT
51
+
52
+ TARGET="$DEVLOG_DIR/devlog.lessons.$TOPIC.md"
53
+ TS="$(date -Iseconds 2>/dev/null || date '+%Y-%m-%dT%H:%M:%S%z')"
54
+
55
+ if [ ! -f "$TARGET" ]; then
56
+ printf '# Lessons: %s\n\n- source: `.devlog/devlog.md`\n' "$TOPIC" > "$TARGET" || exit 1
57
+ fi
58
+ {
59
+ printf '\n## %s\n' "$TS"
60
+ printf '%s\n' "$TEXT"
61
+ } >> "$TARGET" || exit 1
62
+
63
+ # --- Rebuild "## Lessons 索引" in devlog.md: one line per topic file,
64
+ # re-derived from disk each call (docs/design/lessons-mode.md「## Lessons
65
+ # 索引」). Title = first non-empty line after the LAST "## <timestamp>"
66
+ # heading in that file, truncated at the first 。/. (whichever comes
67
+ # first); no truncation if neither appears.
68
+ INDEX_LINES=""
69
+ shopt -s nullglob
70
+ for f in "$DEVLOG_DIR"/devlog.lessons.*.md; do
71
+ [ -f "$f" ] || continue
72
+ leaf="${f##*/}"
73
+ n="$(grep -c '^## ' "$f" 2>/dev/null || echo 0)"
74
+ case "$n" in ''|*[!0-9]*) n=0 ;; esac
75
+ [ "$n" -gt 0 ] || continue
76
+ last_ln="$(grep -n '^## ' "$f" | tail -1 | cut -d: -f1)"
77
+ updated_at="$(sed -n "${last_ln}p" "$f" | sed -E 's/^## //')"
78
+ title="$(awk -v start="$last_ln" 'NR>start && $0 ~ /[^[:space:]]/ {print; exit}' "$f")"
79
+ title="$(printf '%s' "$title" | sed -E 's/([。.]).*$/\1/')"
80
+ LINE="- \`${leaf}\`:${n} 則,最新一則「${title}」(updated_at ${updated_at})"
81
+ INDEX_LINES="${INDEX_LINES:+$INDEX_LINES$'\n'}$LINE"
82
+ done
83
+
84
+ STRIPPED="$(mktemp "${TMPDIR:-/tmp}/devlog-lessons.XXXXXX")" || exit 1
85
+ trap 'rm -f "$STRIPPED"; devlog_lock_release' EXIT
86
+ devlog_strip_lessons_index "$MAIN" "$STRIPPED"
87
+ {
88
+ cat "$STRIPPED"
89
+ printf '\n## Lessons 索引\n'
90
+ [ -n "$INDEX_LINES" ] && printf '%s\n' "$INDEX_LINES"
91
+ } > "$STRIPPED.new" && mv "$STRIPPED.new" "$MAIN" || exit 1
92
+
93
+ printf 'PATH=.devlog/devlog.lessons.%s.md\n' "$TOPIC"
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env bash
2
+ # User-invoked: /devlog-tracker:lessons-drift filesystem side.
3
+ # Sets the workspace-drift nudge's threshold (docs/design/lessons-mode.md
4
+ # 「機制性訊號:工作區漂移重複發生」): how many cumulative 工作區-mismatch
5
+ # occurrences (round-start.sh) before printing one advisory suggestion to
6
+ # write a Lessons Mode entry. Nested under Lessons Mode, which is itself
7
+ # nested under the main switch. $1 is required: a positive integer.
8
+ set -uo pipefail
9
+ _src="${BASH_SOURCE[0]}"
10
+ SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
11
+ # shellcheck source=json-field.sh
12
+ . "$SCRIPT_DIR/json-field.sh"
13
+
14
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
15
+ DEVLOG_DIR="$PROJECT_DIR/.devlog"
16
+
17
+ if [ ! -f "$DEVLOG_DIR/.enabled" ]; then
18
+ echo "NOT_STARTED"
19
+ exit 0
20
+ fi
21
+ if [ ! -f "$DEVLOG_DIR/.lessons-enabled" ]; then
22
+ echo "LESSONS_NOT_ENABLED"
23
+ exit 0
24
+ fi
25
+
26
+ COUNT_ARG="${1:-}"
27
+ case "$COUNT_ARG" in
28
+ ''|*[!0-9]*|0)
29
+ echo "threshold must be a positive integer number of occurrences, got: $COUNT_ARG" >&2
30
+ exit 1
31
+ ;;
32
+ esac
33
+
34
+ if [ -f "$DEVLOG_DIR/.lessons-drift-state" ]; then
35
+ json_int_set "$DEVLOG_DIR/.lessons-drift-state" threshold "$COUNT_ARG"
36
+ else
37
+ printf '%s\n' "{\"mismatch_count\": 0, \"threshold\": ${COUNT_ARG}}" \
38
+ > "$DEVLOG_DIR/.lessons-drift-state" || exit 1
39
+ fi
40
+
41
+ echo "LESSONS_DRIFT_THRESHOLD=$(json_int_get "$DEVLOG_DIR/.lessons-drift-state" threshold)"
42
+ exit 0
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bash
2
+ # User-invoked: /devlog-tracker:lessons-off filesystem side.
3
+ # Removes .lessons-enabled only -- never touches devlog.lessons.*.md files
4
+ # or the "## Lessons 索引" block (docs/design/lessons-mode.md), same
5
+ # non-destructive posture as pause-devlog.sh toward devlog.md.
6
+ set -uo pipefail
7
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
8
+ DEVLOG_DIR="$PROJECT_DIR/.devlog"
9
+ if [ ! -f "$DEVLOG_DIR/.lessons-enabled" ]; then
10
+ echo "NOT_ENABLED"
11
+ exit 0
12
+ fi
13
+ rm -f "$DEVLOG_DIR/.lessons-enabled"
14
+ echo "LESSONS_DISABLED"
15
+ exit 0
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env bash
2
+ # User-invoked: /devlog-tracker:lessons-on filesystem side.
3
+ # Nested under the main switch (docs/design/lessons-mode.md「Enable /
4
+ # disable」): refuses if .enabled is absent, since there is no Round/Status
5
+ # history to detect a BLOCKED->resolved transition against.
6
+ set -uo pipefail
7
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
8
+ DEVLOG_DIR="$PROJECT_DIR/.devlog"
9
+ if [ ! -f "$DEVLOG_DIR/.enabled" ]; then
10
+ echo "NOT_ENABLED"
11
+ exit 0
12
+ fi
13
+ if [ ! -f "$DEVLOG_DIR/.lessons-enabled" ]; then
14
+ date -Iseconds > "$DEVLOG_DIR/.lessons-enabled" 2>/dev/null || echo enabled > "$DEVLOG_DIR/.lessons-enabled"
15
+ fi
16
+ if [ ! -f "$DEVLOG_DIR/.lessons-drift-state" ]; then
17
+ printf '%s\n' '{"mismatch_count": 0, "threshold": 3}' \
18
+ > "$DEVLOG_DIR/.lessons-drift-state" 2>/dev/null || true
19
+ fi
20
+ echo "LESSONS_ENABLED=$DEVLOG_DIR/.lessons-enabled"
21
+ exit 0
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bash
2
+ # Filesystem side of /devlog-tracker:lessons [<topic>] (docs/design/
3
+ # lessons-mode.md「Reading lessons on demand」). Plain read only -- no
4
+ # workspace verification, no wait-for-confirmation flow (a lessons file is
5
+ # reference material, not a paused work topic, unlike resume-devlog.sh).
6
+ set -uo pipefail
7
+
8
+ _src="${BASH_SOURCE[0]}"
9
+ SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
10
+ # shellcheck source=json-field.sh
11
+ . "$SCRIPT_DIR/json-field.sh"
12
+ # shellcheck source=devlog-md.sh
13
+ . "$SCRIPT_DIR/devlog-md.sh"
14
+ # shellcheck source=devlog-path.sh
15
+ . "$SCRIPT_DIR/devlog-path.sh"
16
+
17
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
18
+ devlog_resolve_paths "$PROJECT_DIR"
19
+ MAIN="$DEVLOG_FILE"
20
+
21
+ TOPIC="${1:-}"
22
+
23
+ if [ -z "$TOPIC" ]; then
24
+ if [ ! -f "$MAIN" ]; then
25
+ echo "NO_INDEX"
26
+ exit 0
27
+ fi
28
+ LINES="$(devlog_lessons_index_lines "$MAIN")"
29
+ if [ -z "$LINES" ]; then
30
+ echo "NO_INDEX"
31
+ else
32
+ printf '%s' "$LINES"
33
+ fi
34
+ exit 0
35
+ fi
36
+
37
+ case "$TOPIC" in *'/'*|*'\'*|*'..'*) echo "INVALID_TOPIC" >&2; exit 1 ;; esac
38
+ case "$TOPIC" in devlog.lessons.*) TOPIC="${TOPIC#devlog.lessons.}" ;; esac
39
+ case "$TOPIC" in devlog.*) TOPIC="${TOPIC#devlog.}" ;; esac
40
+ case "$TOPIC" in *.md) TOPIC="${TOPIC%.md}" ;; esac
41
+ TOPIC="$(slugify "$TOPIC")"
42
+ [ -n "$TOPIC" ] && [ "${#TOPIC}" -le 64 ] || { echo "INVALID_TOPIC" >&2; exit 1; }
43
+
44
+ FILE="$DEVLOG_DIR/devlog.lessons.$TOPIC.md"
45
+ if [ ! -f "$FILE" ]; then
46
+ candidates=""
47
+ shopt -s nullglob
48
+ for path in "$DEVLOG_DIR"/devlog.lessons.*.md; do
49
+ leaf="${path##*/}"
50
+ candidate="${leaf#devlog.lessons.}"
51
+ candidate="${candidate%.md}"
52
+ [ -z "$candidates" ] || candidates="$candidates,"
53
+ candidates="$candidates$candidate"
54
+ done
55
+ echo "MISSING"
56
+ echo "CANDIDATES=$candidates"
57
+ exit 1
58
+ fi
59
+ printf 'PATH=.devlog/devlog.lessons.%s.md\n' "$TOPIC"
60
+ cat "$FILE"
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env bash
2
+ # SessionEnd: stamp INTERRUPTED if a Round is still open.
3
+ set -uo pipefail
4
+
5
+ HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ # shellcheck source=json-field.sh
7
+ . "$HOOKS_DIR/json-field.sh"
8
+ INPUT="$(cat 2>/dev/null || true)"
9
+
10
+ REASON="$(json_str_field "$INPUT" reason)"
11
+ [ -n "$REASON" ] || REASON="other"
12
+
13
+ bash "$HOOKS_DIR/close-open-round.sh" "SessionEnd:${REASON}" || true
14
+ exit 0
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env bash
2
+ # StopFailure: stamp INTERRUPTED unless this is a usage-exhaustion error.
3
+ set -uo pipefail
4
+
5
+ HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ # shellcheck source=json-field.sh
7
+ . "$HOOKS_DIR/json-field.sh"
8
+ INPUT="$(cat 2>/dev/null || true)"
9
+
10
+ ERROR="$(json_str_field "$INPUT" error)"
11
+
12
+ case "$ERROR" in
13
+ rate_limit|billing_error|account_on_hold) exit 0 ;;
14
+ '') ERROR="unknown" ;;
15
+ esac
16
+
17
+ bash "$HOOKS_DIR/close-open-round.sh" "StopFailure:${ERROR}" || true
18
+ exit 0
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env bash
2
+ # PostToolUseFailure: remember user Esc so Stop can stamp without blocking.
3
+ set -uo pipefail
4
+
5
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
6
+ DEVLOG_DIR="$PROJECT_DIR/.devlog"
7
+ ENABLED_FLAG="$DEVLOG_DIR/.enabled"
8
+ [ -f "$ENABLED_FLAG" ] || exit 0
9
+
10
+ INPUT="$(cat 2>/dev/null || true)"
11
+ IS_INT="false"
12
+ if command -v jq >/dev/null 2>&1; then
13
+ IS_INT="$(printf '%s' "$INPUT" | jq -r '.is_interrupt // false' 2>/dev/null || echo false)"
14
+ else
15
+ case "$INPUT" in
16
+ *'"is_interrupt":true'*|*'"is_interrupt": true'*) IS_INT=true ;;
17
+ *) IS_INT=false ;;
18
+ esac
19
+ fi
20
+
21
+ if [ "$IS_INT" = "true" ]; then
22
+ mkdir -p "$DEVLOG_DIR" 2>/dev/null || true
23
+ : > "$DEVLOG_DIR/.interrupted" 2>/dev/null || true
24
+ fi
25
+ exit 0
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
4
+ DEVLOG_DIR="$PROJECT_DIR/.devlog"
5
+ if [ ! -f "$DEVLOG_DIR/.enabled" ]; then
6
+ echo "NOT_ENABLED"
7
+ exit 0
8
+ fi
9
+ rm -f "$DEVLOG_DIR/.enabled" "$DEVLOG_DIR/.span-open" \
10
+ "$DEVLOG_DIR/.round-open" "$DEVLOG_DIR/.interrupted" \
11
+ "$DEVLOG_DIR/.awaiting-reply"
12
+ echo "PAUSED"
13
+ exit 0