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,75 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Self-check for segment-watch-set.sh. Run:
|
|
3
|
+
# bash hooks/scripts/test-segment-watch-set.sh
|
|
4
|
+
set -uo pipefail
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
6
|
+
TMP_ROOT="$(mktemp -d)"
|
|
7
|
+
trap 'rm -rf "$TMP_ROOT"' EXIT
|
|
8
|
+
export CLAUDE_PROJECT_DIR="$TMP_ROOT"
|
|
9
|
+
FAIL=0
|
|
10
|
+
|
|
11
|
+
assert_exit() {
|
|
12
|
+
local desc="$1" expected="$2" actual="$3"
|
|
13
|
+
if [ "$actual" -eq "$expected" ]; then echo "PASS: $desc"
|
|
14
|
+
else echo "FAIL: $desc (expected $expected got $actual)"; FAIL=1; fi
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
# --- 1: never started -> NOT_STARTED, no files created ----------------------
|
|
18
|
+
OUT="$(bash "$SCRIPT_DIR/segment-watch-set.sh" 300)"
|
|
19
|
+
assert_exit "never started -> exit 0" 0 $?
|
|
20
|
+
case "$OUT" in
|
|
21
|
+
*"NOT_STARTED"*) echo "PASS: reports NOT_STARTED" ;;
|
|
22
|
+
*) echo "FAIL: expected NOT_STARTED, got $OUT"; FAIL=1 ;;
|
|
23
|
+
esac
|
|
24
|
+
if [ -e "$TMP_ROOT/.devlog" ]; then
|
|
25
|
+
echo "FAIL: NOT_STARTED path should not create .devlog"; FAIL=1
|
|
26
|
+
else
|
|
27
|
+
echo "PASS: NOT_STARTED path creates no .devlog"
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
mkdir -p "$TMP_ROOT/.devlog"
|
|
31
|
+
touch "$TMP_ROOT/.devlog/.enabled"
|
|
32
|
+
|
|
33
|
+
# --- 2: started, no .segment-state yet -> creates one with the given value --
|
|
34
|
+
OUT="$(bash "$SCRIPT_DIR/segment-watch-set.sh" 300)"
|
|
35
|
+
assert_exit "fresh set -> exit 0" 0 $?
|
|
36
|
+
grep -q '"max_silent_seconds": 300' "$TMP_ROOT/.devlog/.segment-state" \
|
|
37
|
+
&& echo "PASS: fresh set writes 300" || { echo "FAIL: fresh set"; FAIL=1; }
|
|
38
|
+
case "$OUT" in
|
|
39
|
+
*"SEGMENT_MAX_SILENT_SECONDS=300"*) echo "PASS: fresh set reports 300" ;;
|
|
40
|
+
*) echo "FAIL: expected SEGMENT_MAX_SILENT_SECONDS=300, got $OUT"; FAIL=1 ;;
|
|
41
|
+
esac
|
|
42
|
+
|
|
43
|
+
# --- 3: started, existing .segment-state -> overrides just the threshold ----
|
|
44
|
+
printf '%s\n' '{"last_change_epoch": 42, "last_seen_cksum": "abc", "max_silent_seconds": 300, "session_id": "s1"}' \
|
|
45
|
+
> "$TMP_ROOT/.devlog/.segment-state"
|
|
46
|
+
OUT="$(bash "$SCRIPT_DIR/segment-watch-set.sh" 120)"
|
|
47
|
+
assert_exit "override set -> exit 0" 0 $?
|
|
48
|
+
grep -q '"max_silent_seconds": 120' "$TMP_ROOT/.devlog/.segment-state" \
|
|
49
|
+
&& echo "PASS: override set writes 120" || { echo "FAIL: override set"; FAIL=1; }
|
|
50
|
+
grep -q '"last_change_epoch": 42' "$TMP_ROOT/.devlog/.segment-state" \
|
|
51
|
+
&& echo "PASS: override set keeps last_change_epoch" || { echo "FAIL: last_change_epoch clobbered"; FAIL=1; }
|
|
52
|
+
grep -q '"session_id": "s1"' "$TMP_ROOT/.devlog/.segment-state" \
|
|
53
|
+
&& echo "PASS: override set keeps session_id" || { echo "FAIL: session_id clobbered"; FAIL=1; }
|
|
54
|
+
case "$OUT" in
|
|
55
|
+
*"SEGMENT_MAX_SILENT_SECONDS=120"*) echo "PASS: override set reports 120" ;;
|
|
56
|
+
*) echo "FAIL: expected SEGMENT_MAX_SILENT_SECONDS=120, got $OUT"; FAIL=1 ;;
|
|
57
|
+
esac
|
|
58
|
+
|
|
59
|
+
# --- 4: invalid seconds rejected, state untouched ---------------------------
|
|
60
|
+
BEFORE="$(cat "$TMP_ROOT/.devlog/.segment-state")"
|
|
61
|
+
bash "$SCRIPT_DIR/segment-watch-set.sh" abc >/dev/null 2>"$TMP_ROOT/err"
|
|
62
|
+
assert_exit "bad arg -> exit 1" 1 $?
|
|
63
|
+
AFTER="$(cat "$TMP_ROOT/.devlog/.segment-state")"
|
|
64
|
+
if [ "$BEFORE" = "$AFTER" ]; then echo "PASS: bad arg leaves segment-state untouched"
|
|
65
|
+
else echo "FAIL: bad arg changed segment-state"; FAIL=1; fi
|
|
66
|
+
[ -s "$TMP_ROOT/err" ] && echo "PASS: bad arg prints stderr" || { echo "FAIL: bad arg silent"; FAIL=1; }
|
|
67
|
+
|
|
68
|
+
bash "$SCRIPT_DIR/segment-watch-set.sh" 0 >/dev/null 2>&1
|
|
69
|
+
assert_exit "zero -> exit 1" 1 $?
|
|
70
|
+
|
|
71
|
+
bash "$SCRIPT_DIR/segment-watch-set.sh" >/dev/null 2>&1
|
|
72
|
+
assert_exit "missing arg -> exit 1" 1 $?
|
|
73
|
+
|
|
74
|
+
if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
|
|
75
|
+
else echo "Some checks FAILED."; exit 1; fi
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Self-check for segment-watch.sh (+ round-start.sh clock reset in later
|
|
3
|
+
# scenarios). No framework — plain assert-and-exit. Run directly:
|
|
4
|
+
# bash hooks/scripts/test-segment-watch.sh
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
8
|
+
TMP_ROOT="$(mktemp -d)"
|
|
9
|
+
trap 'rm -rf "$TMP_ROOT"' EXIT
|
|
10
|
+
|
|
11
|
+
export CLAUDE_PROJECT_DIR="$TMP_ROOT"
|
|
12
|
+
DEVLOG_DIR="$TMP_ROOT/.devlog"
|
|
13
|
+
mkdir -p "$DEVLOG_DIR"
|
|
14
|
+
touch "$DEVLOG_DIR/.enabled"
|
|
15
|
+
printf 'seed\n' > "$DEVLOG_DIR/.round-current.md"
|
|
16
|
+
SEED_CKSUM="$(cksum < "$DEVLOG_DIR/.round-current.md" | tr -d '\n')"
|
|
17
|
+
|
|
18
|
+
FAIL=0
|
|
19
|
+
assert_exit() {
|
|
20
|
+
local desc="$1" expected="$2" actual="$3"
|
|
21
|
+
if [ "$actual" -eq "$expected" ]; then
|
|
22
|
+
echo "PASS: $desc"
|
|
23
|
+
else
|
|
24
|
+
echo "FAIL: $desc (expected exit $expected, got $actual)"
|
|
25
|
+
FAIL=1
|
|
26
|
+
fi
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
assert_contains() {
|
|
30
|
+
local desc="$1" needle="$2" haystack="$3"
|
|
31
|
+
case "$haystack" in
|
|
32
|
+
*"$needle"*) echo "PASS: $desc" ;;
|
|
33
|
+
*) echo "FAIL: $desc (missing: $needle)"; FAIL=1 ;;
|
|
34
|
+
esac
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
write_state() {
|
|
38
|
+
local epoch="$1" sum="$2" max="$3"
|
|
39
|
+
printf '{"last_change_epoch": %s, "last_seen_cksum": "%s", "max_silent_seconds": %s, "session_id": "aaa"}\n' \
|
|
40
|
+
"$epoch" "$sum" "$max" > "$DEVLOG_DIR/.segment-state"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
NOW="$(date +%s)"
|
|
44
|
+
EXPIRED="$((NOW - 960))"
|
|
45
|
+
BASH_PAYLOAD='{"tool_name":"Bash","tool_input":{"command":"echo hi"}}'
|
|
46
|
+
WRITE_REL='{"tool_name":"Write","tool_input":{"file_path":".devlog/.round-current.md"}}'
|
|
47
|
+
WRITE_ABS="$(printf '{"tool_name":"Write","tool_input":{"file_path":"%s/.devlog/.round-current.md"}}' "$TMP_ROOT")"
|
|
48
|
+
WRITE_OTHER='{"tool_name":"Write","tool_input":{"file_path":"src/foo.ts"}}'
|
|
49
|
+
EDIT_REL='{"tool_name":"Edit","tool_input":{"file_path":".devlog/.round-current.md"}}'
|
|
50
|
+
STRREPLACE_REL='{"tool_name":"StrReplace","tool_input":{"file_path":".devlog/.round-current.md"}}'
|
|
51
|
+
|
|
52
|
+
# --- Scenario 1: no .enabled -> exit 0 ------------------------------------
|
|
53
|
+
rm -f "$DEVLOG_DIR/.enabled"
|
|
54
|
+
write_state "$EXPIRED" "$SEED_CKSUM" 900
|
|
55
|
+
printf '%s' "$BASH_PAYLOAD" | bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
56
|
+
assert_exit "no .enabled -> allowed" 0 $?
|
|
57
|
+
touch "$DEVLOG_DIR/.enabled"
|
|
58
|
+
|
|
59
|
+
# --- Scenario 2: fresh clock, Bash immediately -> exit 0 --------------------
|
|
60
|
+
write_state "$NOW" "$SEED_CKSUM" 900
|
|
61
|
+
printf '%s' "$BASH_PAYLOAD" | bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
62
|
+
assert_exit "fresh epoch, Bash -> allowed" 0 $?
|
|
63
|
+
|
|
64
|
+
# --- Scenario 3: expired + Bash -> exit 2 + stderr phrase -------------------
|
|
65
|
+
write_state "$EXPIRED" "$SEED_CKSUM" 900
|
|
66
|
+
ERR="$(printf '%s' "$BASH_PAYLOAD" | bash "$SCRIPT_DIR/segment-watch.sh" 2>&1 >/dev/null)"
|
|
67
|
+
assert_exit "expired + Bash -> blocked" 2 $?
|
|
68
|
+
assert_contains "block message names ### 段落" "### 段落" "$ERR"
|
|
69
|
+
assert_contains "block message names 門檻 900" "門檻 900 秒" "$ERR"
|
|
70
|
+
assert_contains "block message starts 這一輪已經" "這一輪已經" "$ERR"
|
|
71
|
+
assert_contains "block message ends 寫完再繼續呼叫其他工具" "寫完再繼續呼叫其他工具" "$ERR"
|
|
72
|
+
|
|
73
|
+
# --- Scenario 3a: expired silence valve also allows read-only git inspection,
|
|
74
|
+
# same allowlist as the workspace-mismatch block above ----------------------
|
|
75
|
+
write_state "$EXPIRED" "$SEED_CKSUM" 900
|
|
76
|
+
printf '%s' '{"tool_name":"Bash","tool_input":{"command":"git log -5"}}' \
|
|
77
|
+
| bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
78
|
+
assert_exit "expired silence + read-only git log -> allowed" 0 $?
|
|
79
|
+
|
|
80
|
+
write_state "$EXPIRED" "$SEED_CKSUM" 900
|
|
81
|
+
printf '%s' '{"tool_name":"Bash","tool_input":{"command":"git status && echo done"}}' \
|
|
82
|
+
| bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
83
|
+
assert_exit "expired silence + chained command -> still blocked" 2 $?
|
|
84
|
+
|
|
85
|
+
# --- Scenario 3b: only the parent session uses this valve -------------------
|
|
86
|
+
write_state "$EXPIRED" "$SEED_CKSUM" 900
|
|
87
|
+
printf '%s' '{"tool_name":"Bash","session_id":"aaa"}' | bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
88
|
+
assert_exit "expired + matching session_id -> blocked" 2 $?
|
|
89
|
+
write_state "$EXPIRED" "$SEED_CKSUM" 900
|
|
90
|
+
printf '%s' '{"tool_name":"Bash","session_id":"bbb"}' | bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
91
|
+
assert_exit "expired + different session_id -> allowed" 0 $?
|
|
92
|
+
write_state "$EXPIRED" "$SEED_CKSUM" 900
|
|
93
|
+
printf '%s' '{"tool_name":"Bash"}' | bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
94
|
+
assert_exit "expired + missing session_id -> blocked" 2 $?
|
|
95
|
+
|
|
96
|
+
# --- expired + Read / Grep / agent_id (unblock plan) ------------------------
|
|
97
|
+
write_state "$EXPIRED" "$SEED_CKSUM" 900
|
|
98
|
+
printf '%s' '{"tool_name":"Read","tool_input":{"file_path":".devlog/.round-current.md"},"session_id":"aaa"}' \
|
|
99
|
+
| bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
100
|
+
assert_exit "expired + Read .round-current.md -> allowed" 0 $?
|
|
101
|
+
|
|
102
|
+
write_state "$EXPIRED" "$SEED_CKSUM" 900
|
|
103
|
+
printf '%s' '{"tool_name":"Read","tool_input":{"file_path":"README.md"},"session_id":"aaa"}' \
|
|
104
|
+
| bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
105
|
+
assert_exit "expired + Read other -> blocked" 2 $?
|
|
106
|
+
|
|
107
|
+
write_state "$EXPIRED" "$SEED_CKSUM" 900
|
|
108
|
+
printf '%s' '{"tool_name":"Grep","tool_input":{"path":".devlog/.round-current.md","pattern":"Round"},"session_id":"aaa"}' \
|
|
109
|
+
| bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
110
|
+
assert_exit "expired + Grep .round-current.md -> allowed" 0 $?
|
|
111
|
+
|
|
112
|
+
write_state "$EXPIRED" "$SEED_CKSUM" 900
|
|
113
|
+
printf '%s' '{"tool_name":"Bash","tool_input":{},"session_id":"aaa","agent_id":"agent-xyz"}' \
|
|
114
|
+
| bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
115
|
+
assert_exit "expired + agent_id -> allowed" 0 $?
|
|
116
|
+
|
|
117
|
+
write_state "$EXPIRED" "$SEED_CKSUM" 900
|
|
118
|
+
printf '%s' '{"tool_name":"Bash","tool_input":{},"session_id":"aaa","agent_id":""}' \
|
|
119
|
+
| bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
120
|
+
assert_exit "expired + empty agent_id -> blocked" 2 $?
|
|
121
|
+
|
|
122
|
+
write_state "$EXPIRED" "$SEED_CKSUM" 900
|
|
123
|
+
ERR="$(printf '%s' '{"tool_name":"Bash","tool_input":{},"session_id":"aaa"}' \
|
|
124
|
+
| bash "$SCRIPT_DIR/segment-watch.sh" 2>&1 >/dev/null || true)"
|
|
125
|
+
case "$ERR" in
|
|
126
|
+
*Read*.devlog/.round-current.md*禁止*覆寫*) echo "PASS: stderr instructs Read then append" ;;
|
|
127
|
+
*) echo "FAIL: stderr [$ERR]"; FAIL=1 ;;
|
|
128
|
+
esac
|
|
129
|
+
|
|
130
|
+
# --- Scenario 4: expired + Write relative .round-current.md -> exit 0 ---------------
|
|
131
|
+
write_state "$EXPIRED" "$SEED_CKSUM" 900
|
|
132
|
+
printf '%s' "$WRITE_REL" | bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
133
|
+
assert_exit "expired + Write .devlog/.round-current.md -> allowed" 0 $?
|
|
134
|
+
|
|
135
|
+
# --- Scenario 5: expired + Write absolute path ending in /.devlog/.round-current.md -
|
|
136
|
+
write_state "$EXPIRED" "$SEED_CKSUM" 900
|
|
137
|
+
printf '%s' "$WRITE_ABS" | bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
138
|
+
assert_exit "expired + Write absolute /.devlog/.round-current.md -> allowed" 0 $?
|
|
139
|
+
|
|
140
|
+
# --- Scenario 6: expired + Edit relative .round-current.md -> exit 0 -----------------
|
|
141
|
+
write_state "$EXPIRED" "$SEED_CKSUM" 900
|
|
142
|
+
printf '%s' "$EDIT_REL" | bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
143
|
+
assert_exit "expired + Edit .devlog/.round-current.md -> allowed" 0 $?
|
|
144
|
+
|
|
145
|
+
write_state "$EXPIRED" "$SEED_CKSUM" 900
|
|
146
|
+
printf '%s' "$STRREPLACE_REL" | bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
147
|
+
assert_exit "expired + StrReplace .devlog/.round-current.md -> allowed" 0 $?
|
|
148
|
+
|
|
149
|
+
# --- Scenario 6b: allowlist path check against .round-current.md ------------
|
|
150
|
+
ALLOW_DIR="$(mktemp -d)"
|
|
151
|
+
export CLAUDE_PROJECT_DIR="$ALLOW_DIR"
|
|
152
|
+
mkdir -p "$ALLOW_DIR/.devlog"
|
|
153
|
+
touch "$ALLOW_DIR/.devlog/.enabled"
|
|
154
|
+
printf '## Round 1 — 2026-09-17T09:00:00+0800\n\n### User Input\n```text\nX\n```\n\n### Status\nIN_PROGRESS\n' > "$ALLOW_DIR/.devlog/.round-current.md"
|
|
155
|
+
NOW_EPOCH="$(date +%s)"
|
|
156
|
+
OLD_EPOCH=$((NOW_EPOCH - 1000))
|
|
157
|
+
printf '{"last_change_epoch": %s, "last_seen_cksum": "stale", "max_silent_seconds": 600}\n' "$OLD_EPOCH" > "$ALLOW_DIR/.devlog/.segment-state"
|
|
158
|
+
|
|
159
|
+
INPUT='{"tool_name":"Edit","tool_input":{"file_path":".devlog/.round-current.md"}}'
|
|
160
|
+
echo "$INPUT" | bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
161
|
+
RC=$?
|
|
162
|
+
assert_exit "allowlist: Edit on .round-current.md passes through" 0 "$RC"
|
|
163
|
+
rm -rf "$ALLOW_DIR"
|
|
164
|
+
export CLAUDE_PROJECT_DIR="$TMP_ROOT"
|
|
165
|
+
|
|
166
|
+
# --- Scenario 7: expired + Write other file -> exit 2 -----------------------
|
|
167
|
+
write_state "$EXPIRED" "$SEED_CKSUM" 900
|
|
168
|
+
printf '%s' "$WRITE_OTHER" | bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
169
|
+
assert_exit "expired + Write other file -> blocked" 2 $?
|
|
170
|
+
|
|
171
|
+
# --- Scenario 8: hash changed since last_seen -> exit 0 and persist ---------
|
|
172
|
+
write_state "$EXPIRED" "stale-sum" 900
|
|
173
|
+
printf '%s' "$BASH_PAYLOAD" | bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
174
|
+
assert_exit "hash differs from last_seen_cksum -> allowed" 0 $?
|
|
175
|
+
SEEN_AFTER="$(grep -o '"last_seen_cksum"[[:space:]]*:[[:space:]]*"[^"]*"' "$DEVLOG_DIR/.segment-state" | sed 's/.*"last_seen_cksum"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')"
|
|
176
|
+
EPOCH_AFTER="$(grep -o '"last_change_epoch"[[:space:]]*:[[:space:]]*[0-9]\+' "$DEVLOG_DIR/.segment-state" | grep -o '[0-9]\+$')"
|
|
177
|
+
MAX_AFTER="$(grep -o '"max_silent_seconds"[[:space:]]*:[[:space:]]*[0-9]\+' "$DEVLOG_DIR/.segment-state" | grep -o '[0-9]\+$')"
|
|
178
|
+
if [ "$SEEN_AFTER" = "$SEED_CKSUM" ] && [ "$EPOCH_AFTER" -ge "$NOW" ] && [ "$MAX_AFTER" = "900" ]; then
|
|
179
|
+
echo "PASS: hash-change persist updated cksum/epoch and left max_silent_seconds=900"
|
|
180
|
+
else
|
|
181
|
+
echo "FAIL: persist expected cksum=$SEED_CKSUM epoch>=$NOW max=900, got cksum=$SEEN_AFTER epoch=$EPOCH_AFTER max=$MAX_AFTER"
|
|
182
|
+
FAIL=1
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
# --- Scenario 9: malformed state -> fail-open --------------------------------
|
|
186
|
+
echo 'not json' > "$DEVLOG_DIR/.segment-state"
|
|
187
|
+
printf '%s' "$BASH_PAYLOAD" | bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
188
|
+
assert_exit "malformed .segment-state -> allowed" 0 $?
|
|
189
|
+
|
|
190
|
+
# --- Scenario 10: missing .segment-state -> fail-open ------------------------
|
|
191
|
+
rm -f "$DEVLOG_DIR/.segment-state"
|
|
192
|
+
printf '%s' "$BASH_PAYLOAD" | bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
193
|
+
assert_exit "missing .segment-state -> allowed" 0 $?
|
|
194
|
+
|
|
195
|
+
# --- Scenario 11: span open does NOT disable the valve ----------------------
|
|
196
|
+
write_state "$EXPIRED" "$SEED_CKSUM" 900
|
|
197
|
+
cat > "$DEVLOG_DIR/.span-open" <<'SPANEOF'
|
|
198
|
+
{"round": 1, "opened_at": "2026-09-09T00:00:00+08:00", "ticks_since_checkin": 1, "max_silent_ticks": 5}
|
|
199
|
+
SPANEOF
|
|
200
|
+
printf '%s' "$BASH_PAYLOAD" | bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
201
|
+
assert_exit "expired + open under-budget span + Bash -> still blocked" 2 $?
|
|
202
|
+
rm -f "$DEVLOG_DIR/.span-open"
|
|
203
|
+
|
|
204
|
+
# --- Scenario 12: no jq, expired + Bash still blocks ------------------------
|
|
205
|
+
PATH_NO_JQ="$TMP_ROOT/no-jq-path"
|
|
206
|
+
mkdir -p "$PATH_NO_JQ"
|
|
207
|
+
for bin in bash cat printf cksum date grep sed awk mv tr head; do
|
|
208
|
+
bin_path="$(command -v "$bin" 2>/dev/null || true)"
|
|
209
|
+
[ -n "$bin_path" ] && ln -sf "$bin_path" "$PATH_NO_JQ/$bin"
|
|
210
|
+
done
|
|
211
|
+
write_state "$EXPIRED" "$SEED_CKSUM" 900
|
|
212
|
+
printf '%s' "$BASH_PAYLOAD" | PATH="$PATH_NO_JQ" bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
213
|
+
assert_exit "expired + Bash without jq -> blocked" 2 $?
|
|
214
|
+
write_state "$EXPIRED" "$SEED_CKSUM" 900
|
|
215
|
+
printf '%s' "$WRITE_REL" | PATH="$PATH_NO_JQ" bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
216
|
+
assert_exit "expired + Write without jq -> allowed" 0 $?
|
|
217
|
+
WRITE_SPACED='{ "tool_name" : "Write" , "tool_input": {"file_path":".devlog/.round-current.md"} }'
|
|
218
|
+
write_state "$EXPIRED" "$SEED_CKSUM" 900
|
|
219
|
+
printf '%s' "$WRITE_SPACED" | PATH="$PATH_NO_JQ" bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
220
|
+
assert_exit "expired + spaced Write without jq -> allowed" 0 $?
|
|
221
|
+
|
|
222
|
+
# --- Scenario 12b: expired + empty stdin -> fail-open exit 0 ----------------
|
|
223
|
+
write_state "$EXPIRED" "$SEED_CKSUM" 900
|
|
224
|
+
printf '' | bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
225
|
+
assert_exit "expired + empty stdin -> allowed" 0 $?
|
|
226
|
+
|
|
227
|
+
# --- Scenario 13: round-start.sh resets clock and preserves max -------------
|
|
228
|
+
write_state 1 "old-sum" 600
|
|
229
|
+
bash "$SCRIPT_DIR/round-start.sh" < /dev/null
|
|
230
|
+
POST_SKEL="$(cksum < "$DEVLOG_DIR/.round-current.md" | tr -d '\n')"
|
|
231
|
+
RS_EPOCH="$(grep -o '"last_change_epoch"[[:space:]]*:[[:space:]]*[0-9]\+' "$DEVLOG_DIR/.segment-state" | grep -o '[0-9]\+$')"
|
|
232
|
+
RS_SUM="$(grep -o '"last_seen_cksum"[[:space:]]*:[[:space:]]*"[^"]*"' "$DEVLOG_DIR/.segment-state" | sed 's/.*"last_seen_cksum"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')"
|
|
233
|
+
RS_MAX="$(grep -o '"max_silent_seconds"[[:space:]]*:[[:space:]]*[0-9]\+' "$DEVLOG_DIR/.segment-state" | grep -o '[0-9]\+$')"
|
|
234
|
+
if [ "$RS_EPOCH" -ge "$NOW" ] && [ "$RS_SUM" = "$POST_SKEL" ] && [ "$RS_MAX" = "600" ]; then
|
|
235
|
+
echo "PASS: round-start.sh reset epoch/cksum to post-skeleton hash and kept max_silent_seconds=600"
|
|
236
|
+
else
|
|
237
|
+
echo "FAIL: round-start expected epoch>=$NOW cksum=$POST_SKEL max=600, got epoch=$RS_EPOCH cksum=$RS_SUM max=$RS_MAX"
|
|
238
|
+
FAIL=1
|
|
239
|
+
fi
|
|
240
|
+
printf '%s' "$BASH_PAYLOAD" | bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
241
|
+
assert_exit "immediately after round-start.sh, Bash -> allowed" 0 $?
|
|
242
|
+
|
|
243
|
+
# --- Scenario 14: missing last_seen_cksum key -> fail-open ------------------
|
|
244
|
+
printf '{"last_change_epoch": 1, "max_silent_seconds": 900}\n' > "$DEVLOG_DIR/.segment-state"
|
|
245
|
+
printf '%s' "$BASH_PAYLOAD" | bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
246
|
+
assert_exit "segment-state missing last_seen_cksum key -> allowed" 0 $?
|
|
247
|
+
|
|
248
|
+
# --- Scenario 15: identity match skips cksum but still blocks when expired --
|
|
249
|
+
# round-start may have rewritten the file; re-seed so cksum matches identity.
|
|
250
|
+
printf 'seed\n' > "$DEVLOG_DIR/.round-current.md"
|
|
251
|
+
SEED_CKSUM="$(cksum < "$DEVLOG_DIR/.round-current.md" | tr -d '\n')"
|
|
252
|
+
|
|
253
|
+
file_identity() {
|
|
254
|
+
local f="$1" mt sz
|
|
255
|
+
if mt="$(stat -f '%m' "$f" 2>/dev/null)" && sz="$(stat -f '%z' "$f" 2>/dev/null)"; then
|
|
256
|
+
printf '%s %s\n' "$mt" "$sz"
|
|
257
|
+
return 0
|
|
258
|
+
fi
|
|
259
|
+
if mt="$(stat -c '%Y' "$f" 2>/dev/null)" && sz="$(stat -c '%s' "$f" 2>/dev/null)"; then
|
|
260
|
+
printf '%s %s\n' "$mt" "$sz"
|
|
261
|
+
return 0
|
|
262
|
+
fi
|
|
263
|
+
return 1
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
ID_LINE="$(file_identity "$DEVLOG_DIR/.round-current.md")"
|
|
267
|
+
ID_MT="${ID_LINE%% *}"
|
|
268
|
+
ID_SZ="${ID_LINE#* }"
|
|
269
|
+
printf '{"last_change_epoch": %s, "last_seen_cksum": "%s", "last_seen_mtime": "%s", "last_seen_size": "%s", "max_silent_seconds": 900, "session_id": "aaa"}\n' \
|
|
270
|
+
"$EXPIRED" "$SEED_CKSUM" "$ID_MT" "$ID_SZ" > "$DEVLOG_DIR/.segment-state"
|
|
271
|
+
|
|
272
|
+
SPY="$TMP_ROOT/spybin"
|
|
273
|
+
mkdir -p "$SPY"
|
|
274
|
+
REAL_CKSUM="$(command -v cksum)"
|
|
275
|
+
rm -f "$TMP_ROOT/cksum_count"
|
|
276
|
+
cat > "$SPY/cksum" <<EOF
|
|
277
|
+
#!/usr/bin/env bash
|
|
278
|
+
echo 1 >> "$TMP_ROOT/cksum_count"
|
|
279
|
+
exec "$REAL_CKSUM" "\$@"
|
|
280
|
+
EOF
|
|
281
|
+
chmod +x "$SPY/cksum"
|
|
282
|
+
printf '%s' "$BASH_PAYLOAD" | PATH="$SPY:$PATH" bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
283
|
+
assert_exit "identity match + expired -> still blocked" 2 $?
|
|
284
|
+
if [ ! -f "$TMP_ROOT/cksum_count" ]; then
|
|
285
|
+
echo "PASS: identity match skipped cksum"
|
|
286
|
+
else
|
|
287
|
+
echo "FAIL: cksum was invoked despite matching identity"
|
|
288
|
+
FAIL=1
|
|
289
|
+
fi
|
|
290
|
+
|
|
291
|
+
# --- Scenario 16: identity mismatch forces cksum and clears silence ---------
|
|
292
|
+
printf '{"last_change_epoch": %s, "last_seen_cksum": "%s", "last_seen_mtime": "%s", "last_seen_size": "%s", "max_silent_seconds": 900, "session_id": "aaa"}\n' \
|
|
293
|
+
"$EXPIRED" "$SEED_CKSUM" "$ID_MT" "$ID_SZ" > "$DEVLOG_DIR/.segment-state"
|
|
294
|
+
printf 'x' >> "$DEVLOG_DIR/.round-current.md"
|
|
295
|
+
NEW_CKSUM="$(cksum < "$DEVLOG_DIR/.round-current.md" | tr -d '\n')"
|
|
296
|
+
printf '%s' "$BASH_PAYLOAD" | bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
297
|
+
assert_exit "mtime/size change -> allowed (re-hash)" 0 $?
|
|
298
|
+
SEEN_MT="$(grep -o '"last_seen_mtime"[[:space:]]*:[[:space:]]*"[^"]*"' "$DEVLOG_DIR/.segment-state" | sed 's/.*"last_seen_mtime"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')"
|
|
299
|
+
SEEN_SZ="$(grep -o '"last_seen_size"[[:space:]]*:[[:space:]]*"[^"]*"' "$DEVLOG_DIR/.segment-state" | sed 's/.*"last_seen_size"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')"
|
|
300
|
+
SEEN_SUM="$(grep -o '"last_seen_cksum"[[:space:]]*:[[:space:]]*"[^"]*"' "$DEVLOG_DIR/.segment-state" | sed 's/.*"last_seen_cksum"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')"
|
|
301
|
+
ID_LINE2="$(file_identity "$DEVLOG_DIR/.round-current.md")"
|
|
302
|
+
ID_MT2="${ID_LINE2%% *}"
|
|
303
|
+
ID_SZ2="${ID_LINE2#* }"
|
|
304
|
+
if [ "$SEEN_MT" = "$ID_MT2" ] && [ "$SEEN_SZ" = "$ID_SZ2" ] && [ "$SEEN_SUM" = "$NEW_CKSUM" ]; then
|
|
305
|
+
echo "PASS: identity mismatch refreshed cksum + identity fields"
|
|
306
|
+
else
|
|
307
|
+
echo "FAIL: expected mtime=$ID_MT2 size=$ID_SZ2 cksum=$NEW_CKSUM, got mtime=$SEEN_MT size=$SEEN_SZ cksum=$SEEN_SUM"
|
|
308
|
+
FAIL=1
|
|
309
|
+
fi
|
|
310
|
+
|
|
311
|
+
# --- Scenario 17: missing identity fields fall through to cksum -------------
|
|
312
|
+
printf 'seed\n' > "$DEVLOG_DIR/.round-current.md"
|
|
313
|
+
SEED_CKSUM="$(cksum < "$DEVLOG_DIR/.round-current.md" | tr -d '\n')"
|
|
314
|
+
printf '{"last_change_epoch": %s, "last_seen_cksum": "%s", "max_silent_seconds": 900, "session_id": "aaa"}\n' \
|
|
315
|
+
"$EXPIRED" "$SEED_CKSUM" > "$DEVLOG_DIR/.segment-state"
|
|
316
|
+
printf '%s' "$BASH_PAYLOAD" | bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
317
|
+
assert_exit "missing identity fields + expired -> still blocked via cksum" 2 $?
|
|
318
|
+
|
|
319
|
+
printf '{"last_change_epoch": %s, "last_seen_cksum": "stale-sum", "max_silent_seconds": 900, "session_id": "aaa"}\n' \
|
|
320
|
+
"$EXPIRED" > "$DEVLOG_DIR/.segment-state"
|
|
321
|
+
printf '%s' "$BASH_PAYLOAD" | bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
322
|
+
assert_exit "missing identity + hash change -> allowed" 0 $?
|
|
323
|
+
|
|
324
|
+
# --- workspace mismatch marker blocks non-devlog tools ----------------------
|
|
325
|
+
cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
|
|
326
|
+
## Round 1 — 2026-09-11T00:00:00+08:00
|
|
327
|
+
|
|
328
|
+
### User Input
|
|
329
|
+
```text
|
|
330
|
+
keep going
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Status
|
|
334
|
+
IN_PROGRESS
|
|
335
|
+
EOF
|
|
336
|
+
printf '%s\n' 'main @ deadbeef,工作樹乾淨' > "$DEVLOG_DIR/.workspace-mismatch"
|
|
337
|
+
write_state "$NOW" "$(cksum < "$DEVLOG_DIR/.round-current.md" | tr -d '\n')" 900
|
|
338
|
+
printf '%s' '{"tool_name":"Bash","tool_input":{},"session_id":"aaa"}' \
|
|
339
|
+
| bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
340
|
+
assert_exit "mismatch marker + Bash -> blocked" 2 $?
|
|
341
|
+
|
|
342
|
+
printf '%s' '{"tool_name":"Read","tool_input":{"file_path":".devlog/.round-current.md"},"session_id":"aaa"}' \
|
|
343
|
+
| bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
344
|
+
assert_exit "mismatch marker + Read devlog -> allowed" 0 $?
|
|
345
|
+
|
|
346
|
+
ERR="$(printf '%s' '{"tool_name":"Bash","tool_input":{},"session_id":"aaa"}' \
|
|
347
|
+
| bash "$SCRIPT_DIR/segment-watch.sh" 2>&1 >/dev/null || true)"
|
|
348
|
+
case "$ERR" in
|
|
349
|
+
*實際*deadbeef*) echo "PASS: mismatch stderr includes live snapshot" ;;
|
|
350
|
+
*) echo "FAIL: mismatch stderr [$ERR]"; FAIL=1 ;;
|
|
351
|
+
esac
|
|
352
|
+
|
|
353
|
+
# last Round contains the live snapshot -> marker cleared, Bash allowed
|
|
354
|
+
# (fresh clock so the silence valve does not also fire)
|
|
355
|
+
cat >> "$DEVLOG_DIR/.round-current.md" <<'EOF'
|
|
356
|
+
### 段落 1
|
|
357
|
+
宣稱 vs 實際
|
|
358
|
+
main @ deadbeef,工作樹乾淨
|
|
359
|
+
EOF
|
|
360
|
+
write_state "$NOW" "$(cksum < "$DEVLOG_DIR/.round-current.md" | tr -d '\n')" 900
|
|
361
|
+
printf '%s' '{"tool_name":"Bash","tool_input":{},"session_id":"aaa"}' \
|
|
362
|
+
| bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
363
|
+
assert_exit "mismatch recorded in 段落 -> Bash allowed" 0 $?
|
|
364
|
+
[ ! -f "$DEVLOG_DIR/.workspace-mismatch" ] && echo "PASS: marker removed after 段落" || { echo "FAIL: marker remains"; FAIL=1; }
|
|
365
|
+
|
|
366
|
+
# two-line dirty snapshot (branch @ hash + 未提交) recorded in 段落 -> marker cleared
|
|
367
|
+
cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
|
|
368
|
+
## Round 1 — 2026-09-11T00:00:00+08:00
|
|
369
|
+
|
|
370
|
+
### User Input
|
|
371
|
+
```text
|
|
372
|
+
keep going
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### 段落 1
|
|
376
|
+
feat/foo @ abc1234
|
|
377
|
+
未提交:a.txt
|
|
378
|
+
|
|
379
|
+
### Status
|
|
380
|
+
IN_PROGRESS
|
|
381
|
+
EOF
|
|
382
|
+
cat > "$DEVLOG_DIR/.workspace-mismatch" <<'EOF'
|
|
383
|
+
feat/foo @ abc1234
|
|
384
|
+
未提交:a.txt
|
|
385
|
+
EOF
|
|
386
|
+
write_state "$NOW" "$(cksum < "$DEVLOG_DIR/.round-current.md" | tr -d '\n')" 900
|
|
387
|
+
printf '%s' '{"tool_name":"Bash","tool_input":{},"session_id":"aaa"}' \
|
|
388
|
+
| bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
389
|
+
assert_exit "two-line dirty snapshot in 段落 -> Bash allowed" 0 $?
|
|
390
|
+
[ ! -f "$DEVLOG_DIR/.workspace-mismatch" ] && echo "PASS: two-line marker removed after 段落" || { echo "FAIL: two-line marker remains"; FAIL=1; }
|
|
391
|
+
|
|
392
|
+
# Same string outside a 段落 must not clear the marker
|
|
393
|
+
cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
|
|
394
|
+
## Round 1 — 2026-09-11T00:00:00+08:00
|
|
395
|
+
|
|
396
|
+
### User Input
|
|
397
|
+
main @ deadbeef,工作樹乾淨
|
|
398
|
+
|
|
399
|
+
### Status
|
|
400
|
+
IN_PROGRESS
|
|
401
|
+
EOF
|
|
402
|
+
printf '%s\n' 'main @ deadbeef,工作樹乾淨' > "$DEVLOG_DIR/.workspace-mismatch"
|
|
403
|
+
write_state "$NOW" "$(cksum < "$DEVLOG_DIR/.round-current.md" | tr -d '\n')" 900
|
|
404
|
+
printf '%s' '{"tool_name":"Bash","tool_input":{},"session_id":"aaa"}' \
|
|
405
|
+
| bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
406
|
+
assert_exit "snapshot in User Input only -> still blocked" 2 $?
|
|
407
|
+
|
|
408
|
+
# --- read-only git inspection commands stay allowed while blocked ----------
|
|
409
|
+
# The whole point of the mismatch block is to force a look at live git
|
|
410
|
+
# before acting — but Bash itself is blocked, so there was no way to run
|
|
411
|
+
# `git status`/`git diff` to see what actually changed. A small allowlist of
|
|
412
|
+
# unconditionally non-mutating git subcommands stays open; anything with
|
|
413
|
+
# shell metacharacters (chaining/redirection/substitution) or a mutating
|
|
414
|
+
# subcommand (commit, branch -d, etc.) stays blocked.
|
|
415
|
+
cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
|
|
416
|
+
## Round 1 — 2026-09-11T00:00:00+08:00
|
|
417
|
+
|
|
418
|
+
### User Input
|
|
419
|
+
```text
|
|
420
|
+
keep going
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Status
|
|
424
|
+
IN_PROGRESS
|
|
425
|
+
EOF
|
|
426
|
+
printf '%s\n' 'main @ deadbeef,工作樹乾淨' > "$DEVLOG_DIR/.workspace-mismatch"
|
|
427
|
+
write_state "$NOW" "$(cksum < "$DEVLOG_DIR/.round-current.md" | tr -d '\n')" 900
|
|
428
|
+
printf '%s' '{"tool_name":"Bash","tool_input":{"command":"git status --short"},"session_id":"aaa"}' \
|
|
429
|
+
| bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
430
|
+
assert_exit "mismatch marker + read-only git status -> allowed" 0 $?
|
|
431
|
+
[ -f "$DEVLOG_DIR/.workspace-mismatch" ] && echo "PASS: read-only git inspection does not clear the marker" || { echo "FAIL: marker cleared by mere inspection"; FAIL=1; }
|
|
432
|
+
|
|
433
|
+
printf '%s' '{"tool_name":"Bash","tool_input":{"command":"git diff"},"session_id":"aaa"}' \
|
|
434
|
+
| bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
435
|
+
assert_exit "mismatch marker + read-only git diff -> allowed" 0 $?
|
|
436
|
+
|
|
437
|
+
printf '%s' '{"tool_name":"Bash","tool_input":{"command":"git status; rm -rf /tmp/whatever"},"session_id":"aaa"}' \
|
|
438
|
+
| bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
439
|
+
assert_exit "mismatch marker + chained command after git status -> still blocked" 2 $?
|
|
440
|
+
|
|
441
|
+
printf '%s' '{"tool_name":"Bash","tool_input":{"command":"git commit -am oops"},"session_id":"aaa"}' \
|
|
442
|
+
| bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
443
|
+
assert_exit "mismatch marker + git commit -> still blocked" 2 $?
|
|
444
|
+
|
|
445
|
+
printf '%s' '{"tool_name":"Bash","tool_input":{"command":"git branch -d main"},"session_id":"aaa"}' \
|
|
446
|
+
| bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
447
|
+
assert_exit "mismatch marker + git branch -d (mutating, not allowlisted) -> still blocked" 2 $?
|
|
448
|
+
|
|
449
|
+
# marker works even without .segment-state (today that file missing exits 0)
|
|
450
|
+
rm -f "$DEVLOG_DIR/.segment-state"
|
|
451
|
+
printf '%s\n' 'main @ deadbeef,工作樹乾淨' > "$DEVLOG_DIR/.workspace-mismatch"
|
|
452
|
+
cat > "$DEVLOG_DIR/.round-current.md" <<'EOF'
|
|
453
|
+
## Round 1 — 2026-09-11T00:00:00+08:00
|
|
454
|
+
|
|
455
|
+
### Status
|
|
456
|
+
IN_PROGRESS
|
|
457
|
+
EOF
|
|
458
|
+
printf '%s' '{"tool_name":"Bash","tool_input":{},"session_id":"aaa"}' \
|
|
459
|
+
| bash "$SCRIPT_DIR/segment-watch.sh" >/dev/null 2>&1
|
|
460
|
+
assert_exit "mismatch marker without segment-state + Bash -> blocked" 2 $?
|
|
461
|
+
|
|
462
|
+
if [ "$FAIL" -eq 0 ]; then
|
|
463
|
+
echo "All checks passed."
|
|
464
|
+
exit 0
|
|
465
|
+
else
|
|
466
|
+
echo "Some checks FAILED."
|
|
467
|
+
exit 1
|
|
468
|
+
fi
|