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,19 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Sourced helper: redact_prompt reads stdin and writes sanitized prompt text.
|
|
3
|
+
redact_prompt() {
|
|
4
|
+
awk '
|
|
5
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----/ { skip=1; print "(已遮罩)"; next }
|
|
6
|
+
skip && /-----END [A-Z ]*PRIVATE KEY-----/ { skip=0; next }
|
|
7
|
+
skip { next }
|
|
8
|
+
{ print }
|
|
9
|
+
' | sed -E \
|
|
10
|
+
-e 's/sk-ant-api03-[A-Za-z0-9_-]+/(已遮罩)/g' \
|
|
11
|
+
-e 's/sk-ant-[A-Za-z0-9_-]{20,}/(已遮罩)/g' \
|
|
12
|
+
-e 's/sk-[A-Za-z0-9]{20,}/(已遮罩)/g' \
|
|
13
|
+
-e 's/ghp_[A-Za-z0-9]{20,}/(已遮罩)/g' \
|
|
14
|
+
-e 's/github_pat_[A-Za-z0-9_]{20,}/(已遮罩)/g' \
|
|
15
|
+
-e 's/xox[baprs]-[A-Za-z0-9-]{10,}/(已遮罩)/g' \
|
|
16
|
+
-e 's/AKIA[A-Z0-9]{16}/(已遮罩)/g' \
|
|
17
|
+
-e 's/Bearer [A-Za-z0-9._+=/-]{20,}/(已遮罩)/g' \
|
|
18
|
+
-e 's/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/(已遮罩)/g'
|
|
19
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
|
|
4
|
+
_src="${BASH_SOURCE[0]}"
|
|
5
|
+
SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
|
|
6
|
+
# shellcheck source=json-field.sh
|
|
7
|
+
. "$SCRIPT_DIR/json-field.sh"
|
|
8
|
+
|
|
9
|
+
NAME=""
|
|
10
|
+
if [ "${1:-}" = "--name" ]; then
|
|
11
|
+
NAME="${2:-}"
|
|
12
|
+
else
|
|
13
|
+
echo "MISSING_NAME" >&2
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
case "$NAME" in devlog.md|*'/'*|*'\'*|*'..'*) echo "INVALID_NAME" >&2; exit 1 ;; esac
|
|
17
|
+
case "$NAME" in devlog.*) NAME="${NAME#devlog.}" ;; esac
|
|
18
|
+
case "$NAME" in *.md) NAME="${NAME%.md}" ;; esac
|
|
19
|
+
NAME="$(slugify "$NAME")"
|
|
20
|
+
[ -n "$NAME" ] && [ "$NAME" != "archive" ] && [ "${#NAME}" -le 64 ] \
|
|
21
|
+
|| { echo "INVALID_NAME" >&2; exit 1; }
|
|
22
|
+
|
|
23
|
+
DEVLOG_DIR="${CLAUDE_PROJECT_DIR:-.}/.devlog"
|
|
24
|
+
FILE="$DEVLOG_DIR/devlog.$NAME.md"
|
|
25
|
+
if [ ! -f "$FILE" ]; then
|
|
26
|
+
candidates=""
|
|
27
|
+
shopt -s nullglob
|
|
28
|
+
for path in "$DEVLOG_DIR"/devlog.*.md; do
|
|
29
|
+
leaf="${path##*/}"
|
|
30
|
+
case "$leaf" in devlog.md|devlog.archive.md) continue ;; esac
|
|
31
|
+
candidate="${leaf#devlog.}"
|
|
32
|
+
candidate="${candidate%.md}"
|
|
33
|
+
[ -z "$candidates" ] || candidates="$candidates,"
|
|
34
|
+
candidates="$candidates$candidate"
|
|
35
|
+
done
|
|
36
|
+
echo "MISSING"
|
|
37
|
+
echo "CANDIDATES=$candidates"
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
printf 'PATH=.devlog/devlog.%s.md\n' "$NAME"
|
|
41
|
+
cat "$FILE"
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# UserPromptSubmit hook:每次使用者送出新訊息時執行。
|
|
3
|
+
# 互動輪次:先(如有)把上一輪未收尾標成 INTERRUPTED,再追加本輪 User Input
|
|
4
|
+
# skeleton,然後把 .turn-start 設成「寫完 skeleton 之後」的雜湊,供 Stop hook
|
|
5
|
+
# 判斷 Claude 有沒有再寫 Summary/Handoff。
|
|
6
|
+
# Span 開著且檔案格式有效時不開新 Round。
|
|
7
|
+
# fail-open:這支腳本本身出任何問題都不該影響使用者送出訊息,一律 exit 0。
|
|
8
|
+
|
|
9
|
+
set -uo pipefail
|
|
10
|
+
|
|
11
|
+
_src="${BASH_SOURCE[0]}"
|
|
12
|
+
HOOKS_DIR="$(cd "${_src%/*}" && pwd)"
|
|
13
|
+
# shellcheck source=json-field.sh
|
|
14
|
+
. "$HOOKS_DIR/json-field.sh"
|
|
15
|
+
# shellcheck source=redact-prompt.sh
|
|
16
|
+
. "$HOOKS_DIR/redact-prompt.sh"
|
|
17
|
+
# shellcheck source=devlog-lock.sh
|
|
18
|
+
. "$HOOKS_DIR/devlog-lock.sh"
|
|
19
|
+
# shellcheck source=devlog-md.sh
|
|
20
|
+
. "$HOOKS_DIR/devlog-md.sh"
|
|
21
|
+
# shellcheck source=workspace-snapshot.sh
|
|
22
|
+
. "$HOOKS_DIR/workspace-snapshot.sh"
|
|
23
|
+
# shellcheck source=detect-pending-question.sh
|
|
24
|
+
. "$HOOKS_DIR/detect-pending-question.sh"
|
|
25
|
+
# shellcheck source=devlog-path.sh
|
|
26
|
+
. "$HOOKS_DIR/devlog-path.sh"
|
|
27
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
|
28
|
+
[ -f "$PROJECT_DIR/.devlog/.enabled" ] || exit 0
|
|
29
|
+
devlog_resolve_paths "$PROJECT_DIR"
|
|
30
|
+
ROUND_CURRENT="$DEVLOG_DIR/.round-current.md"
|
|
31
|
+
SPAN_FILE="$DEVLOG_DIR/.span-open"
|
|
32
|
+
CHECKPOINT_FILE="$DEVLOG_DIR/.checkpoint-state"
|
|
33
|
+
SEGMENT_FILE="$DEVLOG_DIR/.segment-state"
|
|
34
|
+
ROUND_OPEN="$DEVLOG_DIR/.round-open"
|
|
35
|
+
AWAITING_FILE="$DEVLOG_DIR/.awaiting-reply"
|
|
36
|
+
DRIFT_FILE="$DEVLOG_DIR/.lessons-drift-state"
|
|
37
|
+
|
|
38
|
+
devlog_lock_acquire
|
|
39
|
+
trap 'devlog_lock_release' EXIT
|
|
40
|
+
|
|
41
|
+
INPUT="$(cat 2>/dev/null || true)"
|
|
42
|
+
SESSION_ID="$(json_str_field "$INPUT" session_id)"
|
|
43
|
+
|
|
44
|
+
if [ -f "$ROUND_OPEN" ]; then
|
|
45
|
+
DANGLING_DETAIL=""
|
|
46
|
+
if [ -n "$(detect_pending_question "$(json_str_field "$INPUT" transcript_path)")" ]; then
|
|
47
|
+
DANGLING_DETAIL="awaiting_question"
|
|
48
|
+
fi
|
|
49
|
+
bash "$HOOKS_DIR/close-open-round.sh" "dangling:next_prompt" "$DANGLING_DETAIL" || true
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
FOLD_ROUND=""
|
|
53
|
+
FOLD_NOTE="(回覆上一輪的問題)"
|
|
54
|
+
if [ -f "$AWAITING_FILE" ]; then
|
|
55
|
+
AWAIT_ROUND="$(json_int_get "$AWAITING_FILE" round)"
|
|
56
|
+
rm -f "$AWAITING_FILE" 2>/dev/null || true
|
|
57
|
+
case "$AWAIT_ROUND" in
|
|
58
|
+
''|*[!0-9]*) AWAIT_ROUND='' ;;
|
|
59
|
+
esac
|
|
60
|
+
if [ -n "$AWAIT_ROUND" ] && [ -f "$DEVLOG_FILE" ]; then
|
|
61
|
+
CURRENT_LAST="$(devlog_list_round_starts "$DEVLOG_FILE" | awk 'END { print $2 }')"
|
|
62
|
+
if [ "$CURRENT_LAST" = "$AWAIT_ROUND" ]; then
|
|
63
|
+
FOLD_ROUND="$AWAIT_ROUND"
|
|
64
|
+
fi
|
|
65
|
+
fi
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
SPAN_SKIP=0
|
|
69
|
+
if [ -f "$SPAN_FILE" ]; then
|
|
70
|
+
SPAN_TICKS="$(json_int_get "$SPAN_FILE" ticks_since_checkin)"
|
|
71
|
+
SPAN_MAX="$(json_int_get "$SPAN_FILE" max_silent_ticks)"
|
|
72
|
+
case "$SPAN_TICKS" in ''|*[!0-9]*) SPAN_TICKS='' ;; esac
|
|
73
|
+
case "$SPAN_MAX" in ''|*[!0-9]*) SPAN_MAX='' ;; esac
|
|
74
|
+
if [ -n "$SPAN_TICKS" ] && [ -n "$SPAN_MAX" ]; then
|
|
75
|
+
SPAN_SKIP=1
|
|
76
|
+
fi
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
PROMPT="$(json_str_field "$INPUT" prompt)"
|
|
80
|
+
|
|
81
|
+
# 背景 task-notification(例如子 agent 完成通知)不是使用者真的打字:不留原始
|
|
82
|
+
# XML,換成精簡摘要;也不當成新話題開新 Round,改折進最後一個 Round 當段落。
|
|
83
|
+
TASK_NOTIF=0
|
|
84
|
+
case "$PROMPT" in
|
|
85
|
+
*'<task-notification>'*'</task-notification>'*) TASK_NOTIF=1 ;;
|
|
86
|
+
esac
|
|
87
|
+
if [ "$TASK_NOTIF" -eq 1 ]; then
|
|
88
|
+
NOTIF_SUMMARY="$(printf '%s\n' "$PROMPT" | sed -n 's/.*<summary>\(.*\)<\/summary>.*/\1/p' | head -1)"
|
|
89
|
+
NOTIF_STATUS="$(printf '%s\n' "$PROMPT" | sed -n 's/.*<status>\(.*\)<\/status>.*/\1/p' | head -1)"
|
|
90
|
+
NOTIF_TASKID="$(printf '%s\n' "$PROMPT" | sed -n 's/.*<task-id>\(.*\)<\/task-id>.*/\1/p' | head -1)"
|
|
91
|
+
[ -n "$NOTIF_SUMMARY" ] || NOTIF_SUMMARY="背景任務通知"
|
|
92
|
+
NOTIF_META=""
|
|
93
|
+
[ -n "$NOTIF_STATUS" ] && NOTIF_META="status=$NOTIF_STATUS"
|
|
94
|
+
if [ -n "$NOTIF_TASKID" ]; then
|
|
95
|
+
if [ -n "$NOTIF_META" ]; then NOTIF_META="$NOTIF_META, task-id=$NOTIF_TASKID"
|
|
96
|
+
else NOTIF_META="task-id=$NOTIF_TASKID"
|
|
97
|
+
fi
|
|
98
|
+
fi
|
|
99
|
+
if [ -n "$NOTIF_META" ]; then
|
|
100
|
+
PROMPT="${NOTIF_SUMMARY}(${NOTIF_META})"
|
|
101
|
+
else
|
|
102
|
+
PROMPT="$NOTIF_SUMMARY"
|
|
103
|
+
fi
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
if [ "$SPAN_SKIP" -eq 1 ]; then
|
|
107
|
+
FOLD_ROUND=""
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
if [ "$TASK_NOTIF" -eq 1 ] && [ -z "$FOLD_ROUND" ] && [ "$SPAN_SKIP" -eq 0 ] && [ -f "$DEVLOG_FILE" ]; then
|
|
111
|
+
TASK_NOTIF_LAST="$(devlog_list_round_starts "$DEVLOG_FILE" | awk 'END { print $2 }')"
|
|
112
|
+
case "$TASK_NOTIF_LAST" in
|
|
113
|
+
''|*[!0-9]*) : ;;
|
|
114
|
+
*)
|
|
115
|
+
FOLD_ROUND="$TASK_NOTIF_LAST"
|
|
116
|
+
FOLD_NOTE="(背景任務通知)"
|
|
117
|
+
;;
|
|
118
|
+
esac
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
MISMATCH_FILE="$DEVLOG_DIR/.workspace-mismatch"
|
|
122
|
+
rm -f "$MISMATCH_FILE" 2>/dev/null || true
|
|
123
|
+
if [ "$SPAN_SKIP" -eq 0 ] && [ "$TASK_NOTIF" -eq 0 ] && [ -f "$DEVLOG_FILE" ]; then
|
|
124
|
+
CLAIM_ST="$(workspace_claim_state "$PROJECT_DIR" "$DEVLOG_FILE" 2>/dev/null || echo NO_CLAIM)"
|
|
125
|
+
if [ "$CLAIM_ST" = "MISMATCH" ]; then
|
|
126
|
+
LAST_START="$(devlog_list_round_starts "$DEVLOG_FILE" | awk 'END { print $1 }')"
|
|
127
|
+
LAST_END="$(devlog_block_end "$DEVLOG_FILE" "$LAST_START")"
|
|
128
|
+
CLAIMED_WS="$(devlog_round_workspace_body "$DEVLOG_FILE" "$LAST_START" "$LAST_END")"
|
|
129
|
+
LIVE_WS="$(workspace_snapshot "$PROJECT_DIR")"
|
|
130
|
+
if [ -n "$LIVE_WS" ]; then
|
|
131
|
+
printf '%s\n' "$LIVE_WS" > "$MISMATCH_FILE" 2>/dev/null || true
|
|
132
|
+
printf '%s\n' "上一輪 Handoff「#### 工作區」跟目前 git 不符。先在這一輪追加 ### 段落,寫宣稱 vs 實際(實際用下面「實際」逐字內容),再依實際工作樹行動,不要照上一輪「現況/下一步」的字面。"
|
|
133
|
+
printf '\n宣稱:\n%s\n\n實際:\n%s\n' "$CLAIMED_WS" "$LIVE_WS"
|
|
134
|
+
|
|
135
|
+
if [ -f "$DEVLOG_DIR/.lessons-enabled" ]; then
|
|
136
|
+
if [ ! -f "$DRIFT_FILE" ]; then
|
|
137
|
+
printf '%s\n' '{"mismatch_count": 0, "threshold": 3}' \
|
|
138
|
+
> "$DRIFT_FILE" 2>/dev/null || true
|
|
139
|
+
fi
|
|
140
|
+
DRIFT_COUNT="$(json_int_get "$DRIFT_FILE" mismatch_count)"
|
|
141
|
+
DRIFT_MAX="$(json_int_get "$DRIFT_FILE" threshold)"
|
|
142
|
+
case "$DRIFT_COUNT" in ''|*[!0-9]*) DRIFT_COUNT=0 ;; esac
|
|
143
|
+
case "$DRIFT_MAX" in ''|*[!0-9]*) DRIFT_MAX=3 ;; esac
|
|
144
|
+
NEW_DRIFT_COUNT=$((DRIFT_COUNT + 1))
|
|
145
|
+
if [ "$NEW_DRIFT_COUNT" -ge "$DRIFT_MAX" ]; then
|
|
146
|
+
json_int_set "$DRIFT_FILE" mismatch_count 0
|
|
147
|
+
printf '\n[Lessons Mode 提示] 工作區宣稱與實際不符已累積出現 %s 次(門檻 %s)。可考慮用 lessons-append.sh 記一筆流程教訓,非強制。\n' "$NEW_DRIFT_COUNT" "$DRIFT_MAX"
|
|
148
|
+
else
|
|
149
|
+
json_int_set "$DRIFT_FILE" mismatch_count "$NEW_DRIFT_COUNT"
|
|
150
|
+
fi
|
|
151
|
+
fi
|
|
152
|
+
fi
|
|
153
|
+
fi
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
if [ -n "$FOLD_ROUND" ]; then
|
|
157
|
+
devlog_reopen_last_round "$DEVLOG_FILE" "$ROUND_CURRENT" || : > "$ROUND_CURRENT"
|
|
158
|
+
if [ -z "$PROMPT" ]; then
|
|
159
|
+
PROMPT="(無 prompt)"
|
|
160
|
+
fi
|
|
161
|
+
TRUNC_NOTE=""
|
|
162
|
+
if [ "${#PROMPT}" -gt 4000 ]; then
|
|
163
|
+
PROMPT="${PROMPT:0:4000}"
|
|
164
|
+
TRUNC_NOTE="(後略,已截斷至 4000 字)"
|
|
165
|
+
fi
|
|
166
|
+
PROMPT="$(printf '%s' "$PROMPT" | sed 's/```/⟨fence⟩/g')"
|
|
167
|
+
PROMPT="$(printf '%s' "$PROMPT" | redact_prompt)"
|
|
168
|
+
|
|
169
|
+
TS="$(date +%H:%M 2>/dev/null || echo unknown)"
|
|
170
|
+
FULL_TS="$(date +%Y-%m-%dT%H:%M:%S%z 2>/dev/null || echo unknown)"
|
|
171
|
+
START_LINE="$(devlog_list_round_starts "$ROUND_CURRENT" | awk 'END { print $1 }')"
|
|
172
|
+
END_LINE="$(devlog_block_end "$ROUND_CURRENT" "$START_LINE")"
|
|
173
|
+
SEG_N=$(( $(devlog_count_segments "$ROUND_CURRENT" "$START_LINE" "$END_LINE") + 1 ))
|
|
174
|
+
|
|
175
|
+
SEG_TMP="$DEVLOG_DIR/.segment-insert.tmp"
|
|
176
|
+
{
|
|
177
|
+
printf '### 段落 %s - %s%s\n' "$SEG_N" "$TS" "$FOLD_NOTE"
|
|
178
|
+
printf '```text\n'
|
|
179
|
+
printf '%s\n' "$PROMPT"
|
|
180
|
+
printf '```\n'
|
|
181
|
+
if [ -n "$TRUNC_NOTE" ]; then
|
|
182
|
+
printf '%s\n' "$TRUNC_NOTE"
|
|
183
|
+
fi
|
|
184
|
+
printf '\n'
|
|
185
|
+
} > "$SEG_TMP" 2>/dev/null || true
|
|
186
|
+
|
|
187
|
+
if [ -f "$SEG_TMP" ]; then
|
|
188
|
+
devlog_insert_before_summary "$ROUND_CURRENT" "$START_LINE" "$END_LINE" "$SEG_TMP" > "$ROUND_CURRENT.tmp" 2>/dev/null \
|
|
189
|
+
&& mv "$ROUND_CURRENT.tmp" "$ROUND_CURRENT" 2>/dev/null || rm -f "$ROUND_CURRENT.tmp" 2>/dev/null || true
|
|
190
|
+
rm -f "$SEG_TMP" 2>/dev/null || true
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
printf '{"round": %s, "opened_at": "%s", "file": "%s"}\n' "$FOLD_ROUND" "$FULL_TS" "${DEVLOG_FILE##*/}" > "$ROUND_OPEN" 2>/dev/null || true
|
|
194
|
+
elif [ "$SPAN_SKIP" -eq 0 ]; then
|
|
195
|
+
if [ -z "$PROMPT" ]; then
|
|
196
|
+
PROMPT="(無 prompt)"
|
|
197
|
+
fi
|
|
198
|
+
TRUNC_NOTE=""
|
|
199
|
+
if [ "${#PROMPT}" -gt 4000 ]; then
|
|
200
|
+
PROMPT="${PROMPT:0:4000}"
|
|
201
|
+
TRUNC_NOTE="(後略,已截斷至 4000 字)"
|
|
202
|
+
fi
|
|
203
|
+
PROMPT="$(printf '%s' "$PROMPT" | sed 's/```/⟨fence⟩/g')"
|
|
204
|
+
PROMPT="$(printf '%s' "$PROMPT" | redact_prompt)"
|
|
205
|
+
|
|
206
|
+
# 救孤兒:正常情況下這裡 .round-current.md 應該已經是空的/不存在(上一輪
|
|
207
|
+
# 已經正常收尾併回 devlog.md)。如果不是——例如上一次 Stop 或
|
|
208
|
+
# close-open-round.sh 併入失敗,內容被孤立在這裡卻沒有任何機制知道要去
|
|
209
|
+
# 救它——在下面用 `>` 蓋掉新 skeleton 之前,先把它搶救併回 devlog.md,
|
|
210
|
+
# 而不是讓 `>` 直接蓋掉遺失。正常情況(檔案已空/不存在)這裡是 no-op。
|
|
211
|
+
# 必須放在下面的 LAST_N 掃描之前:這樣被搶救回來的孤兒 Round 才會被算進
|
|
212
|
+
# 編號,下一輪不會意外沿用它的號碼。
|
|
213
|
+
devlog_merge_round_current "$DEVLOG_FILE" "$ROUND_CURRENT"
|
|
214
|
+
|
|
215
|
+
LAST_N=0
|
|
216
|
+
if [ -f "$DEVLOG_FILE" ]; then
|
|
217
|
+
LAST_N="$(awk '
|
|
218
|
+
/^[ \t]*```/ { fence = !fence }
|
|
219
|
+
!fence && /^## Round / {
|
|
220
|
+
split($0, parts, /[ \t]+/)
|
|
221
|
+
n = parts[3] + 0
|
|
222
|
+
if (n > last) last = n
|
|
223
|
+
}
|
|
224
|
+
END { print last + 0 }
|
|
225
|
+
' "$DEVLOG_FILE" 2>/dev/null || echo 0)"
|
|
226
|
+
case "$LAST_N" in ''|*[!0-9]*) LAST_N=0 ;; esac
|
|
227
|
+
fi
|
|
228
|
+
NEXT_N=$((LAST_N + 1))
|
|
229
|
+
TS="$(date +%Y-%m-%dT%H:%M:%S%z 2>/dev/null || echo unknown)"
|
|
230
|
+
|
|
231
|
+
{
|
|
232
|
+
printf '## Round %s — %s\n\n' "$NEXT_N" "$TS"
|
|
233
|
+
printf '### User Input\n'
|
|
234
|
+
printf '```text\n'
|
|
235
|
+
printf '%s\n' "$PROMPT"
|
|
236
|
+
printf '```\n'
|
|
237
|
+
if [ -n "$TRUNC_NOTE" ]; then
|
|
238
|
+
printf '%s\n' "$TRUNC_NOTE"
|
|
239
|
+
fi
|
|
240
|
+
printf '\n### Status\nIN_PROGRESS\n'
|
|
241
|
+
} > "$ROUND_CURRENT" 2>/dev/null || true
|
|
242
|
+
|
|
243
|
+
if [ -f "$ROUND_CURRENT" ]; then
|
|
244
|
+
printf '{"round": %s, "opened_at": "%s", "file": "%s"}\n' "$NEXT_N" "$TS" "${DEVLOG_FILE##*/}" > "$ROUND_OPEN" 2>/dev/null || true
|
|
245
|
+
fi
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
if [ -f "$ROUND_CURRENT" ]; then
|
|
249
|
+
cksum < "$ROUND_CURRENT" > "$DEVLOG_DIR/.turn-start" 2>/dev/null || true
|
|
250
|
+
else
|
|
251
|
+
echo "MISSING" > "$DEVLOG_DIR/.turn-start" 2>/dev/null || true
|
|
252
|
+
fi
|
|
253
|
+
|
|
254
|
+
SPAN_WILL_PASS_THROUGH=0
|
|
255
|
+
if [ -f "$SPAN_FILE" ]; then
|
|
256
|
+
SPAN_TICKS="$(json_int_get "$SPAN_FILE" ticks_since_checkin)"
|
|
257
|
+
SPAN_MAX="$(json_int_get "$SPAN_FILE" max_silent_ticks)"
|
|
258
|
+
case "$SPAN_TICKS" in ''|*[!0-9]*) SPAN_TICKS='' ;; esac
|
|
259
|
+
case "$SPAN_MAX" in ''|*[!0-9]*) SPAN_MAX='' ;; esac
|
|
260
|
+
if [ -n "$SPAN_TICKS" ]; then
|
|
261
|
+
NEW_TICKS=$((SPAN_TICKS + 1))
|
|
262
|
+
json_int_set "$SPAN_FILE" ticks_since_checkin "$NEW_TICKS"
|
|
263
|
+
if [ -n "$SPAN_MAX" ] && [ "$NEW_TICKS" -lt "$SPAN_MAX" ]; then
|
|
264
|
+
SPAN_WILL_PASS_THROUGH=1
|
|
265
|
+
fi
|
|
266
|
+
fi
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
if [ "$SPAN_WILL_PASS_THROUGH" -eq 0 ] && [ -z "$FOLD_ROUND" ] && [ -f "$CHECKPOINT_FILE" ]; then
|
|
270
|
+
CP_ROUNDS="$(json_int_get "$CHECKPOINT_FILE" rounds_since_checkpoint)"
|
|
271
|
+
case "$CP_ROUNDS" in
|
|
272
|
+
''|*[!0-9]*) : ;;
|
|
273
|
+
*)
|
|
274
|
+
NEW_CP_ROUNDS=$((CP_ROUNDS + 1))
|
|
275
|
+
json_int_set "$CHECKPOINT_FILE" rounds_since_checkpoint "$NEW_CP_ROUNDS"
|
|
276
|
+
;;
|
|
277
|
+
esac
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
if [ -f "$SEGMENT_FILE" ]; then
|
|
281
|
+
SEG_EPOCH="$(json_int_get "$SEGMENT_FILE" last_change_epoch)"
|
|
282
|
+
SEG_MAX="$(json_int_get "$SEGMENT_FILE" max_silent_seconds)"
|
|
283
|
+
SEG_SUM_KEY="$(grep -o '"last_seen_cksum"[[:space:]]*:' "$SEGMENT_FILE" 2>/dev/null || echo '')"
|
|
284
|
+
case "$SEG_EPOCH" in ''|*[!0-9]*) SEG_EPOCH='' ;; esac
|
|
285
|
+
case "$SEG_MAX" in ''|*[!0-9]*) SEG_MAX='' ;; esac
|
|
286
|
+
if [ -n "$SEG_EPOCH" ] && [ -n "$SEG_MAX" ] && [ -n "$SEG_SUM_KEY" ]; then
|
|
287
|
+
SEG_NOW="$(date +%s 2>/dev/null || echo '')"
|
|
288
|
+
case "$SEG_NOW" in
|
|
289
|
+
''|*[!0-9]*) : ;;
|
|
290
|
+
*)
|
|
291
|
+
if [ -f "$ROUND_CURRENT" ]; then
|
|
292
|
+
SEG_CUR="$(cksum < "$ROUND_CURRENT" 2>/dev/null | tr -d '\n' || echo '')"
|
|
293
|
+
else
|
|
294
|
+
SEG_CUR="MISSING"
|
|
295
|
+
fi
|
|
296
|
+
if [ -n "$SEG_CUR" ]; then
|
|
297
|
+
json_int_set "$SEGMENT_FILE" last_change_epoch "$SEG_NOW"
|
|
298
|
+
json_str_set "$SEGMENT_FILE" last_seen_cksum "$SEG_CUR"
|
|
299
|
+
SEG_MT=""; SEG_SZ=""
|
|
300
|
+
if [ -f "$ROUND_CURRENT" ]; then
|
|
301
|
+
if SEG_ID="$(stat -f '%m %z' "$ROUND_CURRENT" 2>/dev/null || stat -c '%Y %s' "$ROUND_CURRENT" 2>/dev/null || true)"; then
|
|
302
|
+
SEG_MT="${SEG_ID%% *}"
|
|
303
|
+
SEG_SZ="${SEG_ID#* }"
|
|
304
|
+
fi
|
|
305
|
+
fi
|
|
306
|
+
if [ -n "$SEG_MT" ] && [ -n "$SEG_SZ" ]; then
|
|
307
|
+
if ! grep -q '"last_seen_mtime"' "$SEGMENT_FILE" 2>/dev/null; then
|
|
308
|
+
SEG_SID="$(json_str_get "$SEGMENT_FILE" session_id 2>/dev/null || true)"
|
|
309
|
+
if [ -n "$SESSION_ID" ]; then SEG_SID="$SESSION_ID"; fi
|
|
310
|
+
printf '{"last_change_epoch": %s, "last_seen_cksum": "%s", "last_seen_mtime": "%s", "last_seen_size": "%s", "max_silent_seconds": %s, "session_id": "%s"}\n' \
|
|
311
|
+
"$SEG_NOW" "$SEG_CUR" "$SEG_MT" "$SEG_SZ" "$SEG_MAX" "${SEG_SID}" > "$SEGMENT_FILE" 2>/dev/null || true
|
|
312
|
+
else
|
|
313
|
+
json_str_set "$SEGMENT_FILE" last_seen_mtime "$SEG_MT"
|
|
314
|
+
json_str_set "$SEGMENT_FILE" last_seen_size "$SEG_SZ"
|
|
315
|
+
if [ -n "$SESSION_ID" ]; then
|
|
316
|
+
json_str_set "$SEGMENT_FILE" session_id "$SESSION_ID"
|
|
317
|
+
fi
|
|
318
|
+
fi
|
|
319
|
+
elif [ -n "$SESSION_ID" ]; then
|
|
320
|
+
json_str_set "$SEGMENT_FILE" session_id "$SESSION_ID"
|
|
321
|
+
SEG_SESSION="$(json_str_get "$SEGMENT_FILE" session_id)"
|
|
322
|
+
if [ "$SEG_SESSION" != "$SESSION_ID" ]; then
|
|
323
|
+
printf '{"last_change_epoch": %s, "last_seen_cksum": "%s", "max_silent_seconds": %s, "session_id": "%s"}\n' \
|
|
324
|
+
"$SEG_NOW" "$SEG_CUR" "$SEG_MAX" "$SESSION_ID" > "$SEGMENT_FILE" 2>/dev/null || true
|
|
325
|
+
fi
|
|
326
|
+
fi
|
|
327
|
+
fi
|
|
328
|
+
;;
|
|
329
|
+
esac
|
|
330
|
+
fi
|
|
331
|
+
fi
|
|
332
|
+
|
|
333
|
+
exit 0
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Run every hook self-check. Usage: bash hooks/scripts/run-tests.sh
|
|
3
|
+
set -uo pipefail
|
|
4
|
+
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
FAIL=0
|
|
6
|
+
shopt -s nullglob
|
|
7
|
+
for t in "$DIR"/tests/test-*.sh; do
|
|
8
|
+
echo "=== $(basename "$t") ==="
|
|
9
|
+
if ! bash "$t"; then
|
|
10
|
+
FAIL=1
|
|
11
|
+
fi
|
|
12
|
+
done
|
|
13
|
+
echo "=== test-adapters.sh ==="
|
|
14
|
+
if ! bash "$DIR/../../cursor/hooks/test-adapters.sh"; then
|
|
15
|
+
FAIL=1
|
|
16
|
+
fi
|
|
17
|
+
echo "=== codex/test-adapters.sh ==="
|
|
18
|
+
if ! bash "$DIR/../../codex/hooks/test-adapters.sh"; then
|
|
19
|
+
FAIL=1
|
|
20
|
+
fi
|
|
21
|
+
if [ "$FAIL" -ne 0 ]; then
|
|
22
|
+
echo "Some hook self-checks FAILED."
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
echo "All hook self-checks passed."
|
|
26
|
+
exit 0
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# User-invoked: /devlog-tracker:segment-watch filesystem side.
|
|
3
|
+
# Sets Segment Watch's max_silent_seconds threshold. $1 is required: a
|
|
4
|
+
# positive integer number of seconds.
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
_src="${BASH_SOURCE[0]}"
|
|
7
|
+
SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
|
|
8
|
+
# shellcheck source=json-field.sh
|
|
9
|
+
. "$SCRIPT_DIR/json-field.sh"
|
|
10
|
+
|
|
11
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
|
12
|
+
DEVLOG_DIR="$PROJECT_DIR/.devlog"
|
|
13
|
+
|
|
14
|
+
if [ ! -f "$DEVLOG_DIR/.enabled" ]; then
|
|
15
|
+
echo "NOT_STARTED"
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
SECONDS_ARG="${1:-}"
|
|
20
|
+
case "$SECONDS_ARG" in
|
|
21
|
+
''|*[!0-9]*|0)
|
|
22
|
+
echo "max_silent_seconds must be a positive integer number of seconds, got: $SECONDS_ARG" >&2
|
|
23
|
+
exit 1
|
|
24
|
+
;;
|
|
25
|
+
esac
|
|
26
|
+
|
|
27
|
+
if [ -f "$DEVLOG_DIR/.segment-state" ]; then
|
|
28
|
+
json_int_set "$DEVLOG_DIR/.segment-state" max_silent_seconds "$SECONDS_ARG"
|
|
29
|
+
APPLIED="$(json_int_get "$DEVLOG_DIR/.segment-state" max_silent_seconds)"
|
|
30
|
+
if [ "$APPLIED" != "$SECONDS_ARG" ]; then
|
|
31
|
+
# Existing file didn't have a matching max_silent_seconds key to patch
|
|
32
|
+
# (malformed / hand-edited) -- rebuild it fresh rather than leave the
|
|
33
|
+
# requested value silently unapplied.
|
|
34
|
+
LAST_EPOCH="$(json_int_get "$DEVLOG_DIR/.segment-state" last_change_epoch)"
|
|
35
|
+
LAST_SUM="$(json_str_get "$DEVLOG_DIR/.segment-state" last_seen_cksum)"
|
|
36
|
+
SESSION="$(json_str_get "$DEVLOG_DIR/.segment-state" session_id)"
|
|
37
|
+
case "$LAST_EPOCH" in ''|*[!0-9]*) LAST_EPOCH=0 ;; esac
|
|
38
|
+
printf '%s\n' "{\"last_change_epoch\": ${LAST_EPOCH}, \"last_seen_cksum\": \"${LAST_SUM}\", \"max_silent_seconds\": ${SECONDS_ARG}, \"session_id\": \"${SESSION}\"}" \
|
|
39
|
+
> "$DEVLOG_DIR/.segment-state" || exit 1
|
|
40
|
+
fi
|
|
41
|
+
else
|
|
42
|
+
printf '%s\n' "{\"last_change_epoch\": 0, \"last_seen_cksum\": \"\", \"max_silent_seconds\": ${SECONDS_ARG}, \"session_id\": \"\"}" \
|
|
43
|
+
> "$DEVLOG_DIR/.segment-state" || exit 1
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
echo "SEGMENT_MAX_SILENT_SECONDS=$(json_int_get "$DEVLOG_DIR/.segment-state" max_silent_seconds)"
|
|
47
|
+
exit 0
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PreToolUse hook:同一輪若太久沒改 .round-current.md,擋住下一個工具,逼補 ### 段落。
|
|
3
|
+
# fail-open:這支腳本自己出錯一律 exit 0,不該卡死使用者的 session。
|
|
4
|
+
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
|
|
7
|
+
_src="${BASH_SOURCE[0]}"
|
|
8
|
+
SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
|
|
9
|
+
# shellcheck source=json-field.sh
|
|
10
|
+
. "$SCRIPT_DIR/json-field.sh"
|
|
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
|
+
[ -f "$PROJECT_DIR/.devlog/.enabled" ] || exit 0
|
|
18
|
+
devlog_resolve_paths "$PROJECT_DIR"
|
|
19
|
+
ROUND_CURRENT="$DEVLOG_DIR/.round-current.md"
|
|
20
|
+
SEGMENT_FILE="$DEVLOG_DIR/.segment-state"
|
|
21
|
+
|
|
22
|
+
INPUT="$(cat 2>/dev/null || true)"
|
|
23
|
+
[ -n "$INPUT" ] || exit 0
|
|
24
|
+
|
|
25
|
+
TOOL_NAME="$(json_str_field "$INPUT" tool_name)"
|
|
26
|
+
FILE_PATH=""
|
|
27
|
+
COMMAND=""
|
|
28
|
+
if command -v jq >/dev/null 2>&1; then
|
|
29
|
+
FILE_PATH="$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty' 2>/dev/null || echo '')"
|
|
30
|
+
[ "$FILE_PATH" = "null" ] && FILE_PATH=""
|
|
31
|
+
COMMAND="$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null || echo '')"
|
|
32
|
+
[ "$COMMAND" = "null" ] && COMMAND=""
|
|
33
|
+
else
|
|
34
|
+
FILE_PATH="$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' 2>/dev/null | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || echo '')"
|
|
35
|
+
if [ -z "$FILE_PATH" ]; then
|
|
36
|
+
FILE_PATH="$(printf '%s' "$INPUT" | grep -o '"path"[[:space:]]*:[[:space:]]*"[^"]*"' 2>/dev/null | head -1 | sed 's/.*"path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || echo '')"
|
|
37
|
+
fi
|
|
38
|
+
COMMAND="$(printf '%s' "$INPUT" | grep -o '"command"[[:space:]]*:[[:space:]]*"[^"]*"' 2>/dev/null | head -1 | sed 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || echo '')"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Blocked-and-stuck recovery: Bash itself is denied while blocked, so there
|
|
42
|
+
# was no way to run `git status`/`git diff` to see what actually changed —
|
|
43
|
+
# exactly the thing both valves below are asking Claude to reconcile against.
|
|
44
|
+
# Allow a small allowlist of git subcommands that are non-mutating in every
|
|
45
|
+
# documented invocation form (no flag combination writes). Reject anything
|
|
46
|
+
# with shell metacharacters first — chaining/redirection/substitution could
|
|
47
|
+
# otherwise smuggle a second, unrelated command past the git-prefix match.
|
|
48
|
+
is_safe_readonly_git_command() {
|
|
49
|
+
local cmd
|
|
50
|
+
cmd="$(printf '%s' "$1" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
|
51
|
+
[ -n "$cmd" ] || return 1
|
|
52
|
+
case "$cmd" in
|
|
53
|
+
*';'*|*'&'*|*'|'*|*'`'*|*'$('*|*'>'*|*'<'*|*$'\n'*) return 1 ;;
|
|
54
|
+
esac
|
|
55
|
+
case "$cmd" in
|
|
56
|
+
git\ status|git\ status\ *) return 0 ;;
|
|
57
|
+
git\ diff|git\ diff\ *) return 0 ;;
|
|
58
|
+
git\ log|git\ log\ *) return 0 ;;
|
|
59
|
+
git\ show|git\ show\ *) return 0 ;;
|
|
60
|
+
git\ rev-parse|git\ rev-parse\ *) return 0 ;;
|
|
61
|
+
esac
|
|
62
|
+
return 1
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
is_devlog_tool_allowed() {
|
|
66
|
+
case "$1" in
|
|
67
|
+
Write|Edit|StrReplace|Read|Grep)
|
|
68
|
+
case "$2" in
|
|
69
|
+
.devlog/.round-current.md|*/.devlog/.round-current.md) return 0 ;;
|
|
70
|
+
esac
|
|
71
|
+
;;
|
|
72
|
+
Bash)
|
|
73
|
+
is_safe_readonly_git_command "$3" && return 0
|
|
74
|
+
;;
|
|
75
|
+
esac
|
|
76
|
+
return 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
AGENT_ID="$(json_str_field "$INPUT" agent_id)"
|
|
80
|
+
[ "$AGENT_ID" = "null" ] && AGENT_ID=""
|
|
81
|
+
if [ -n "$AGENT_ID" ]; then
|
|
82
|
+
exit 0
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
MISMATCH_FILE="$DEVLOG_DIR/.workspace-mismatch"
|
|
86
|
+
if [ -f "$MISMATCH_FILE" ]; then
|
|
87
|
+
LIVE_MARK="$(cat "$MISMATCH_FILE" 2>/dev/null || true)"
|
|
88
|
+
if [ -z "$LIVE_MARK" ]; then
|
|
89
|
+
rm -f "$MISMATCH_FILE" 2>/dev/null || true
|
|
90
|
+
else
|
|
91
|
+
SEGMENTS=""
|
|
92
|
+
if [ -f "$ROUND_CURRENT" ]; then
|
|
93
|
+
LAST_START="$(devlog_list_round_starts "$ROUND_CURRENT" | awk 'END { print $1 }')"
|
|
94
|
+
if [ -n "$LAST_START" ]; then
|
|
95
|
+
LAST_END="$(devlog_block_end "$ROUND_CURRENT" "$LAST_START")"
|
|
96
|
+
SEGMENTS="$(devlog_round_segments_body "$ROUND_CURRENT" "$LAST_START" "$LAST_END")"
|
|
97
|
+
fi
|
|
98
|
+
fi
|
|
99
|
+
segments_flat="$(printf '%s' "$SEGMENTS" | tr '\n' '\036')"
|
|
100
|
+
mark_flat="$(printf '%s' "$LIVE_MARK" | tr '\n' '\036')"
|
|
101
|
+
# "$mark_flat" is quoted so it is literal; dirty paths with *?[ do not glob.
|
|
102
|
+
case "$segments_flat" in
|
|
103
|
+
*"$mark_flat"*) rm -f "$MISMATCH_FILE" 2>/dev/null || true ;;
|
|
104
|
+
*)
|
|
105
|
+
if ! is_devlog_tool_allowed "$TOOL_NAME" "$FILE_PATH" "$COMMAND"; then
|
|
106
|
+
echo "上一輪「#### 工作區」跟目前 git 不符。請先 Read .devlog/.round-current.md,再用 Edit/StrReplace 在這一輪追加 ### 段落,把下面「實際」逐字貼進段落(宣稱 vs 實際)。寫完再呼叫其他工具。不要照上一輪 Handoff「現況/下一步」的字面行動。(唯讀的 git status/diff/log/show/rev-parse 仍可執行,方便自行核對。)" >&2
|
|
107
|
+
echo "" >&2
|
|
108
|
+
echo "實際:" >&2
|
|
109
|
+
printf '%s\n' "$LIVE_MARK" >&2
|
|
110
|
+
exit 2
|
|
111
|
+
fi
|
|
112
|
+
;;
|
|
113
|
+
esac
|
|
114
|
+
fi
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
[ -f "$SEGMENT_FILE" ] || exit 0
|
|
118
|
+
|
|
119
|
+
STORED="$(json_str_get "$SEGMENT_FILE" session_id 2>/dev/null || true)"
|
|
120
|
+
INCOMING="$(json_str_field "$INPUT" session_id)"
|
|
121
|
+
if [ -n "$STORED" ] && [ -n "$INCOMING" ] && [ "$STORED" != "$INCOMING" ]; then
|
|
122
|
+
exit 0
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
SEG_EPOCH="$(json_int_get "$SEGMENT_FILE" last_change_epoch)"
|
|
126
|
+
SEG_MAX="$(json_int_get "$SEGMENT_FILE" max_silent_seconds)"
|
|
127
|
+
SEG_SUM="$(json_str_get "$SEGMENT_FILE" last_seen_cksum)"
|
|
128
|
+
SEG_SUM_KEY="$(grep -o '"last_seen_cksum"[[:space:]]*:' "$SEGMENT_FILE" 2>/dev/null || echo '')"
|
|
129
|
+
|
|
130
|
+
case "$SEG_EPOCH" in ''|*[!0-9]*) exit 0 ;; esac
|
|
131
|
+
case "$SEG_MAX" in ''|*[!0-9]*) exit 0 ;; esac
|
|
132
|
+
[ -n "$SEG_SUM_KEY" ] || exit 0
|
|
133
|
+
|
|
134
|
+
persist_seen() {
|
|
135
|
+
local epoch="$1" sum="$2" mt="$3" sz="$4"
|
|
136
|
+
json_int_set "$SEGMENT_FILE" last_change_epoch "$epoch"
|
|
137
|
+
json_str_set "$SEGMENT_FILE" last_seen_cksum "$sum"
|
|
138
|
+
[ -n "$mt" ] && json_str_set "$SEGMENT_FILE" last_seen_mtime "$mt"
|
|
139
|
+
[ -n "$sz" ] && json_str_set "$SEGMENT_FILE" last_seen_size "$sz"
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
devlog_file_identity() {
|
|
143
|
+
# prints: "<mtime_epoch> <size_bytes>" or empty on failure
|
|
144
|
+
local f="$1" mt sz
|
|
145
|
+
if mt="$(stat -f '%m' "$f" 2>/dev/null)" && sz="$(stat -f '%z' "$f" 2>/dev/null)"; then
|
|
146
|
+
printf '%s %s\n' "$mt" "$sz"
|
|
147
|
+
return 0
|
|
148
|
+
fi
|
|
149
|
+
if mt="$(stat -c '%Y' "$f" 2>/dev/null)" && sz="$(stat -c '%s' "$f" 2>/dev/null)"; then
|
|
150
|
+
printf '%s %s\n' "$mt" "$sz"
|
|
151
|
+
return 0
|
|
152
|
+
fi
|
|
153
|
+
return 1
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
NOW="$(date +%s 2>/dev/null || echo '')"
|
|
157
|
+
case "$NOW" in ''|*[!0-9]*) exit 0 ;; esac
|
|
158
|
+
|
|
159
|
+
ID_MT=""; ID_SZ=""
|
|
160
|
+
if [ -f "$ROUND_CURRENT" ]; then
|
|
161
|
+
ID="$(devlog_file_identity "$ROUND_CURRENT" || true)"
|
|
162
|
+
ID_MT="${ID%% *}"
|
|
163
|
+
ID_SZ="${ID#* }"
|
|
164
|
+
STORED_MT="$(json_str_get "$SEGMENT_FILE" last_seen_mtime 2>/dev/null || true)"
|
|
165
|
+
STORED_SZ="$(json_str_get "$SEGMENT_FILE" last_seen_size 2>/dev/null || true)"
|
|
166
|
+
if [ -n "$ID_MT" ] && [ -n "$ID_SZ" ] && [ "$ID_MT" = "$STORED_MT" ] && [ "$ID_SZ" = "$STORED_SZ" ] && [ -n "$SEG_SUM" ]; then
|
|
167
|
+
CURRENT="$SEG_SUM"
|
|
168
|
+
else
|
|
169
|
+
CURRENT="$(cksum < "$ROUND_CURRENT" 2>/dev/null | tr -d '\n' || echo '')"
|
|
170
|
+
ID="$(devlog_file_identity "$ROUND_CURRENT" || true)"
|
|
171
|
+
ID_MT="${ID%% *}"
|
|
172
|
+
ID_SZ="${ID#* }"
|
|
173
|
+
fi
|
|
174
|
+
else
|
|
175
|
+
CURRENT="MISSING"
|
|
176
|
+
ID_MT=""; ID_SZ=""
|
|
177
|
+
fi
|
|
178
|
+
[ -n "$CURRENT" ] || exit 0
|
|
179
|
+
|
|
180
|
+
if [ "$CURRENT" != "$SEG_SUM" ]; then
|
|
181
|
+
persist_seen "$NOW" "$CURRENT" "$ID_MT" "$ID_SZ"
|
|
182
|
+
exit 0
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
[ -n "$TOOL_NAME" ] || exit 0
|
|
186
|
+
|
|
187
|
+
if is_devlog_tool_allowed "$TOOL_NAME" "$FILE_PATH" "$COMMAND"; then
|
|
188
|
+
exit 0
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
ELAPSED=$((NOW - SEG_EPOCH))
|
|
192
|
+
if [ "$ELAPSED" -ge "$SEG_MAX" ]; then
|
|
193
|
+
echo "這一輪已經 ${ELAPSED} 秒沒有更新 .devlog/.round-current.md(門檻 ${SEG_MAX} 秒)。請先 Read .devlog/.round-current.md,再用 Edit 或 StrReplace **追加**一段「### 段落」(一行也可以);禁止用 Write 覆寫整份檔。寫完再繼續呼叫其他工具。(唯讀的 git status/diff/log/show/rev-parse 仍可執行。)" >&2
|
|
194
|
+
exit 2
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
exit 0
|