devlog-tracker 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +234 -0
  3. package/bin/devlog-tracker.js +51 -0
  4. package/cli/init.js +55 -0
  5. package/cli/init.test.js +37 -0
  6. package/cli/merge-hooks.js +60 -0
  7. package/cli/merge-hooks.test.js +187 -0
  8. package/cli/platforms/codex.js +13 -0
  9. package/cli/platforms/cursor.js +13 -0
  10. package/cli/status.js +14 -0
  11. package/cli/status.test.js +33 -0
  12. package/cli/vendor.js +37 -0
  13. package/cli/vendor.test.js +84 -0
  14. package/codex/hooks/on-pre-tool.sh +19 -0
  15. package/codex/hooks/on-session-end.sh +31 -0
  16. package/codex/hooks/on-session-start.sh +37 -0
  17. package/codex/hooks/on-stop.sh +18 -0
  18. package/codex/hooks/on-user-prompt-submit.sh +15 -0
  19. package/codex/hooks/project-dir.sh +13 -0
  20. package/codex/hooks/test-adapters.sh +98 -0
  21. package/codex/hooks.json +54 -0
  22. package/commands/checkpoint.md +17 -0
  23. package/commands/clean.md +50 -0
  24. package/commands/compact.md +24 -0
  25. package/commands/continue.md +30 -0
  26. package/commands/keep.md +100 -0
  27. package/commands/lessons-drift.md +19 -0
  28. package/commands/lessons-off.md +16 -0
  29. package/commands/lessons-on.md +20 -0
  30. package/commands/lessons.md +20 -0
  31. package/commands/overview.md +32 -0
  32. package/commands/pause.md +20 -0
  33. package/commands/resume.md +18 -0
  34. package/commands/segment-watch.md +21 -0
  35. package/commands/span.md +13 -0
  36. package/commands/start.md +19 -0
  37. package/commands/status.md +14 -0
  38. package/cursor/hooks/on-pre-tool.sh +24 -0
  39. package/cursor/hooks/on-session-end.sh +11 -0
  40. package/cursor/hooks/on-session-start.sh +15 -0
  41. package/cursor/hooks/on-stop.sh +44 -0
  42. package/cursor/hooks/on-submit-prompt.sh +28 -0
  43. package/cursor/hooks/on-tool-failure.sh +16 -0
  44. package/cursor/hooks/project-dir.sh +10 -0
  45. package/cursor/hooks/test-adapters.sh +201 -0
  46. package/cursor/hooks.json +26 -0
  47. package/hooks/scripts/await-open.sh +23 -0
  48. package/hooks/scripts/checkpoint-set.sh +44 -0
  49. package/hooks/scripts/clean-devlog.sh +69 -0
  50. package/hooks/scripts/close-open-round.sh +186 -0
  51. package/hooks/scripts/compact-devlog.sh +72 -0
  52. package/hooks/scripts/detect-pending-question.sh +31 -0
  53. package/hooks/scripts/devlog-lock.sh +38 -0
  54. package/hooks/scripts/devlog-md.sh +222 -0
  55. package/hooks/scripts/devlog-path.sh +70 -0
  56. package/hooks/scripts/enforce-devlog.sh +629 -0
  57. package/hooks/scripts/files-snapshot.sh +70 -0
  58. package/hooks/scripts/json-field.sh +76 -0
  59. package/hooks/scripts/keep-move.sh +155 -0
  60. package/hooks/scripts/kept-list.sh +40 -0
  61. package/hooks/scripts/lessons-append.sh +93 -0
  62. package/hooks/scripts/lessons-drift-set.sh +42 -0
  63. package/hooks/scripts/lessons-off.sh +15 -0
  64. package/hooks/scripts/lessons-on.sh +21 -0
  65. package/hooks/scripts/lessons-read.sh +60 -0
  66. package/hooks/scripts/on-session-end.sh +14 -0
  67. package/hooks/scripts/on-stop-failure.sh +18 -0
  68. package/hooks/scripts/on-tool-failure.sh +25 -0
  69. package/hooks/scripts/pause-devlog.sh +13 -0
  70. package/hooks/scripts/redact-prompt.sh +19 -0
  71. package/hooks/scripts/resume-devlog.sh +41 -0
  72. package/hooks/scripts/round-start.sh +333 -0
  73. package/hooks/scripts/run-tests.sh +26 -0
  74. package/hooks/scripts/segment-watch-set.sh +47 -0
  75. package/hooks/scripts/segment-watch.sh +197 -0
  76. package/hooks/scripts/session-start-devlog.sh +157 -0
  77. package/hooks/scripts/span-close.sh +9 -0
  78. package/hooks/scripts/span-open.sh +21 -0
  79. package/hooks/scripts/start-devlog.sh +24 -0
  80. package/hooks/scripts/status-devlog.sh +43 -0
  81. package/hooks/scripts/tests/test-await-open.sh +112 -0
  82. package/hooks/scripts/tests/test-branch-scoped-integration.sh +108 -0
  83. package/hooks/scripts/tests/test-checkpoint-set.sh +36 -0
  84. package/hooks/scripts/tests/test-clean-devlog.sh +160 -0
  85. package/hooks/scripts/tests/test-cli-init-e2e.sh +37 -0
  86. package/hooks/scripts/tests/test-close-open-round.sh +325 -0
  87. package/hooks/scripts/tests/test-compact-devlog.sh +62 -0
  88. package/hooks/scripts/tests/test-devlog-lock.sh +37 -0
  89. package/hooks/scripts/tests/test-devlog-md.sh +530 -0
  90. package/hooks/scripts/tests/test-devlog-path.sh +118 -0
  91. package/hooks/scripts/tests/test-enforce-devlog-files.sh +233 -0
  92. package/hooks/scripts/tests/test-enforce-devlog-handoff-order.sh +223 -0
  93. package/hooks/scripts/tests/test-enforce-devlog-workspace.sh +333 -0
  94. package/hooks/scripts/tests/test-enforce-devlog.sh +1290 -0
  95. package/hooks/scripts/tests/test-files-snapshot.sh +142 -0
  96. package/hooks/scripts/tests/test-json-field.sh +57 -0
  97. package/hooks/scripts/tests/test-keep-move.sh +127 -0
  98. package/hooks/scripts/tests/test-kept-list.sh +80 -0
  99. package/hooks/scripts/tests/test-lessons-append.sh +138 -0
  100. package/hooks/scripts/tests/test-lessons-drift-set.sh +36 -0
  101. package/hooks/scripts/tests/test-lessons-on-off.sh +75 -0
  102. package/hooks/scripts/tests/test-lessons-read.sh +97 -0
  103. package/hooks/scripts/tests/test-on-interrupt.sh +186 -0
  104. package/hooks/scripts/tests/test-redact-prompt.sh +27 -0
  105. package/hooks/scripts/tests/test-resume-devlog.sh +35 -0
  106. package/hooks/scripts/tests/test-round-start.sh +825 -0
  107. package/hooks/scripts/tests/test-segment-watch-set.sh +75 -0
  108. package/hooks/scripts/tests/test-segment-watch.sh +468 -0
  109. package/hooks/scripts/tests/test-session-start-devlog.sh +508 -0
  110. package/hooks/scripts/tests/test-start-pause-devlog.sh +84 -0
  111. package/hooks/scripts/tests/test-status-span.sh +50 -0
  112. package/hooks/scripts/tests/test-workspace-snapshot.sh +130 -0
  113. package/hooks/scripts/workspace-snapshot.sh +69 -0
  114. package/package.json +37 -0
  115. package/skills/devlog-tracker/SKILL.md +411 -0
  116. package/skills/devlog-tracker/references/checkpoint-mode.md +42 -0
  117. package/skills/devlog-tracker/references/lessons-mode.md +32 -0
  118. package/skills/devlog-tracker/references/reply-fold.md +129 -0
  119. package/skills/devlog-tracker/references/round-segments.md +55 -0
  120. package/skills/devlog-tracker/references/span-mode.md +65 -0
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env bash
2
+ # Self-check for files-snapshot.sh's files_snapshot(), covering the
3
+ # uncommitted branch (git status), the committed branch (git diff-tree),
4
+ # rename-as-delete+add, and .devlog/ exclusion in both branches.
5
+ # No framework — plain assert-and-exit, matching this repo's existing style.
6
+ set -uo pipefail
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
9
+ . "$SCRIPT_DIR/files-snapshot.sh"
10
+
11
+ TMP_ROOT="$(mktemp -d)"
12
+ trap 'rm -rf "$TMP_ROOT"' EXIT
13
+
14
+ FAIL=0
15
+ assert_eq() {
16
+ local desc="$1" expected="$2" actual="$3"
17
+ if [ "$actual" = "$expected" ]; then
18
+ echo "PASS: $desc"
19
+ else
20
+ echo "FAIL: $desc"
21
+ printf ' expected: %s\n' "$expected"
22
+ printf ' actual: %s\n' "$actual"
23
+ FAIL=1
24
+ fi
25
+ }
26
+
27
+ # --- non-git directory ----------------------------------------------------
28
+ NONGIT="$TMP_ROOT/nongit"
29
+ mkdir -p "$NONGIT"
30
+ OUT="$(files_snapshot "$NONGIT")"
31
+ assert_eq "non-git directory" "" "$OUT"
32
+
33
+ # --- repo setup -------------------------------------------------------------
34
+ REPO="$TMP_ROOT/repo"
35
+ mkdir -p "$REPO/.devlog"
36
+ git -C "$REPO" init -q -b main
37
+ git -C "$REPO" config user.email test@example.com
38
+ git -C "$REPO" config user.name test
39
+ echo one > "$REPO/a.txt"
40
+ echo two > "$REPO/b.txt"
41
+ echo bookkeeping > "$REPO/.devlog/devlog.md"
42
+ git -C "$REPO" add a.txt b.txt .devlog/devlog.md
43
+ git -C "$REPO" commit -q -m init
44
+
45
+ # --- clean tree -> empty ------------------------------------------------------
46
+ OUT="$(files_snapshot "$REPO")"
47
+ assert_eq "clean tree" "" "$OUT"
48
+
49
+ # --- untracked file -> 新增 ----------------------------------------------------
50
+ echo new > "$REPO/c.txt"
51
+ OUT="$(files_snapshot "$REPO")"
52
+ assert_eq "untracked file" "新增:c.txt" "$OUT"
53
+ rm -f "$REPO/c.txt"
54
+
55
+ # --- modified tracked file -> 修改 ---------------------------------------------
56
+ echo changed >> "$REPO/a.txt"
57
+ OUT="$(files_snapshot "$REPO")"
58
+ assert_eq "modified tracked file" "修改:a.txt" "$OUT"
59
+ git -C "$REPO" checkout -q -- a.txt
60
+
61
+ # --- deleted tracked file -> 刪除 ----------------------------------------------
62
+ rm -f "$REPO/b.txt"
63
+ OUT="$(files_snapshot "$REPO")"
64
+ assert_eq "deleted tracked file" "刪除:b.txt" "$OUT"
65
+ git -C "$REPO" checkout -q -- b.txt
66
+
67
+ # --- mixed: new + modified + deleted, fixed category order --------------------
68
+ echo new > "$REPO/c.txt"
69
+ echo changed >> "$REPO/a.txt"
70
+ rm -f "$REPO/b.txt"
71
+ OUT="$(files_snapshot "$REPO")"
72
+ EXPECTED="新增:c.txt
73
+ 修改:a.txt
74
+ 刪除:b.txt"
75
+ assert_eq "mixed changes, fixed category order" "$EXPECTED" "$OUT"
76
+ rm -f "$REPO/c.txt"
77
+ git -C "$REPO" checkout -q -- a.txt b.txt
78
+
79
+ # --- .devlog/ excluded, real change still reported -----------------------------
80
+ echo bookkeeping change >> "$REPO/.devlog/devlog.md"
81
+ echo real change >> "$REPO/a.txt"
82
+ OUT="$(files_snapshot "$REPO")"
83
+ assert_eq ".devlog/ excluded (uncommitted branch)" "修改:a.txt" "$OUT"
84
+ git -C "$REPO" checkout -q -- a.txt .devlog/devlog.md
85
+
86
+ # --- rename shows as delete+add (no -M anywhere) --------------------------------
87
+ git -C "$REPO" mv a.txt a-renamed.txt
88
+ OUT="$(files_snapshot "$REPO")"
89
+ EXPECTED="新增:a-renamed.txt
90
+ 刪除:a.txt"
91
+ assert_eq "rename -> delete+add" "$EXPECTED" "$OUT"
92
+ git -C "$REPO" reset -q --hard HEAD
93
+
94
+ # --- committed branch: add + modify + delete in one commit -----------------------
95
+ echo new-committed > "$REPO/d.txt"
96
+ echo changed-committed >> "$REPO/a.txt"
97
+ rm -f "$REPO/b.txt"
98
+ git -C "$REPO" add -A
99
+ git -C "$REPO" commit -q -m "mixed commit"
100
+ HASH="$(git -C "$REPO" rev-parse --short HEAD)"
101
+ OUT="$(files_snapshot "$REPO" "$HASH")"
102
+ EXPECTED="新增:d.txt
103
+ 修改:a.txt
104
+ 刪除:b.txt"
105
+ assert_eq "committed branch, mixed" "$EXPECTED" "$OUT"
106
+
107
+ # --- committed branch: .devlog/ excluded there too --------------------------------
108
+ echo bookkeeping2 >> "$REPO/.devlog/devlog.md"
109
+ echo e > "$REPO/e.txt"
110
+ git -C "$REPO" add -A
111
+ git -C "$REPO" commit -q -m "with devlog bookkeeping"
112
+ HASH2="$(git -C "$REPO" rev-parse --short HEAD)"
113
+ OUT="$(files_snapshot "$REPO" "$HASH2")"
114
+ assert_eq ".devlog/ excluded (committed branch)" "新增:e.txt" "$OUT"
115
+
116
+ # --- root commit with --root flag (should show files, not empty) ------------------
117
+ ROOTREPO="$TMP_ROOT/root"
118
+ mkdir -p "$ROOTREPO/.devlog"
119
+ git -C "$ROOTREPO" init -q -b main
120
+ git -C "$ROOTREPO" config user.email test@example.com
121
+ git -C "$ROOTREPO" config user.name test
122
+ echo file1 > "$ROOTREPO/file1.txt"
123
+ echo file2 > "$ROOTREPO/file2.txt"
124
+ echo bookkeeping > "$ROOTREPO/.devlog/devlog.md"
125
+ git -C "$ROOTREPO" add file1.txt file2.txt .devlog/devlog.md
126
+ git -C "$ROOTREPO" commit -q -m "initial root commit"
127
+ ROOT_HASH="$(git -C "$ROOTREPO" rev-parse --short HEAD)"
128
+ OUT="$(files_snapshot "$ROOTREPO" "$ROOT_HASH")"
129
+ EXPECTED="新增:file1.txt, file2.txt"
130
+ assert_eq "root commit with multiple files (--root)" "$EXPECTED" "$OUT"
131
+
132
+ # --- unresolvable hash -> empty (fail-open) ---------------------------------------
133
+ OUT="$(files_snapshot "$REPO" "0000000")"
134
+ assert_eq "unresolvable hash -> empty" "" "$OUT"
135
+
136
+ if [ "$FAIL" -eq 0 ]; then
137
+ echo "All checks passed."
138
+ exit 0
139
+ else
140
+ echo "Some checks FAILED."
141
+ exit 1
142
+ fi
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
4
+ # shellcheck source=json-field.sh
5
+ . "$SCRIPT_DIR/json-field.sh"
6
+ TMP="$(mktemp)"
7
+ trap 'rm -f "$TMP"' EXIT
8
+ FAIL=0
9
+ assert_eq() {
10
+ local desc="$1" expected="$2" actual="$3"
11
+ if [ "$expected" = "$actual" ]; then echo "PASS: $desc"
12
+ else echo "FAIL: $desc (expected [$expected] got [$actual])"; FAIL=1; fi
13
+ }
14
+
15
+ printf '%s\n' '{"ticks_since_checkin": 2, "max_silent_ticks": 5}' > "$TMP"
16
+ assert_eq "int get" "2" "$(json_int_get "$TMP" ticks_since_checkin)"
17
+ assert_eq "int get max" "5" "$(json_int_get "$TMP" max_silent_ticks)"
18
+ assert_eq "missing key" "" "$(json_int_get "$TMP" no_such)"
19
+
20
+ printf '%s\n' '{"ticks_since_checkin": 08}' > "$TMP"
21
+ assert_eq "reject octal pad" "" "$(json_int_get "$TMP" ticks_since_checkin)"
22
+
23
+ printf '%s\n' '{"opened_at": "2026-09-08T21:40:00+08:00", "round": 12}' > "$TMP"
24
+ assert_eq "str get" "2026-09-08T21:40:00+08:00" "$(json_str_get "$TMP" opened_at)"
25
+ assert_eq "int round" "12" "$(json_int_get "$TMP" round)"
26
+
27
+ printf '%s\n' '{"ticks_since_checkin": 2, "max_silent_ticks": 5}' > "$TMP"
28
+ json_int_set "$TMP" ticks_since_checkin 0
29
+ assert_eq "int set" "0" "$(json_int_get "$TMP" ticks_since_checkin)"
30
+ assert_eq "int set preserves sibling" "5" "$(json_int_get "$TMP" max_silent_ticks)"
31
+
32
+ printf '%s\n' '{"last_seen_cksum": "old", "last_change_epoch": 1}' > "$TMP"
33
+ json_str_set "$TMP" last_seen_cksum "123 2"
34
+ assert_eq "str set" "123 2" "$(json_str_get "$TMP" last_seen_cksum)"
35
+ assert_eq "str set preserves epoch" "1" "$(json_int_get "$TMP" last_change_epoch)"
36
+
37
+ printf '%s\n' 'not json' > "$TMP"
38
+ assert_eq "malformed int" "" "$(json_int_get "$TMP" round)"
39
+ assert_eq "malformed str" "" "$(json_str_get "$TMP" opened_at)"
40
+
41
+ INPUT='{"session_id": "abc-123", "prompt": "hello world"}'
42
+ assert_eq "str field get" "abc-123" "$(json_str_field "$INPUT" session_id)"
43
+ assert_eq "str field get other key" "hello world" "$(json_str_field "$INPUT" prompt)"
44
+ assert_eq "str field missing key" "" "$(json_str_field "$INPUT" no_such)"
45
+
46
+ INPUT='{"reason": null}'
47
+ assert_eq "str field null" "" "$(json_str_field "$INPUT" reason)"
48
+
49
+ INPUT='not json'
50
+ assert_eq "str field malformed" "" "$(json_str_field "$INPUT" reason)"
51
+
52
+ assert_eq "slugify basic" "foo-bar" "$(slugify 'foo bar')"
53
+ assert_eq "slugify collapses runs" "foo-bar" "$(slugify 'foo bar')"
54
+ assert_eq "slugify trims edges" "foo" "$(slugify ' foo ')"
55
+
56
+ if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
57
+ else echo "Some checks FAILED."; exit 1; fi
@@ -0,0 +1,127 @@
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_eq() {
8
+ local d="$1" e="$2" a="$3"
9
+ if [ "$e" = "$a" ]; then echo "PASS: $d"; else echo "FAIL: $d expected [$e] got [$a]"; FAIL=1; fi
10
+ }
11
+ make_round() {
12
+ local file="$1" n="$2" status="$3"
13
+ cat >> "$file" <<EOF
14
+ ## Round $n — 2026-09-09T12:00:00+08:00
15
+
16
+ ### Status
17
+ $status
18
+ EOF
19
+ }
20
+
21
+ export CLAUDE_PROJECT_DIR="$TMP/missing"
22
+ bash "$SCRIPT_DIR/keep-move.sh" --from 1 --to 1 --name x >/dev/null 2>&1
23
+ assert_eq "missing log exit 1" 1 "$?"
24
+
25
+ BAD="$TMP/bad-name"
26
+ mkdir -p "$BAD/.devlog"
27
+ export CLAUDE_PROJECT_DIR="$BAD"
28
+ make_round "$BAD/.devlog/devlog.md" 1 DONE
29
+ bash "$SCRIPT_DIR/keep-move.sh" --from 1 --to 1 --name devlog.md >/dev/null 2>&1
30
+ assert_eq "devlog.md name rejected" 1 "$?"
31
+ [ ! -e "$BAD/.devlog/devlog.md.md" ] || { echo "FAIL: devlog.md.md created"; FAIL=1; }
32
+
33
+ EP="$TMP/episode"
34
+ mkdir -p "$EP/.devlog"
35
+ export CLAUDE_PROJECT_DIR="$EP"
36
+ printf '# project\n\n' > "$EP/.devlog/devlog.md"
37
+ make_round "$EP/.devlog/devlog.md" 1 DONE
38
+ make_round "$EP/.devlog/devlog.md" 2 DONE
39
+ make_round "$EP/.devlog/devlog.md" 3 IN_PROGRESS
40
+ printf '%s\n' '{"round": 3, "opened_at": "now"}' > "$EP/.devlog/.round-open"
41
+ bash "$SCRIPT_DIR/keep-move.sh" --from 1 --to 1 --name first >/dev/null
42
+ assert_eq "episode keep exits 0" 0 "$?"
43
+ grep -q '^# Kept log' "$EP/.devlog/devlog.first.md" && echo "PASS: provenance written" || { echo "FAIL: provenance"; FAIL=1; }
44
+ grep -q '^## Round 1' "$EP/.devlog/devlog.first.md" && echo "PASS: named file has round" || { echo "FAIL: named round"; FAIL=1; }
45
+ grep -q '^## Round 1' "$EP/.devlog/devlog.md" && { echo "FAIL: moved round remains"; FAIL=1; } || echo "PASS: moved round removed"
46
+ grep -q '^## Round 3' "$EP/.devlog/devlog.md" && echo "PASS: open round remains" || { echo "FAIL: open moved"; FAIL=1; }
47
+ bash "$SCRIPT_DIR/keep-move.sh" --from 3 --to 3 --name open >/dev/null 2>&1
48
+ assert_eq "open round rejected" 1 "$?"
49
+ bash "$SCRIPT_DIR/keep-move.sh" --from 2 --to 2 --name first >/dev/null 2>&1
50
+ assert_eq "overwrite rejected" 1 "$?"
51
+
52
+ FULL="$TMP/full"
53
+ mkdir -p "$FULL/.devlog"
54
+ export CLAUDE_PROJECT_DIR="$FULL"
55
+ printf '# project\n\n' > "$FULL/.devlog/devlog.md"
56
+ make_round "$FULL/.devlog/devlog.md" 1 DONE
57
+ make_round "$FULL/.devlog/devlog.md" 2 DONE
58
+ make_round "$FULL/.devlog/devlog.md" 3 IN_PROGRESS
59
+ printf '%s\n' '{"round": 3, "opened_at": "now"}' > "$FULL/.devlog/.round-open"
60
+ printf '%s\n' '{"round": 2, "ticks": 1, "max_silent_ticks": 5}' > "$FULL/.devlog/.span-open"
61
+ printf '%s\n' '{"rounds_since_checkpoint": 19, "max_silent_rounds": 20, "checkpoint_marker_count": 0}' > "$FULL/.devlog/.checkpoint-state"
62
+ bash "$SCRIPT_DIR/keep-move.sh" --from 1 --to 2 --name episode >/dev/null
63
+ assert_eq "full keep exits 0" 0 "$?"
64
+ grep -q '^## Round 1 ' "$FULL/.devlog/devlog.md" && echo "PASS: open round renumbered" || { echo "FAIL: full renumber"; FAIL=1; }
65
+ grep -q '"round": 1' "$FULL/.devlog/.round-open" && echo "PASS: round state reset" || { echo "FAIL: round state"; FAIL=1; }
66
+ grep -q '"rounds_since_checkpoint": 0' "$FULL/.devlog/.checkpoint-state" && echo "PASS: checkpoint reset" || { echo "FAIL: checkpoint state"; FAIL=1; }
67
+ grep -q '"max_silent_rounds": 20' "$FULL/.devlog/.checkpoint-state" && echo "PASS: checkpoint max kept" || { echo "FAIL: checkpoint max"; FAIL=1; }
68
+ [ ! -e "$FULL/.devlog/.span-open" ] && echo "PASS: span removed" || { echo "FAIL: span remains"; FAIL=1; }
69
+
70
+ grep -q '^## Kept 索引' "$FULL/.devlog/devlog.md" && echo "PASS: full keep wrote a Kept 索引 block" || { echo "FAIL: full keep should write a Kept 索引 block"; FAIL=1; }
71
+ grep -q 'devlog.episode.md' "$FULL/.devlog/devlog.md" && echo "PASS: full keep index line names devlog.episode.md" || { echo "FAIL: full keep index line missing devlog.episode.md"; FAIL=1; }
72
+
73
+ INDEX="$TMP/index"
74
+ mkdir -p "$INDEX/.devlog"
75
+ export CLAUDE_PROJECT_DIR="$INDEX"
76
+ printf '# project\n\n' > "$INDEX/.devlog/devlog.md"
77
+ make_round "$INDEX/.devlog/devlog.md" 1 DONE
78
+ make_round "$INDEX/.devlog/devlog.md" 2 DONE
79
+ make_round "$INDEX/.devlog/devlog.md" 3 DONE
80
+ # Keep an open round so the 3rd keep below (--from 3 --to 3) stays a *partial*
81
+ # keep — a full keep drops everything before the first round, which would
82
+ # leave ## Kept 索引 at line 1 and silently stop testing the blank-line
83
+ # separator regression checked below.
84
+ make_round "$INDEX/.devlog/devlog.md" 4 IN_PROGRESS
85
+ printf '%s\n' '{"round": 4, "opened_at": "now"}' > "$INDEX/.devlog/.round-open"
86
+ bash "$SCRIPT_DIR/keep-move.sh" --from 1 --to 1 --name topic-a >/dev/null
87
+ KEPT_COUNT_1="$(grep -c '^- ' "$INDEX/.devlog/devlog.md")"
88
+ [ "$KEPT_COUNT_1" -eq 1 ] && echo "PASS: first partial keep adds exactly one index line" || { echo "FAIL: expected 1 index line, got $KEPT_COUNT_1"; FAIL=1; }
89
+ bash "$SCRIPT_DIR/keep-move.sh" --from 2 --to 2 --name topic-b >/dev/null
90
+ KEPT_COUNT_2="$(grep -c '^- ' "$INDEX/.devlog/devlog.md")"
91
+ [ "$KEPT_COUNT_2" -eq 2 ] && echo "PASS: second partial keep accumulates to two index lines" || { echo "FAIL: expected 2 index lines, got $KEPT_COUNT_2"; FAIL=1; }
92
+ KEPT_BLOCKS="$(grep -c '^## Kept 索引' "$INDEX/.devlog/devlog.md")"
93
+ [ "$KEPT_BLOCKS" -eq 1 ] && echo "PASS: exactly one Kept 索引 heading after two keeps" || { echo "FAIL: expected exactly 1 Kept 索引 heading, got $KEPT_BLOCKS"; FAIL=1; }
94
+ grep -q 'devlog.topic-a.md' "$INDEX/.devlog/devlog.md" && echo "PASS: first line survives the second keep's rewrite" || { echo "FAIL: devlog.topic-a.md line lost after second keep"; FAIL=1; }
95
+ bash "$SCRIPT_DIR/keep-move.sh" --from 3 --to 3 --name topic-c >/dev/null
96
+ KEPT_COUNT_3="$(grep -c '^- ' "$INDEX/.devlog/devlog.md")"
97
+ [ "$KEPT_COUNT_3" -eq 3 ] && echo "PASS: third partial keep accumulates to three index lines" || { echo "FAIL: expected 3 index lines, got $KEPT_COUNT_3"; FAIL=1; }
98
+ HEADING_LINE="$(grep -n '^## Kept 索引' "$INDEX/.devlog/devlog.md" | head -1 | cut -d: -f1)"
99
+ PREV_LINE="$(sed -n "$((HEADING_LINE - 1))p" "$INDEX/.devlog/devlog.md")"
100
+ PREV2_LINE="$(sed -n "$((HEADING_LINE - 2))p" "$INDEX/.devlog/devlog.md")"
101
+ if [ -z "$PREV_LINE" ] && [ -n "$PREV2_LINE" ]; then
102
+ echo "PASS: exactly one blank line separates content from ## Kept 索引 after three keep-move.sh calls"
103
+ else
104
+ echo "FAIL: expected exactly one blank line before ## Kept 索引 after three keep-move.sh calls, got [$PREV_LINE] then [$PREV2_LINE]"
105
+ FAIL=1
106
+ fi
107
+
108
+ DESC="$TMP/desc"
109
+ mkdir -p "$DESC/.devlog"
110
+ export CLAUDE_PROJECT_DIR="$DESC"
111
+ printf '# project\n\n' > "$DESC/.devlog/devlog.md"
112
+ make_round "$DESC/.devlog/devlog.md" 1 DONE
113
+ make_round "$DESC/.devlog/devlog.md" 2 DONE
114
+ make_round "$DESC/.devlog/devlog.md" 3 IN_PROGRESS
115
+ printf '%s\n' '{"round": 3, "opened_at": "now"}' > "$DESC/.devlog/.round-open"
116
+ bash "$SCRIPT_DIR/keep-move.sh" --from 1 --to 1 --name with-desc --desc "調整 span 模式的門檻" >/dev/null
117
+ grep -q '調整 span 模式的門檻' "$DESC/.devlog/devlog.md" && echo "PASS: --desc text written into Kept 索引 line" || { echo "FAIL: expected desc text in index line"; FAIL=1; }
118
+ bash "$SCRIPT_DIR/keep-move.sh" --from 2 --to 2 --name no-desc >/dev/null
119
+ LINE_NO_DESC="$(grep 'devlog.no-desc.md' "$DESC/.devlog/devlog.md")"
120
+ case "$LINE_NO_DESC" in
121
+ *"kept_at "*","*) echo "FAIL: unexpected extra trailing segment without --desc: $LINE_NO_DESC"; FAIL=1 ;;
122
+ *"kept_at "*) echo "PASS: omitting --desc keeps the plain kept_at-only line" ;;
123
+ *) echo "FAIL: index line for no-desc missing: $LINE_NO_DESC"; FAIL=1 ;;
124
+ esac
125
+
126
+ if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
127
+ else echo "Some checks FAILED."; exit 1; fi
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env bash
2
+ # Self-check for kept-list.sh (docs/design/keep.md "Kept index" +
3
+ # /devlog-tracker:overview). Plain read only -- no workspace verification.
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
+ DEVLOG_DIR="$TMP_ROOT/.devlog"
10
+ mkdir -p "$DEVLOG_DIR"
11
+ FAIL=0
12
+ assert_exit() {
13
+ local desc="$1" expected="$2" actual="$3"
14
+ if [ "$actual" -eq "$expected" ]; then echo "PASS: $desc"
15
+ else echo "FAIL: $desc (expected $expected got $actual)"; FAIL=1; fi
16
+ }
17
+
18
+ # --- No devlog.md at all -> NO_INDEX ----------------------------------------
19
+ OUT="$(bash "$SCRIPT_DIR/kept-list.sh" 2>&1)"
20
+ assert_exit "no devlog.md -> 0" 0 $?
21
+ case "$OUT" in
22
+ *"NO_INDEX"*) echo "PASS: reports NO_INDEX when devlog.md is absent" ;;
23
+ *) echo "FAIL: expected NO_INDEX, got $OUT"; FAIL=1 ;;
24
+ esac
25
+
26
+ # --- devlog.md exists but no Kept 索引 -> NO_INDEX --------------------------
27
+ echo '## Round 1 — 2026-09-12T00:00:00+08:00
28
+
29
+ ### Summary
30
+ fixture
31
+
32
+ ### Handoff
33
+ #### 現況
34
+ fixture
35
+
36
+ ### Status
37
+ DONE' > "$DEVLOG_DIR/devlog.md"
38
+ OUT="$(bash "$SCRIPT_DIR/kept-list.sh" 2>&1)"
39
+ assert_exit "devlog.md with no index -> 0" 0 $?
40
+ case "$OUT" in
41
+ *"NO_INDEX"*) echo "PASS: reports NO_INDEX when no Kept 索引 block exists" ;;
42
+ *) echo "FAIL: expected NO_INDEX, got $OUT"; FAIL=1 ;;
43
+ esac
44
+
45
+ # --- Kept 索引 present, target file exists -> EXISTS=1 ----------------------
46
+ printf 'kept content\n' > "$DEVLOG_DIR/devlog.foo-bar.md"
47
+ printf '\n## Kept 索引\n- `devlog.foo-bar.md`:Round 1-1,kept_at 2026-09-12T00:00:00+08:00,foo bar 主題\n' >> "$DEVLOG_DIR/devlog.md"
48
+ OUT="$(bash "$SCRIPT_DIR/kept-list.sh" 2>&1)"
49
+ assert_exit "index with one existing file -> 0" 0 $?
50
+ case "$OUT" in
51
+ *"FILE="*"devlog.foo-bar.md"*"EXISTS=1"*) echo "PASS: existing kept file reported with EXISTS=1" ;;
52
+ *) echo "FAIL: expected FILE=...devlog.foo-bar.md EXISTS=1, got $OUT"; FAIL=1 ;;
53
+ esac
54
+
55
+ # --- Ghost row: index line present, file missing on disk -> EXISTS=0 -------
56
+ rm -f "$DEVLOG_DIR/devlog.foo-bar.md"
57
+ OUT="$(bash "$SCRIPT_DIR/kept-list.sh" 2>&1)"
58
+ assert_exit "ghost row -> 0" 0 $?
59
+ case "$OUT" in
60
+ *"FILE="*"devlog.foo-bar.md"*"EXISTS=0"*) echo "PASS: ghost kept file reported with EXISTS=0" ;;
61
+ *) echo "FAIL: expected FILE=...devlog.foo-bar.md EXISTS=0, got $OUT"; FAIL=1 ;;
62
+ esac
63
+
64
+ # --- Multiple entries listed, one per line -----------------------------------
65
+ printf 'kept content two\n' > "$DEVLOG_DIR/devlog.second-topic.md"
66
+ sed -i.bak '$ a\
67
+ - `devlog.second-topic.md`:Round 2-2,kept_at 2026-09-13T00:00:00+08:00
68
+ ' "$DEVLOG_DIR/devlog.md"
69
+ rm -f "$DEVLOG_DIR/devlog.md.bak"
70
+ OUT="$(bash "$SCRIPT_DIR/kept-list.sh" 2>&1)"
71
+ assert_exit "index with two entries -> 0" 0 $?
72
+ LINE_COUNT="$(printf '%s\n' "$OUT" | grep -c '^FILE=')"
73
+ [ "$LINE_COUNT" -eq 2 ] && echo "PASS: two FILE= lines printed for two index entries" || { echo "FAIL: expected 2 FILE= lines, got $LINE_COUNT"; FAIL=1; }
74
+ case "$OUT" in
75
+ *"devlog.second-topic.md"*"EXISTS=1"*) echo "PASS: second-topic file reported with EXISTS=1" ;;
76
+ *) echo "FAIL: expected devlog.second-topic.md EXISTS=1, got $OUT"; FAIL=1 ;;
77
+ esac
78
+
79
+ if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
80
+ else echo "Some checks FAILED."; exit 1; fi
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env bash
2
+ # Self-check for lessons-append.sh (docs/design/lessons-mode.md "Storage" /
3
+ # "## Lessons 索引" / "Write mechanism"). No framework, plain
4
+ # assert-and-exit, matching this repo's existing style.
5
+ set -uo pipefail
6
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
7
+ TMP_ROOT="$(mktemp -d)"
8
+ trap 'rm -rf "$TMP_ROOT"' EXIT
9
+ export CLAUDE_PROJECT_DIR="$TMP_ROOT"
10
+ DEVLOG_DIR="$TMP_ROOT/.devlog"
11
+ mkdir -p "$DEVLOG_DIR"
12
+ FAIL=0
13
+ assert_exit() {
14
+ local desc="$1" expected="$2" actual="$3"
15
+ if [ "$actual" -eq "$expected" ]; then echo "PASS: $desc"
16
+ else echo "FAIL: $desc (expected $expected got $actual)"; FAIL=1; fi
17
+ }
18
+
19
+ echo '## Round 1 — 2026-09-12T00:00:00+08:00
20
+
21
+ ### Summary
22
+ fixture
23
+
24
+ ### Reply
25
+ fixture reply.
26
+
27
+ ### Handoff
28
+ #### 現況
29
+ fixture
30
+
31
+ ### Status
32
+ DONE' > "$DEVLOG_DIR/devlog.md"
33
+
34
+ # --- Refused without the main switch ---------------------------------------
35
+ OUT="$(bash "$SCRIPT_DIR/lessons-append.sh" --topic foo --text "x" 2>&1)"
36
+ assert_exit "no .enabled -> exit 1" 1 $?
37
+ case "$OUT" in
38
+ *"NOT_ENABLED"*) echo "PASS: reports NOT_ENABLED" ;;
39
+ *) echo "FAIL: expected NOT_ENABLED, got $OUT"; FAIL=1 ;;
40
+ esac
41
+
42
+ touch "$DEVLOG_DIR/.enabled"
43
+
44
+ # --- Refused without lessons-enabled ---------------------------------------
45
+ OUT="$(bash "$SCRIPT_DIR/lessons-append.sh" --topic foo --text "x" 2>&1)"
46
+ assert_exit "enabled but lessons off -> exit 1" 1 $?
47
+ case "$OUT" in
48
+ *"LESSONS_NOT_ENABLED"*) echo "PASS: reports LESSONS_NOT_ENABLED" ;;
49
+ *) echo "FAIL: expected LESSONS_NOT_ENABLED, got $OUT"; FAIL=1 ;;
50
+ esac
51
+
52
+ touch "$DEVLOG_DIR/.lessons-enabled"
53
+
54
+ # --- Missing --text ----------------------------------------------------------
55
+ bash "$SCRIPT_DIR/lessons-append.sh" --topic foo >/dev/null 2>&1
56
+ assert_exit "missing --text -> exit 1" 1 $?
57
+
58
+ # --- Invalid topic (path traversal) -----------------------------------------
59
+ bash "$SCRIPT_DIR/lessons-append.sh" --topic "../etc" --text "x" >/dev/null 2>&1
60
+ assert_exit "path-traversal topic -> exit 1" 1 $?
61
+
62
+ # --- Reserved topic names ----------------------------------------------------
63
+ bash "$SCRIPT_DIR/lessons-append.sh" --topic "archive" --text "x" >/dev/null 2>&1
64
+ assert_exit "topic 'archive' rejected -> exit 1" 1 $?
65
+ bash "$SCRIPT_DIR/lessons-append.sh" --topic "lessons" --text "x" >/dev/null 2>&1
66
+ assert_exit "topic 'lessons' rejected -> exit 1" 1 $?
67
+
68
+ # --- First entry creates the topic file and the header ----------------------
69
+ OUT="$(bash "$SCRIPT_DIR/lessons-append.sh" --topic "keep-move-bug" --text "第一次卡在 keep-move.sh 沒處理缺號範圍。後來改成逐一檢查連續性。")"
70
+ assert_exit "first entry -> 0" 0 $?
71
+ case "$OUT" in
72
+ *"PATH=.devlog/devlog.lessons.keep-move-bug.md"*) echo "PASS: reports PATH" ;;
73
+ *) echo "FAIL: expected PATH=..., got $OUT"; FAIL=1 ;;
74
+ esac
75
+ TARGET="$DEVLOG_DIR/devlog.lessons.keep-move-bug.md"
76
+ if [ -f "$TARGET" ]; then echo "PASS: topic file created"; else echo "FAIL: topic file missing"; FAIL=1; fi
77
+ grep -q '^# Lessons: keep-move-bug$' "$TARGET" && echo "PASS: header present" || { echo "FAIL: header missing"; FAIL=1; }
78
+ grep -q '^## 2026-\|^## [0-9]' "$TARGET" && echo "PASS: timestamped entry heading present" || { echo "FAIL: no entry heading"; FAIL=1; }
79
+ grep -q '第一次卡在 keep-move.sh' "$TARGET" && echo "PASS: entry text present" || { echo "FAIL: entry text missing"; FAIL=1; }
80
+
81
+ # --- devlog.<name>.md / .md normalization on --topic input ------------------
82
+ OUT2="$(bash "$SCRIPT_DIR/lessons-append.sh" --topic "devlog.lessons.keep-move-bug.md" --text "第二筆,測試正規化。")"
83
+ assert_exit "prefixed/suffixed topic input normalizes to the same file -> 0" 0 $?
84
+ case "$OUT2" in
85
+ *"PATH=.devlog/devlog.lessons.keep-move-bug.md"*) echo "PASS: normalized-input call reports the same PATH" ;;
86
+ *) echo "FAIL: expected PATH=.devlog/devlog.lessons.keep-move-bug.md, got: $OUT2"; FAIL=1 ;;
87
+ esac
88
+ ENTRY_COUNT="$(grep -c '^## ' "$TARGET")"
89
+ if [ "$ENTRY_COUNT" = "2" ]; then
90
+ echo "PASS: second call appended to the SAME file (normalization collapsed devlog.lessons.<x>.md to <x>)"
91
+ else
92
+ echo "FAIL: expected 2 entries in one file, got $ENTRY_COUNT (normalization likely created a second file)"
93
+ FAIL=1
94
+ fi
95
+
96
+ # --- Lessons 索引 rebuilt: one line, correct count and latest title ---------
97
+ INDEX_BLOCK="$(awk '/^## Lessons 索引/{grab=1;next} grab{print}' "$DEVLOG_DIR/devlog.md")"
98
+ case "$INDEX_BLOCK" in
99
+ *"devlog.lessons.keep-move-bug.md"*"2 則"*"第二筆,測試正規化。"*)
100
+ echo "PASS: index line has file, count=2, and latest entry's title" ;;
101
+ *)
102
+ echo "FAIL: unexpected index block: $INDEX_BLOCK"; FAIL=1 ;;
103
+ esac
104
+
105
+ # --- A second topic gets its own file and its own index line ----------------
106
+ bash "$SCRIPT_DIR/lessons-append.sh" --topic "span-mode-detour" --text "一開始想在 hook 裡分辨自動續接跟真人插話,後來確認做不到,改成明確標記。" >/dev/null
107
+ INDEX_BLOCK2="$(awk '/^## Lessons 索引/{grab=1;next} grab{print}' "$DEVLOG_DIR/devlog.md")"
108
+ LINE_COUNT="$(printf '%s\n' "$INDEX_BLOCK2" | grep -c '^- `devlog.lessons\.')"
109
+ if [ "$LINE_COUNT" = "2" ]; then
110
+ echo "PASS: index has one line per topic file (2 topics -> 2 lines)"
111
+ else
112
+ echo "FAIL: expected 2 index lines, got $LINE_COUNT: $INDEX_BLOCK2"
113
+ FAIL=1
114
+ fi
115
+ case "$INDEX_BLOCK2" in
116
+ *"devlog.lessons.span-mode-detour.md"*"1 則"*) echo "PASS: second topic's own line present" ;;
117
+ *) echo "FAIL: second topic missing from index: $INDEX_BLOCK2"; FAIL=1 ;;
118
+ esac
119
+
120
+ # --- Index rebuild never duplicates the heading -----------------------------
121
+ HEADING_COUNT="$(grep -c '^## Lessons 索引' "$DEVLOG_DIR/devlog.md")"
122
+ if [ "$HEADING_COUNT" = "1" ]; then
123
+ echo "PASS: exactly one '## Lessons 索引' heading after multiple appends"
124
+ else
125
+ echo "FAIL: expected exactly one heading, got $HEADING_COUNT"
126
+ FAIL=1
127
+ fi
128
+
129
+ # --- Title truncates at the first 。/. -------------------------------------
130
+ bash "$SCRIPT_DIR/lessons-append.sh" --topic "punct" --text "先卡住了。後來才發現是路徑問題,多寫了一段沒用的重試邏輯。" >/dev/null
131
+ INDEX_BLOCK3="$(awk '/^## Lessons 索引/{grab=1;next} grab{print}' "$DEVLOG_DIR/devlog.md")"
132
+ case "$INDEX_BLOCK3" in
133
+ *"「先卡住了。」"*) echo "PASS: title truncated at first 。" ;;
134
+ *) echo "FAIL: expected title truncated at first 。, got: $INDEX_BLOCK3"; FAIL=1 ;;
135
+ esac
136
+
137
+ if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
138
+ else echo "Some checks FAILED."; exit 1; fi
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
4
+ FAIL=0
5
+ TMP="$(mktemp -d)"
6
+ trap 'rm -rf "$TMP"' EXIT
7
+
8
+ OUT="$(CLAUDE_PROJECT_DIR="$TMP" bash "$SCRIPT_DIR/lessons-drift-set.sh" 5)"
9
+ [ "$OUT" = "NOT_STARTED" ] && echo "PASS: not started" || { echo "FAIL: $OUT"; FAIL=1; }
10
+
11
+ mkdir -p "$TMP/.devlog"
12
+ touch "$TMP/.devlog/.enabled"
13
+ OUT="$(CLAUDE_PROJECT_DIR="$TMP" bash "$SCRIPT_DIR/lessons-drift-set.sh" 5)"
14
+ [ "$OUT" = "LESSONS_NOT_ENABLED" ] && echo "PASS: lessons not enabled" || { echo "FAIL: $OUT"; FAIL=1; }
15
+
16
+ touch "$TMP/.devlog/.lessons-enabled"
17
+ printf '%s\n' '{"mismatch_count": 2, "threshold": 3}' > "$TMP/.devlog/.lessons-drift-state"
18
+
19
+ OUT="$(CLAUDE_PROJECT_DIR="$TMP" bash "$SCRIPT_DIR/lessons-drift-set.sh" 5)"
20
+ case "$OUT" in *LESSONS_DRIFT_THRESHOLD=5*) echo "PASS: set 5" ;; *) echo "FAIL: $OUT"; FAIL=1 ;; esac
21
+ grep -q '"mismatch_count": 2' "$TMP/.devlog/.lessons-drift-state" \
22
+ && echo "PASS: count preserved" || { echo "FAIL: count reset"; FAIL=1; }
23
+ grep -q '"threshold": 5' "$TMP/.devlog/.lessons-drift-state" \
24
+ && echo "PASS: threshold written" || { echo "FAIL: threshold"; FAIL=1; }
25
+
26
+ CLAUDE_PROJECT_DIR="$TMP" bash "$SCRIPT_DIR/lessons-drift-set.sh" 0 >/dev/null 2>&1
27
+ [ $? -eq 1 ] && echo "PASS: reject 0" || { echo "FAIL: accepted 0"; FAIL=1; }
28
+
29
+ rm -f "$TMP/.devlog/.lessons-drift-state"
30
+ OUT="$(CLAUDE_PROJECT_DIR="$TMP" bash "$SCRIPT_DIR/lessons-drift-set.sh" 7)"
31
+ case "$OUT" in *LESSONS_DRIFT_THRESHOLD=7*) echo "PASS: create 7" ;; *) echo "FAIL: create $OUT"; FAIL=1 ;; esac
32
+ grep -q '"mismatch_count": 0' "$TMP/.devlog/.lessons-drift-state" \
33
+ && echo "PASS: create count 0" || { echo "FAIL: create count"; FAIL=1; }
34
+
35
+ if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
36
+ else echo "Some checks FAILED."; exit 1; fi