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,28 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
_src="${BASH_SOURCE[0]}"
|
|
4
|
+
SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
|
|
5
|
+
PLUGIN_SCRIPTS="$(cd "$SCRIPT_DIR/../../hooks/scripts" && pwd)"
|
|
6
|
+
INPUT="$(cat 2>/dev/null || true)"
|
|
7
|
+
ROOT="$(printf '%s' "$INPUT" | bash "$SCRIPT_DIR/project-dir.sh")"
|
|
8
|
+
export CLAUDE_PROJECT_DIR="$ROOT"
|
|
9
|
+
PROMPT=""
|
|
10
|
+
SESSION_ID=""
|
|
11
|
+
if command -v jq >/dev/null 2>&1; then
|
|
12
|
+
PROMPT="$(printf '%s' "$INPUT" | jq -r '.prompt // empty' 2>/dev/null || true)"
|
|
13
|
+
SESSION_ID="$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null || true)"
|
|
14
|
+
PAYLOAD="$(jq -n --arg prompt "$PROMPT" --arg session_id "$SESSION_ID" '{prompt:$prompt,session_id:$session_id}')"
|
|
15
|
+
else
|
|
16
|
+
PROMPT="$(printf '%s' "$INPUT" | grep -o '"prompt"[[:space:]]*:[[:space:]]*"[^"]*"' 2>/dev/null | head -1 | sed 's/.*"prompt"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || true)"
|
|
17
|
+
SESSION_ID="$(printf '%s' "$INPUT" | grep -o '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' 2>/dev/null | head -1 | sed 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || true)"
|
|
18
|
+
# Escape only what we embed; prompts with quotes remain a known no-jq limitation.
|
|
19
|
+
PAYLOAD="$(printf '{"prompt":"%s","session_id":"%s"}' "$PROMPT" "$SESSION_ID")"
|
|
20
|
+
fi
|
|
21
|
+
NOTE="$(printf '%s' "$PAYLOAD" | bash "$PLUGIN_SCRIPTS/round-start.sh" 2>/dev/null || true)"
|
|
22
|
+
if command -v jq >/dev/null 2>&1; then
|
|
23
|
+
jq -cn --arg n "$NOTE" '{continue:true, additional_context:$n}'
|
|
24
|
+
else
|
|
25
|
+
esc="$(printf '%s' "$NOTE" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | awk '{printf "%s\\n", $0}' | sed '$ s/\\n$//')"
|
|
26
|
+
printf '{"continue":true,"additional_context":"%s"}\n' "$esc"
|
|
27
|
+
fi
|
|
28
|
+
exit 0
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
_src="${BASH_SOURCE[0]}"
|
|
4
|
+
SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
|
|
5
|
+
PLUGIN_SCRIPTS="$(cd "$SCRIPT_DIR/../../hooks/scripts" && pwd)"
|
|
6
|
+
INPUT="$(cat 2>/dev/null || true)"
|
|
7
|
+
ROOT="$(printf '%s' "$INPUT" | bash "$SCRIPT_DIR/project-dir.sh")"
|
|
8
|
+
export CLAUDE_PROJECT_DIR="$ROOT"
|
|
9
|
+
if command -v jq >/dev/null 2>&1; then
|
|
10
|
+
IS_INTERRUPT="$(printf '%s' "$INPUT" | jq -r '.is_interrupt // .tool_result.is_interrupt // false' 2>/dev/null || echo false)"
|
|
11
|
+
else
|
|
12
|
+
case "$INPUT" in *'"is_interrupt":true'*|*'"is_interrupt": true'*) IS_INTERRUPT=true ;; *) IS_INTERRUPT=false ;; esac
|
|
13
|
+
fi
|
|
14
|
+
printf '{"is_interrupt":%s}' "$IS_INTERRUPT" | bash "$PLUGIN_SCRIPTS/on-tool-failure.sh" >/dev/null 2>&1 || true
|
|
15
|
+
echo '{}'
|
|
16
|
+
exit 0
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
INPUT="$(cat 2>/dev/null || true)"
|
|
4
|
+
ROOT=""
|
|
5
|
+
if command -v jq >/dev/null 2>&1; then
|
|
6
|
+
ROOT="$(printf '%s' "$INPUT" | jq -r '.workspace_roots[0] // empty' 2>/dev/null || true)"
|
|
7
|
+
else
|
|
8
|
+
ROOT="$(printf '%s' "$INPUT" | grep -o '"workspace_roots"[[:space:]]*:[[:space:]]*\["[^"]*"' 2>/dev/null | head -1 | sed 's/.*"workspace_roots"[[:space:]]*:[[:space:]]*\["\([^"]*\)".*/\1/' || true)"
|
|
9
|
+
fi
|
|
10
|
+
printf '%s\n' "${ROOT:-.}"
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
4
|
+
TMP="$(mktemp -d)"
|
|
5
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
6
|
+
FAIL=0
|
|
7
|
+
assert_single_json() {
|
|
8
|
+
local desc="$1" json="$2" count
|
|
9
|
+
if command -v jq >/dev/null 2>&1; then
|
|
10
|
+
count="$(printf '%s' "$json" | jq -s 'length' 2>/dev/null || echo '')"
|
|
11
|
+
else
|
|
12
|
+
# No jq: count top-level {...} closures ourselves, quote/escape-aware so
|
|
13
|
+
# a brace inside a string value doesn't throw the depth count off.
|
|
14
|
+
count="$(printf '%s' "$json" | awk '
|
|
15
|
+
{
|
|
16
|
+
line = $0
|
|
17
|
+
for (i = 1; i <= length(line); i++) {
|
|
18
|
+
c = substr(line, i, 1)
|
|
19
|
+
if (esc) { esc = 0; continue }
|
|
20
|
+
if (c == "\\" && in_str) { esc = 1; continue }
|
|
21
|
+
if (c == "\"") { in_str = !in_str; continue }
|
|
22
|
+
if (in_str) continue
|
|
23
|
+
if (c == "{") depth++
|
|
24
|
+
else if (c == "}") { depth--; if (depth == 0) n_obj++ }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
END { print n_obj + 0 }
|
|
28
|
+
' 2>/dev/null || echo '')"
|
|
29
|
+
fi
|
|
30
|
+
if [ "$count" = "1" ]; then
|
|
31
|
+
echo "PASS: $desc"
|
|
32
|
+
else
|
|
33
|
+
echo "FAIL: $desc (expected 1 top-level JSON value, got [$count]) [$json]"; FAIL=1
|
|
34
|
+
fi
|
|
35
|
+
}
|
|
36
|
+
mkdir -p "$TMP/project/.devlog"
|
|
37
|
+
cat > "$TMP/project/.devlog/devlog.md" <<'EOF'
|
|
38
|
+
## Round 1 — now
|
|
39
|
+
|
|
40
|
+
### Summary
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
### Handoff
|
|
44
|
+
#### 現況
|
|
45
|
+
done
|
|
46
|
+
|
|
47
|
+
### Status
|
|
48
|
+
DONE
|
|
49
|
+
EOF
|
|
50
|
+
OUT="$(printf '{"workspace_roots":["%s"]}' "$TMP/project" | bash "$SCRIPT_DIR/on-session-start.sh")"
|
|
51
|
+
case "$OUT" in *'"additional_context"'*"Round 1"*) echo "PASS: sessionStart context" ;; *) echo "FAIL: sessionStart [$OUT]"; FAIL=1 ;; esac
|
|
52
|
+
assert_single_json "sessionStart single json" "$OUT"
|
|
53
|
+
EMPTY="$(printf '{}' | bash "$SCRIPT_DIR/on-session-start.sh")"
|
|
54
|
+
case "$EMPTY" in \{*\}) echo "PASS: empty workspace valid json" ;; *) echo "FAIL: empty json [$EMPTY]"; FAIL=1 ;; esac
|
|
55
|
+
assert_single_json "empty workspace single json" "$EMPTY"
|
|
56
|
+
|
|
57
|
+
# submit prompt
|
|
58
|
+
SUBMIT="$TMP/submit"
|
|
59
|
+
mkdir -p "$SUBMIT/.devlog"
|
|
60
|
+
touch "$SUBMIT/.devlog/.enabled"
|
|
61
|
+
OUT="$(printf '{"workspace_roots":["%s"],"prompt":"hello","session_id":"cursor-1"}' "$SUBMIT" | bash "$SCRIPT_DIR/on-submit-prompt.sh")"
|
|
62
|
+
case "$OUT" in *'"continue":true'*) echo "PASS: submit continues" ;; *) echo "FAIL: submit [$OUT]"; FAIL=1 ;; esac
|
|
63
|
+
assert_single_json "submit single json" "$OUT"
|
|
64
|
+
grep -q 'hello' "$SUBMIT/.devlog/.round-current.md" && echo "PASS: submit writes round" || { echo "FAIL: submit round"; FAIL=1; }
|
|
65
|
+
ROUNDS="$(grep -c '^## Round ' "$SUBMIT/.devlog/.round-current.md" || true)"
|
|
66
|
+
if [ "$ROUNDS" -eq 1 ]; then echo "PASS: submit writes one round"; else echo "FAIL: submit round count [$ROUNDS]"; FAIL=1; fi
|
|
67
|
+
|
|
68
|
+
# submit prompt with stale 工作區 forwards additional_context
|
|
69
|
+
MIS="$TMP/mismatch-submit"
|
|
70
|
+
mkdir -p "$MIS/.devlog"
|
|
71
|
+
touch "$MIS/.devlog/.enabled"
|
|
72
|
+
git -C "$MIS" init -q -b main
|
|
73
|
+
git -C "$MIS" config user.email test@example.com
|
|
74
|
+
git -C "$MIS" config user.name test
|
|
75
|
+
echo hello > "$MIS/a.txt"
|
|
76
|
+
git -C "$MIS" add a.txt
|
|
77
|
+
git -C "$MIS" commit -q -m init
|
|
78
|
+
cat > "$MIS/.devlog/devlog.md" <<'EOF'
|
|
79
|
+
## Round 1 — 2026-09-11T00:00:00+08:00
|
|
80
|
+
|
|
81
|
+
### Handoff
|
|
82
|
+
#### 工作區
|
|
83
|
+
main @ deadbeef,工作樹乾淨
|
|
84
|
+
#### 現況
|
|
85
|
+
going
|
|
86
|
+
#### 下一步
|
|
87
|
+
do x
|
|
88
|
+
|
|
89
|
+
### Status
|
|
90
|
+
IN_PROGRESS
|
|
91
|
+
EOF
|
|
92
|
+
OUT="$(printf '{"workspace_roots":["%s"],"prompt":"keep going","session_id":"cursor-mismatch"}' "$MIS" | bash "$SCRIPT_DIR/on-submit-prompt.sh")"
|
|
93
|
+
case "$OUT" in *'"continue":true'*) echo "PASS: mismatch submit continues" ;; *) echo "FAIL: mismatch submit [$OUT]"; FAIL=1 ;; esac
|
|
94
|
+
case "$OUT" in *'"additional_context"'*工作區*) echo "PASS: mismatch submit additional_context" ;; *) echo "FAIL: mismatch context [$OUT]"; FAIL=1 ;; esac
|
|
95
|
+
assert_single_json "mismatch submit single json" "$OUT"
|
|
96
|
+
[ -f "$MIS/.devlog/.workspace-mismatch" ] && echo "PASS: mismatch submit writes marker" || { echo "FAIL: no cursor marker"; FAIL=1; }
|
|
97
|
+
|
|
98
|
+
# preToolUse allowlists writes to the devlog and denies expired other tools
|
|
99
|
+
NOW="$(date +%s)"
|
|
100
|
+
OLD=$((NOW - 1000))
|
|
101
|
+
SUM="$(cksum < "$SUBMIT/.devlog/.round-current.md" | tr -d '\n')"
|
|
102
|
+
printf '{"last_change_epoch": %s, "last_seen_cksum": "%s", "max_silent_seconds": 900, "session_id": "cursor-1"}\n' "$OLD" "$SUM" > "$SUBMIT/.devlog/.segment-state"
|
|
103
|
+
OUT="$(printf '{"workspace_roots":["%s"],"tool_name":"Write","tool_input":{"file_path":"%s/.devlog/.round-current.md"},"session_id":"cursor-1"}' "$SUBMIT" "$SUBMIT" | bash "$SCRIPT_DIR/on-pre-tool.sh")"
|
|
104
|
+
case "$OUT" in *'"permission":"deny"'*) echo "FAIL: devlog write denied"; FAIL=1 ;; *) echo "PASS: devlog write allowed" ;; esac
|
|
105
|
+
assert_single_json "preTool write single json" "$OUT"
|
|
106
|
+
OUT="$(printf '{"workspace_roots":["%s"],"tool_name":"StrReplace","tool_input":{"file_path":"%s/.devlog/.round-current.md"},"session_id":"cursor-1"}' "$SUBMIT" "$SUBMIT" | bash "$SCRIPT_DIR/on-pre-tool.sh")"
|
|
107
|
+
case "$OUT" in *'"permission":"deny"'*) echo "FAIL: StrReplace devlog denied"; FAIL=1 ;; *) echo "PASS: StrReplace devlog allowed" ;; esac
|
|
108
|
+
assert_single_json "preTool StrReplace single json" "$OUT"
|
|
109
|
+
OUT="$(printf '{"workspace_roots":["%s"],"tool_name":"Bash","tool_input":{},"session_id":"cursor-1"}' "$SUBMIT" | bash "$SCRIPT_DIR/on-pre-tool.sh")"
|
|
110
|
+
case "$OUT" in *permission*deny*"### 段落"*) echo "PASS: expired tool denied" ;; *) echo "FAIL: preTool [$OUT]"; FAIL=1 ;; esac
|
|
111
|
+
assert_single_json "preTool deny single json" "$OUT"
|
|
112
|
+
|
|
113
|
+
# stop: incomplete round asks Cursor for one follow-up; loop_count prevents retry
|
|
114
|
+
OUT="$(printf '{"workspace_roots":["%s"],"status":"completed","loop_count":0}' "$SUBMIT" | bash "$SCRIPT_DIR/on-stop.sh")"
|
|
115
|
+
case "$OUT" in *'"followup_message"'*"Summary"*) echo "PASS: stop followup" ;; *) echo "FAIL: stop [$OUT]"; FAIL=1 ;; esac
|
|
116
|
+
assert_single_json "stop followup single json" "$OUT"
|
|
117
|
+
OUT="$(printf '{"workspace_roots":["%s"],"status":"completed","loop_count":1}' "$SUBMIT" | bash "$SCRIPT_DIR/on-stop.sh")"
|
|
118
|
+
case "$OUT" in *followup_message*) echo "FAIL: loop guard followup"; FAIL=1 ;; *) echo "PASS: stop loop guard" ;; esac
|
|
119
|
+
assert_single_json "stop loop guard single json" "$OUT"
|
|
120
|
+
OUT="$(printf '{"workspace_roots":["%s"],"status":"running","loop_count":0}' "$SUBMIT" | bash "$SCRIPT_DIR/on-stop.sh")"
|
|
121
|
+
case "$OUT" in *followup_message*) echo "FAIL: non-completed stop enforced [$OUT]"; FAIL=1 ;; *) echo "PASS: non-completed stop skipped" ;; esac
|
|
122
|
+
assert_single_json "non-completed stop single json" "$OUT"
|
|
123
|
+
|
|
124
|
+
# aborted stop and session end close the open round
|
|
125
|
+
printf '%s\n' '{"round": 1, "opened_at": "now"}' > "$SUBMIT/.devlog/.round-open"
|
|
126
|
+
OUT="$(printf '{"workspace_roots":["%s"],"status":"aborted"}' "$SUBMIT" | bash "$SCRIPT_DIR/on-stop.sh")"
|
|
127
|
+
grep -q 'INTERRUPTED' "$SUBMIT/.devlog/devlog.md" && echo "PASS: aborted closes round" || { echo "FAIL: aborted"; FAIL=1; }
|
|
128
|
+
assert_single_json "aborted stop single json" "$OUT"
|
|
129
|
+
|
|
130
|
+
# sessionEnd delegates its reason to the existing close helper
|
|
131
|
+
printf '\n## Round 2 — now\n\n### Status\nIN_PROGRESS\n' > "$SUBMIT/.devlog/.round-current.md"
|
|
132
|
+
printf '%s\n' '{"round": 2, "opened_at": "now"}' > "$SUBMIT/.devlog/.round-open"
|
|
133
|
+
cksum < "$SUBMIT/.devlog/.round-current.md" > "$SUBMIT/.devlog/.turn-start"
|
|
134
|
+
OUT="$(printf '{"workspace_roots":["%s"],"reason":"windowClosed"}' "$SUBMIT" | bash "$SCRIPT_DIR/on-session-end.sh")"
|
|
135
|
+
grep -q 'SessionEnd:windowClosed' "$SUBMIT/.devlog/devlog.md" && echo "PASS: session end reason" || { echo "FAIL: session end"; FAIL=1; }
|
|
136
|
+
assert_single_json "sessionEnd single json" "$OUT"
|
|
137
|
+
|
|
138
|
+
# project-dir without jq still reads workspace_roots[0]
|
|
139
|
+
PATH_NO_JQ="$TMP/no-jq-path"
|
|
140
|
+
mkdir -p "$PATH_NO_JQ"
|
|
141
|
+
for bin in bash cat printf grep sed head mktemp rm date cksum awk tr mkdir; do
|
|
142
|
+
bin_path="$(command -v "$bin" 2>/dev/null || true)"
|
|
143
|
+
[ -n "$bin_path" ] && ln -sf "$bin_path" "$PATH_NO_JQ/$bin"
|
|
144
|
+
done
|
|
145
|
+
ROOT="$(printf '{"workspace_roots":["%s"]}' "$SUBMIT" | PATH="$PATH_NO_JQ" bash "$SCRIPT_DIR/project-dir.sh")"
|
|
146
|
+
if [ "$ROOT" = "$SUBMIT" ]; then echo "PASS: project-dir without jq"; else echo "FAIL: project-dir without jq [$ROOT]"; FAIL=1; fi
|
|
147
|
+
|
|
148
|
+
# tool interruption marker
|
|
149
|
+
rm -f "$SUBMIT/.devlog/.interrupted"
|
|
150
|
+
OUT="$(printf '{"workspace_roots":["%s"],"is_interrupt":true}' "$SUBMIT" | bash "$SCRIPT_DIR/on-tool-failure.sh")"
|
|
151
|
+
[ -f "$SUBMIT/.devlog/.interrupted" ] && echo "PASS: tool interruption marked" || { echo "FAIL: interrupt marker"; FAIL=1; }
|
|
152
|
+
assert_single_json "toolFailure single json" "$OUT"
|
|
153
|
+
|
|
154
|
+
# --- no-jq session_id must reach segment-state via round-start ------------
|
|
155
|
+
NOJQ_HOME="$TMP/nojq_home"
|
|
156
|
+
mkdir -p "$NOJQ_HOME/.devlog" "$TMP/nojq_path"
|
|
157
|
+
touch "$NOJQ_HOME/.devlog/.enabled"
|
|
158
|
+
printf '%s\n' '{"last_change_epoch": 0, "last_seen_cksum": "", "max_silent_seconds": 600, "session_id": ""}' \
|
|
159
|
+
> "$NOJQ_HOME/.devlog/.segment-state"
|
|
160
|
+
CLEAN_PATH="$TMP/nojq_path"
|
|
161
|
+
# Resolve real binaries (zsh `command -v` may return aliases / builtins).
|
|
162
|
+
for cmd in bash sh cksum date grep sed cat mkdir tr mktemp rm head awk touch \
|
|
163
|
+
pwd dirname uname ln printf sort cut wc env sleep; do
|
|
164
|
+
src=""
|
|
165
|
+
for cand in "/bin/$cmd" "/usr/bin/$cmd"; do
|
|
166
|
+
if [ -x "$cand" ]; then src="$cand"; break; fi
|
|
167
|
+
done
|
|
168
|
+
[ -n "$src" ] && ln -sf "$src" "$CLEAN_PATH/$cmd"
|
|
169
|
+
done
|
|
170
|
+
# Explicitly do NOT link jq.
|
|
171
|
+
OUT="$(printf '{"workspace_roots":["%s"],"prompt":"nojq-hi","session_id":"cursor-nojq"}' "$NOJQ_HOME" \
|
|
172
|
+
| env PATH="$CLEAN_PATH" bash "$SCRIPT_DIR/on-submit-prompt.sh")"
|
|
173
|
+
case "$OUT" in *'"continue":true'*) echo "PASS: nojq submit continues" ;; *) echo "FAIL: nojq submit [$OUT]"; FAIL=1 ;; esac
|
|
174
|
+
SEG_SID="$(grep -o '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' "$NOJQ_HOME/.devlog/.segment-state" 2>/dev/null | head -1 | sed 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || true)"
|
|
175
|
+
if [ "$SEG_SID" = "cursor-nojq" ]; then
|
|
176
|
+
echo "PASS: nojq session_id stored in segment-state"
|
|
177
|
+
else
|
|
178
|
+
echo "FAIL: nojq session_id missing (got [$SEG_SID])"; FAIL=1
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
# --- no-jq PreToolUse deny must emit a single-line JSON object --------------
|
|
182
|
+
# segment-watch deny stderr is multiline; without newline escaping, bash 3.2
|
|
183
|
+
# prints unparseable JSON and Cursor fail-opens (valve vanishes).
|
|
184
|
+
OUT="$(printf '{"workspace_roots":["%s"],"tool_name":"Bash","tool_input":{},"session_id":"cursor-mismatch"}' "$MIS" \
|
|
185
|
+
| env PATH="$CLEAN_PATH" bash "$SCRIPT_DIR/on-pre-tool.sh")"
|
|
186
|
+
case "$OUT" in *'"permission":"deny"'*) echo "PASS: nojq mismatch preTool denies" ;; *) echo "FAIL: nojq mismatch preTool [$OUT]"; FAIL=1 ;; esac
|
|
187
|
+
assert_single_json "nojq mismatch preTool deny single json" "$OUT"
|
|
188
|
+
case "$OUT" in
|
|
189
|
+
*$'\n'*$'\n'*) echo "FAIL: nojq deny JSON contains raw newlines [$OUT]"; FAIL=1 ;;
|
|
190
|
+
*) echo "PASS: nojq deny JSON has no raw newlines inside the string" ;;
|
|
191
|
+
esac
|
|
192
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
193
|
+
if printf '%s' "$OUT" | python3 -c 'import json,sys; json.load(sys.stdin)' >/dev/null 2>&1; then
|
|
194
|
+
echo "PASS: nojq deny JSON is parseable"
|
|
195
|
+
else
|
|
196
|
+
echo "FAIL: nojq deny JSON is not parseable [$OUT]"; FAIL=1
|
|
197
|
+
fi
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
|
|
201
|
+
else echo "Some checks FAILED."; exit 1; fi
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"hooks": {
|
|
4
|
+
"sessionStart": [
|
|
5
|
+
{ "command": "bash \"${DEVLOG_TRACKER_ROOT}/cursor/hooks/on-session-start.sh\"" }
|
|
6
|
+
],
|
|
7
|
+
"beforeSubmitPrompt": [
|
|
8
|
+
{ "command": "bash \"${DEVLOG_TRACKER_ROOT}/cursor/hooks/on-submit-prompt.sh\"" }
|
|
9
|
+
],
|
|
10
|
+
"preToolUse": [
|
|
11
|
+
{ "command": "bash \"${DEVLOG_TRACKER_ROOT}/cursor/hooks/on-pre-tool.sh\"" }
|
|
12
|
+
],
|
|
13
|
+
"stop": [
|
|
14
|
+
{
|
|
15
|
+
"command": "bash \"${DEVLOG_TRACKER_ROOT}/cursor/hooks/on-stop.sh\"",
|
|
16
|
+
"loop_limit": 1
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"sessionEnd": [
|
|
20
|
+
{ "command": "bash \"${DEVLOG_TRACKER_ROOT}/cursor/hooks/on-session-end.sh\"" }
|
|
21
|
+
],
|
|
22
|
+
"postToolUseFailure": [
|
|
23
|
+
{ "command": "bash \"${DEVLOG_TRACKER_ROOT}/cursor/hooks/on-tool-failure.sh\"" }
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
_src="${BASH_SOURCE[0]}"
|
|
4
|
+
SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
|
|
5
|
+
# shellcheck source=json-field.sh
|
|
6
|
+
. "$SCRIPT_DIR/json-field.sh"
|
|
7
|
+
# shellcheck source=devlog-md.sh
|
|
8
|
+
. "$SCRIPT_DIR/devlog-md.sh"
|
|
9
|
+
# shellcheck source=devlog-path.sh
|
|
10
|
+
. "$SCRIPT_DIR/devlog-path.sh"
|
|
11
|
+
|
|
12
|
+
devlog_resolve_paths "${CLAUDE_PROJECT_DIR:-.}"
|
|
13
|
+
[ -f "$DEVLOG_DIR/.enabled" ] || { echo "NOT_ENABLED"; exit 1; }
|
|
14
|
+
[ ! -e "$DEVLOG_DIR/.awaiting-reply" ] || { echo "ALREADY_OPEN"; exit 1; }
|
|
15
|
+
ROUND=""
|
|
16
|
+
[ ! -f "$DEVLOG_DIR/.round-open" ] || ROUND="$(json_int_get "$DEVLOG_DIR/.round-open" round)"
|
|
17
|
+
if [ -z "$ROUND" ] && [ -f "$DEVLOG_FILE" ]; then
|
|
18
|
+
ROUND="$(devlog_list_round_starts "$DEVLOG_FILE" | awk 'END { print $2 }')"
|
|
19
|
+
fi
|
|
20
|
+
[ -n "$ROUND" ] || { echo "NO_ROUND"; exit 1; }
|
|
21
|
+
OPENED_AT="$(date -Iseconds 2>/dev/null || date '+%Y-%m-%dT%H:%M:%S%z')"
|
|
22
|
+
printf '{"round": %s, "opened_at": "%s"}\n' "$ROUND" "$OPENED_AT" > "$DEVLOG_DIR/.awaiting-reply" || exit 1
|
|
23
|
+
printf 'OPENED=%s\n' "$ROUND"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# User-invoked: /devlog-tracker:checkpoint filesystem side.
|
|
3
|
+
# Sets Checkpoint Mode's max_silent_rounds threshold. $1 is required: a
|
|
4
|
+
# positive integer number of rounds.
|
|
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
|
+
ROUNDS_ARG="${1:-}"
|
|
20
|
+
case "$ROUNDS_ARG" in
|
|
21
|
+
''|*[!0-9]*|0)
|
|
22
|
+
echo "max_silent_rounds must be a positive integer number of rounds, got: $ROUNDS_ARG" >&2
|
|
23
|
+
exit 1
|
|
24
|
+
;;
|
|
25
|
+
esac
|
|
26
|
+
|
|
27
|
+
if [ -f "$DEVLOG_DIR/.checkpoint-state" ]; then
|
|
28
|
+
json_int_set "$DEVLOG_DIR/.checkpoint-state" max_silent_rounds "$ROUNDS_ARG"
|
|
29
|
+
APPLIED="$(json_int_get "$DEVLOG_DIR/.checkpoint-state" max_silent_rounds)"
|
|
30
|
+
if [ "$APPLIED" != "$ROUNDS_ARG" ]; then
|
|
31
|
+
ROUNDS="$(json_int_get "$DEVLOG_DIR/.checkpoint-state" rounds_since_checkpoint)"
|
|
32
|
+
MARKERS="$(json_int_get "$DEVLOG_DIR/.checkpoint-state" checkpoint_marker_count)"
|
|
33
|
+
case "$ROUNDS" in ''|*[!0-9]*) ROUNDS=0 ;; esac
|
|
34
|
+
case "$MARKERS" in ''|*[!0-9]*) MARKERS=0 ;; esac
|
|
35
|
+
printf '%s\n' "{\"rounds_since_checkpoint\": ${ROUNDS}, \"max_silent_rounds\": ${ROUNDS_ARG}, \"checkpoint_marker_count\": ${MARKERS}}" \
|
|
36
|
+
> "$DEVLOG_DIR/.checkpoint-state" || exit 1
|
|
37
|
+
fi
|
|
38
|
+
else
|
|
39
|
+
printf '%s\n' "{\"rounds_since_checkpoint\": 0, \"max_silent_rounds\": ${ROUNDS_ARG}, \"checkpoint_marker_count\": 0}" \
|
|
40
|
+
> "$DEVLOG_DIR/.checkpoint-state" || exit 1
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
echo "CHECKPOINT_MAX_SILENT_ROUNDS=$(json_int_get "$DEVLOG_DIR/.checkpoint-state" max_silent_rounds)"
|
|
44
|
+
exit 0
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
# Destructive and irreversible: only run when the caller has explicitly
|
|
12
|
+
# confirmed with the user first. This is a mechanical guard, not a
|
|
13
|
+
# replacement for that confirmation.
|
|
14
|
+
[ "${1:-}" = "--confirmed" ] || { echo "需要 --confirmed(使用者尚未確認,不要呼叫這支腳本)" >&2; exit 1; }
|
|
15
|
+
|
|
16
|
+
devlog_resolve_paths "${CLAUDE_PROJECT_DIR:-.}"
|
|
17
|
+
devlog_lock_acquire
|
|
18
|
+
trap 'devlog_lock_release' EXIT
|
|
19
|
+
MAIN="$DEVLOG_FILE"
|
|
20
|
+
ROUND_OPEN="$DEVLOG_DIR/.round-open"
|
|
21
|
+
ROUND_CURRENT="$DEVLOG_DIR/.round-current.md"
|
|
22
|
+
# Post-split, devlog.md may legitimately not exist yet (a project's very
|
|
23
|
+
# first round can still be open, with real content sitting only in
|
|
24
|
+
# .round-current.md) — accept either devlog.md existing or .round-current.md
|
|
25
|
+
# having content to clean.
|
|
26
|
+
[ -f "$MAIN" ] || [ -s "$ROUND_CURRENT" ] || { echo "devlog.md 不存在" >&2; exit 1; }
|
|
27
|
+
|
|
28
|
+
HAS_OPEN=0
|
|
29
|
+
OPEN=""
|
|
30
|
+
if [ -f "$ROUND_OPEN" ]; then
|
|
31
|
+
OPEN="$(json_int_get "$ROUND_OPEN" round)"
|
|
32
|
+
[ -n "$OPEN" ] || { echo ".round-open 內容無法解析,未清空" >&2; exit 1; }
|
|
33
|
+
[ -s "$ROUND_CURRENT" ] || { echo "找不到開著的 .round-current.md 內容,狀態可能不一致,未清空" >&2; exit 1; }
|
|
34
|
+
HAS_OPEN=1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
if [ "$HAS_OPEN" -eq 1 ]; then
|
|
38
|
+
# Keep the split's invariant intact: the open round's content stays in
|
|
39
|
+
# .round-current.md (renumbered to Round 1), never in devlog.md. Writing
|
|
40
|
+
# it into devlog.md here would leave .round-open pointing at a round
|
|
41
|
+
# that Stop/close-open-round.sh can't find (they only ever look at
|
|
42
|
+
# .round-current.md), silently disabling enforcement for the rest of
|
|
43
|
+
# this turn and orphaning recovery on a crash.
|
|
44
|
+
TMP_DIR="$(mktemp -d "$DEVLOG_DIR/.clean.XXXXXX")" || exit 1
|
|
45
|
+
trap 'rm -rf "$TMP_DIR"; devlog_lock_release' EXIT
|
|
46
|
+
NEW_CURRENT="$TMP_DIR/round-current.md"
|
|
47
|
+
awk -v open="$OPEN" '
|
|
48
|
+
NR == 1 && $0 ~ ("^## Round " open "([^0-9]|$)") { sub("^## Round " open, "## Round 1") }
|
|
49
|
+
{ print }
|
|
50
|
+
' "$ROUND_CURRENT" > "$NEW_CURRENT" || exit 1
|
|
51
|
+
[ -s "$NEW_CURRENT" ] && grep -q '^## Round 1' "$NEW_CURRENT" || { echo "重寫結果異常,未寫入" >&2; exit 1; }
|
|
52
|
+
mv "$NEW_CURRENT" "$ROUND_CURRENT" || exit 1
|
|
53
|
+
rm -f "$MAIN" || exit 1
|
|
54
|
+
rm -rf "$TMP_DIR"
|
|
55
|
+
json_int_set "$ROUND_OPEN" round 1
|
|
56
|
+
KEPT_ROUND=1
|
|
57
|
+
else
|
|
58
|
+
rm -f "$MAIN" "$ROUND_CURRENT" || exit 1
|
|
59
|
+
KEPT_ROUND=0
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
rm -f "$DEVLOG_DIR/.span-open" "$DEVLOG_DIR/.interrupted" "$DEVLOG_DIR/.awaiting-reply"
|
|
63
|
+
if [ -f "$DEVLOG_DIR/.checkpoint-state" ]; then
|
|
64
|
+
json_int_set "$DEVLOG_DIR/.checkpoint-state" rounds_since_checkpoint 0
|
|
65
|
+
json_int_set "$DEVLOG_DIR/.checkpoint-state" checkpoint_marker_count 0
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
printf 'CLEANED KEPT_ROUND=%s\n' "$KEPT_ROUND"
|
|
69
|
+
exit 0
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Stamp the open Round as INTERRUPTED. Always exit 0 (fail-open).
|
|
3
|
+
# Usage: close-open-round.sh <reason> [detail]
|
|
4
|
+
# The reason is written as a bracketed tag under Status (`[reason: ...]`) so
|
|
5
|
+
# it reads as internal debug metadata, not a broken/extra Status value — and
|
|
6
|
+
# stays visible even through a Markdown renderer (unlike an HTML comment,
|
|
7
|
+
# which a renderer would silently drop, losing the debug trail).
|
|
8
|
+
# detail (optional): "awaiting_question" makes the stub Summary/Handoff name
|
|
9
|
+
# the likely cause (interrupted while an AskUserQuestion answer was pending)
|
|
10
|
+
# instead of the generic "沒有正常收尾" — caller decides via
|
|
11
|
+
# detect-pending-question.sh; empty/unknown detail falls back to the generic
|
|
12
|
+
# stub, same as before.
|
|
13
|
+
# When detail is empty, the stub wording also depends on RECOVERED (did
|
|
14
|
+
# devlog.md change at all since the round's skeleton was written): if not,
|
|
15
|
+
# nothing was ever written back for this round (most likely the next prompt
|
|
16
|
+
# arrived before Claude engaged with it at all), so the stub says that
|
|
17
|
+
# plainly instead of implying work was interrupted mid-way.
|
|
18
|
+
# Silent on stdout — SessionStart injects stdout as additionalContext.
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
|
|
21
|
+
REASON="${1:-unknown}"
|
|
22
|
+
DETAIL="${2:-}"
|
|
23
|
+
|
|
24
|
+
_src="${BASH_SOURCE[0]}"
|
|
25
|
+
SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
|
|
26
|
+
# shellcheck source=json-field.sh
|
|
27
|
+
. "$SCRIPT_DIR/json-field.sh"
|
|
28
|
+
# shellcheck source=devlog-lock.sh
|
|
29
|
+
. "$SCRIPT_DIR/devlog-lock.sh"
|
|
30
|
+
# shellcheck source=devlog-path.sh
|
|
31
|
+
. "$SCRIPT_DIR/devlog-path.sh"
|
|
32
|
+
# shellcheck source=devlog-md.sh
|
|
33
|
+
. "$SCRIPT_DIR/devlog-md.sh"
|
|
34
|
+
|
|
35
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
|
36
|
+
devlog_resolve_paths "$PROJECT_DIR"
|
|
37
|
+
ENABLED_FLAG="$DEVLOG_DIR/.enabled"
|
|
38
|
+
ROUND_OPEN="$DEVLOG_DIR/.round-open"
|
|
39
|
+
INTERRUPTED_FLAG="$DEVLOG_DIR/.interrupted"
|
|
40
|
+
ROUND_CURRENT="$DEVLOG_DIR/.round-current.md"
|
|
41
|
+
TURN_MARKER="$DEVLOG_DIR/.turn-start"
|
|
42
|
+
|
|
43
|
+
drop_markers() {
|
|
44
|
+
rm -f "$ROUND_OPEN" "$INTERRUPTED_FLAG" 2>/dev/null || true
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
[ -f "$ENABLED_FLAG" ] || exit 0
|
|
48
|
+
devlog_lock_acquire
|
|
49
|
+
trap 'devlog_lock_release' EXIT
|
|
50
|
+
if [ ! -f "$ROUND_OPEN" ]; then
|
|
51
|
+
rm -f "$INTERRUPTED_FLAG" 2>/dev/null || true
|
|
52
|
+
exit 0
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
ROUND_OPEN_FILE="$(json_str_get "$ROUND_OPEN" file 2>/dev/null || true)"
|
|
56
|
+
if [ -n "$ROUND_OPEN_FILE" ] && [ "$ROUND_OPEN_FILE" != "${DEVLOG_FILE##*/}" ]; then
|
|
57
|
+
# This marker belongs to a different branch's devlog file (e.g. the user
|
|
58
|
+
# switched branches between round-start and now). Leave it untouched —
|
|
59
|
+
# whichever branch it actually belongs to will resolve and close it
|
|
60
|
+
# correctly when that branch is current again.
|
|
61
|
+
exit 0
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
OPEN_ROUND="$(json_int_get "$ROUND_OPEN" round)"
|
|
65
|
+
case "$OPEN_ROUND" in
|
|
66
|
+
''|*[!0-9]*) drop_markers; exit 0 ;;
|
|
67
|
+
esac
|
|
68
|
+
|
|
69
|
+
if [ ! -f "$ROUND_CURRENT" ] || [ ! -s "$ROUND_CURRENT" ]; then
|
|
70
|
+
drop_markers
|
|
71
|
+
exit 0
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
RECOVERED=0
|
|
75
|
+
if [ -f "$TURN_MARKER" ]; then
|
|
76
|
+
TURN_HASH="$(cat "$TURN_MARKER" 2>/dev/null || echo '')"
|
|
77
|
+
CUR_HASH="$(cksum < "$ROUND_CURRENT" 2>/dev/null || echo '')"
|
|
78
|
+
if [ -n "$TURN_HASH" ] && [ -n "$CUR_HASH" ] && [ "$CUR_HASH" != "$TURN_HASH" ]; then
|
|
79
|
+
RECOVERED=1
|
|
80
|
+
fi
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
TMP="$ROUND_CURRENT.tmp"
|
|
84
|
+
awk -v reason="$REASON" -v detail="$DETAIL" -v recovered="$RECOVERED" '
|
|
85
|
+
function summary_stub() {
|
|
86
|
+
if (detail == "awaiting_question") return "這輪在等待使用者回答 AskUserQuestion 時結束,還沒收到答案。"
|
|
87
|
+
if (recovered == 0) return "這一輪送出後,devlog.md 沒有留下任何後續處理紀錄就結束了。"
|
|
88
|
+
return "這輪意外中斷。"
|
|
89
|
+
}
|
|
90
|
+
function reply_stub() {
|
|
91
|
+
if (detail == "awaiting_question") return "(中斷前正在等使用者回答問題。)"
|
|
92
|
+
if (recovered == 0) return "(這一輪在 devlog.md 裡沒有留下回覆紀錄。)"
|
|
93
|
+
return "(這輪意外中斷,沒有對使用者完成回覆。)"
|
|
94
|
+
}
|
|
95
|
+
function handoff_stub() {
|
|
96
|
+
if (detail == "awaiting_question") return "Claude 提了問題還在等回答,session 就先結束了;不是中途出錯,只是還沒收到答案。需要的話重新確認一次問題再繼續。"
|
|
97
|
+
if (recovered == 0) return "這一輪沒有留下任何處理紀錄;下一步請看上面的 User Input 原文決定要不要接著處理。"
|
|
98
|
+
return "這輪沒有正常收尾。"
|
|
99
|
+
}
|
|
100
|
+
{ lines[NR] = $0; n = NR }
|
|
101
|
+
END {
|
|
102
|
+
has_s = 0
|
|
103
|
+
has_r = 0
|
|
104
|
+
has_h = 0
|
|
105
|
+
for (i = 1; i <= n; i++) {
|
|
106
|
+
if (lines[i] ~ /^### Summary/) has_s = 1
|
|
107
|
+
if (lines[i] ~ /^### Reply/) has_r = 1
|
|
108
|
+
if (lines[i] ~ /^### Handoff/) has_h = 1
|
|
109
|
+
}
|
|
110
|
+
# Recovered-complete still keys off Summary+Handoff (pre-Reply contract);
|
|
111
|
+
# Stop will require Reply on the next normal close if missing.
|
|
112
|
+
if (recovered && has_s && has_h) { exit 3 }
|
|
113
|
+
|
|
114
|
+
skip_val = 0
|
|
115
|
+
saw_status = 0
|
|
116
|
+
for (i = 1; i <= n; i++) {
|
|
117
|
+
if (skip_val) { skip_val = 0; continue }
|
|
118
|
+
if (lines[i] ~ /^### Status/) {
|
|
119
|
+
saw_status = 1
|
|
120
|
+
if (!has_s) {
|
|
121
|
+
print "### Summary"
|
|
122
|
+
print summary_stub()
|
|
123
|
+
print ""
|
|
124
|
+
}
|
|
125
|
+
if (!has_r) {
|
|
126
|
+
print "### Reply"
|
|
127
|
+
print reply_stub()
|
|
128
|
+
print ""
|
|
129
|
+
}
|
|
130
|
+
if (!has_h) {
|
|
131
|
+
print "### Handoff"
|
|
132
|
+
print "#### 現況"
|
|
133
|
+
print handoff_stub()
|
|
134
|
+
print ""
|
|
135
|
+
}
|
|
136
|
+
print "### Status"
|
|
137
|
+
print "INTERRUPTED"
|
|
138
|
+
print "[reason: " reason "]"
|
|
139
|
+
skip_val = 1
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
print lines[i]
|
|
143
|
+
}
|
|
144
|
+
if (!saw_status) {
|
|
145
|
+
if (!has_s) {
|
|
146
|
+
print "### Summary"
|
|
147
|
+
print summary_stub()
|
|
148
|
+
print ""
|
|
149
|
+
}
|
|
150
|
+
if (!has_r) {
|
|
151
|
+
print "### Reply"
|
|
152
|
+
print reply_stub()
|
|
153
|
+
print ""
|
|
154
|
+
}
|
|
155
|
+
if (!has_h) {
|
|
156
|
+
print "### Handoff"
|
|
157
|
+
print "#### 現況"
|
|
158
|
+
print handoff_stub()
|
|
159
|
+
print ""
|
|
160
|
+
}
|
|
161
|
+
print "### Status"
|
|
162
|
+
print "INTERRUPTED"
|
|
163
|
+
print "[reason: " reason "]"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
' "$ROUND_CURRENT" > "$TMP" 2>/dev/null
|
|
167
|
+
AWK_RC=$?
|
|
168
|
+
if [ "$AWK_RC" -eq 0 ]; then
|
|
169
|
+
mv "$TMP" "$ROUND_CURRENT" 2>/dev/null || true
|
|
170
|
+
else
|
|
171
|
+
rm -f "$TMP" 2>/dev/null || true
|
|
172
|
+
fi
|
|
173
|
+
if [ "$AWK_RC" -eq 0 ] || [ "$AWK_RC" -eq 3 ]; then
|
|
174
|
+
# 只有併入真的成功才清掉 .round-open:併入失敗(例如寫入失敗)時保留它,
|
|
175
|
+
# 讓下一次 dangling-heal(下一則訊息/下次 SessionStart 都會跑到)還有
|
|
176
|
+
# 機會重試,而不是內容被孤立在 .round-current.md 卻沒有任何機制知道要去
|
|
177
|
+
# 救它。.interrupted 這個訊號本身已經處理完,兩種結果都清掉。
|
|
178
|
+
if devlog_merge_round_current "$DEVLOG_FILE" "$ROUND_CURRENT"; then
|
|
179
|
+
drop_markers
|
|
180
|
+
else
|
|
181
|
+
rm -f "$INTERRUPTED_FLAG" 2>/dev/null || true
|
|
182
|
+
fi
|
|
183
|
+
else
|
|
184
|
+
drop_markers
|
|
185
|
+
fi
|
|
186
|
+
exit 0
|