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,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
|