devlog-tracker 0.29.2 → 0.31.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/README.md +23 -7
- package/README.zh-TW.md +23 -7
- package/bin/devlog-tracker.js +14 -1
- package/claude/hooks.json +21 -0
- package/cli/agents-md.js +3 -0
- package/cli/agents-md.test.js +32 -0
- package/cli/core-script.js +25 -0
- package/cli/core-script.test.js +62 -0
- package/cli/platforms/claude.js +3 -0
- package/commands/overview.md +1 -1
- package/commands/pr.md +61 -0
- package/commands/promote.md +54 -0
- package/commands/report.md +23 -0
- package/commands/timeline.md +18 -0
- package/core/scripts/json-field.sh +16 -0
- package/core/scripts/lessons-subagent-done.sh +36 -0
- package/core/scripts/lessons-subagent-start.sh +28 -0
- package/core/scripts/pr-context.sh +64 -0
- package/core/scripts/promote-sources.sh +40 -0
- package/core/scripts/promote-target.sh +37 -0
- package/core/scripts/promote-write.sh +74 -0
- package/core/scripts/report-devlog.sh +140 -0
- package/core/scripts/report-scan.awk +74 -0
- package/core/scripts/round-start.sh +15 -1
- package/core/scripts/tests/test-json-field.sh +5 -0
- package/core/scripts/tests/test-lessons-subagent-hooks.sh +95 -0
- package/core/scripts/tests/test-pr-context.sh +107 -0
- package/core/scripts/tests/test-promote.sh +158 -0
- package/core/scripts/tests/test-report.sh +197 -0
- package/core/scripts/tests/test-round-start.sh +65 -0
- package/core/scripts/tests/test-timeline.sh +65 -0
- package/core/scripts/timeline-devlog.sh +41 -0
- package/core/scripts/timeline-render.js +247 -0
- package/core/scripts/timeline-render.test.js +101 -0
- package/package.json +2 -2
- package/skills/devlog-tracker/SKILL.md +5 -3
- package/skills/devlog-tracker/references/lessons-mode.md +10 -4
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Self-check for pr-context.sh (docs/design/read-side-and-promote.md A).
|
|
3
|
+
set -uo pipefail
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
TMP_ROOT="$(mktemp -d)"
|
|
6
|
+
trap 'rm -rf "$TMP_ROOT"' EXIT
|
|
7
|
+
FAIL=0
|
|
8
|
+
assert_line() {
|
|
9
|
+
local desc="$1" expected="$2" out="$3"
|
|
10
|
+
if printf '%s\n' "$out" | grep -qxF "$expected"; then echo "PASS: $desc"
|
|
11
|
+
else echo "FAIL: $desc (missing [$expected] in: $out)"; FAIL=1; fi
|
|
12
|
+
}
|
|
13
|
+
assert_eq() {
|
|
14
|
+
local desc="$1" expected="$2" actual="$3"
|
|
15
|
+
if [ "$expected" = "$actual" ]; then echo "PASS: $desc"
|
|
16
|
+
else echo "FAIL: $desc (expected [$expected] got [$actual])"; FAIL=1; fi
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# gh stubs: one that is not logged in, one that is and has PR #42.
|
|
20
|
+
GH_OFF="$TMP_ROOT/gh-off"; GH_ON="$TMP_ROOT/gh-on"
|
|
21
|
+
mkdir -p "$GH_OFF" "$GH_ON"
|
|
22
|
+
printf '#!/usr/bin/env bash\nexit 1\n' > "$GH_OFF/gh"
|
|
23
|
+
cat > "$GH_ON/gh" <<'EOF'
|
|
24
|
+
#!/usr/bin/env bash
|
|
25
|
+
case "$1 $2" in
|
|
26
|
+
"auth status") exit 0 ;;
|
|
27
|
+
"pr view") echo 42 ;;
|
|
28
|
+
*) echo "unexpected gh call: $*" >&2; exit 9 ;;
|
|
29
|
+
esac
|
|
30
|
+
EOF
|
|
31
|
+
chmod +x "$GH_OFF/gh" "$GH_ON/gh"
|
|
32
|
+
ctx() { PATH="$1:$PATH" CLAUDE_PROJECT_DIR="$2" bash "$SCRIPT_DIR/pr-context.sh"; }
|
|
33
|
+
|
|
34
|
+
# --- Not a git repo -------------------------------------------------------------
|
|
35
|
+
PLAIN="$TMP_ROOT/plain"; mkdir -p "$PLAIN"
|
|
36
|
+
assert_eq "not a repo" "NOT_A_REPO" "$(ctx "$GH_OFF" "$PLAIN")"
|
|
37
|
+
|
|
38
|
+
# --- Repo fixture ------------------------------------------------------------------
|
|
39
|
+
REPO="$TMP_ROOT/repo"; mkdir -p "$REPO"
|
|
40
|
+
gitc() { git -C "$REPO" -c user.email=t@example.com -c user.name=t -c commit.gpgsign=false "$@"; }
|
|
41
|
+
gitc init -q -b main
|
|
42
|
+
echo a > "$REPO/a.txt"; gitc add a.txt; gitc commit -qm init
|
|
43
|
+
|
|
44
|
+
assert_eq "on main" "ON_DEFAULT_BRANCH" "$(ctx "$GH_OFF" "$REPO")"
|
|
45
|
+
gitc checkout -q -b MASTER
|
|
46
|
+
assert_eq "on MASTER (case-insensitive)" "ON_DEFAULT_BRANCH" "$(ctx "$GH_OFF" "$REPO")"
|
|
47
|
+
gitc checkout -q main
|
|
48
|
+
gitc checkout -q --detach
|
|
49
|
+
assert_eq "detached" "DETACHED_HEAD" "$(ctx "$GH_OFF" "$REPO")"
|
|
50
|
+
gitc checkout -q main
|
|
51
|
+
|
|
52
|
+
# --- Feature branch, no devlog, gh logged out --------------------------------------
|
|
53
|
+
gitc checkout -q -b feat/x
|
|
54
|
+
echo b > "$REPO/b.txt"; gitc add b.txt; gitc commit -qm "feat: b"
|
|
55
|
+
OUT="$(ctx "$GH_OFF" "$REPO")"
|
|
56
|
+
assert_line "branch" "BRANCH=feat/x" "$OUT"
|
|
57
|
+
assert_line "base from local main" "BASE=main" "$OUT"
|
|
58
|
+
assert_line "base ref" "BASE_REF=main" "$OUT"
|
|
59
|
+
assert_line "devlog file is branch-scoped + absolute" "DEVLOG_FILE=$(cd "$REPO" && pwd)/.devlog/devlog.feat-x.md" "$OUT"
|
|
60
|
+
assert_line "no branch devlog" "NO_BRANCH_DEVLOG" "$OUT"
|
|
61
|
+
assert_line "commit count" "COMMITS=1" "$OUT"
|
|
62
|
+
assert_line "gh logged out" "GH=no" "$OUT"
|
|
63
|
+
assert_line "no pr" "PR=none" "$OUT"
|
|
64
|
+
|
|
65
|
+
# --- Branch devlog with rounds + gh logged in -------------------------------------
|
|
66
|
+
mkdir -p "$REPO/.devlog"
|
|
67
|
+
cat > "$REPO/.devlog/devlog.feat-x.md" <<'EOF'
|
|
68
|
+
## Round 1 — 2026-09-01T10:00:00+0800
|
|
69
|
+
|
|
70
|
+
### Summary
|
|
71
|
+
one
|
|
72
|
+
|
|
73
|
+
### Status
|
|
74
|
+
DONE
|
|
75
|
+
|
|
76
|
+
## Round 2 — 2026-09-02T10:00:00+0800
|
|
77
|
+
|
|
78
|
+
### Summary
|
|
79
|
+
two
|
|
80
|
+
|
|
81
|
+
### Status
|
|
82
|
+
DONE
|
|
83
|
+
EOF
|
|
84
|
+
OUT="$(ctx "$GH_ON" "$REPO")"
|
|
85
|
+
assert_line "round start lines" "ROUNDS=1 9" "$OUT"
|
|
86
|
+
assert_line "gh logged in" "GH=yes" "$OUT"
|
|
87
|
+
assert_line "existing pr number" "PR=42" "$OUT"
|
|
88
|
+
|
|
89
|
+
# --- origin/HEAD wins over local main ---------------------------------------------
|
|
90
|
+
gitc update-ref refs/remotes/origin/main main
|
|
91
|
+
gitc symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main
|
|
92
|
+
OUT="$(ctx "$GH_OFF" "$REPO")"
|
|
93
|
+
assert_line "base from origin/HEAD" "BASE=main" "$OUT"
|
|
94
|
+
assert_line "base ref is remote" "BASE_REF=origin/main" "$OUT"
|
|
95
|
+
|
|
96
|
+
# --- No main/master and no origin/HEAD -> NO_BASE ------------------------------
|
|
97
|
+
REPO2="$TMP_ROOT/repo2"; mkdir -p "$REPO2"
|
|
98
|
+
git -C "$REPO2" -c user.email=t@example.com -c user.name=t init -q -b trunk
|
|
99
|
+
echo a > "$REPO2/a.txt"; git -C "$REPO2" add a.txt
|
|
100
|
+
git -C "$REPO2" -c user.email=t@example.com -c user.name=t -c commit.gpgsign=false commit -qm init
|
|
101
|
+
git -C "$REPO2" checkout -q -b dev
|
|
102
|
+
OUT="$(ctx "$GH_OFF" "$REPO2")"
|
|
103
|
+
assert_line "no base: branch still printed" "BRANCH=dev" "$OUT"
|
|
104
|
+
assert_line "no base" "NO_BASE" "$OUT"
|
|
105
|
+
|
|
106
|
+
if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
|
|
107
|
+
else echo "Some checks FAILED."; exit 1; fi
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Self-check for promote-{sources,target,write}.sh
|
|
3
|
+
# (docs/design/read-side-and-promote.md D).
|
|
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
|
+
FAIL=0
|
|
9
|
+
assert_line() {
|
|
10
|
+
local desc="$1" expected="$2" out="$3"
|
|
11
|
+
if printf '%s\n' "$out" | grep -qxF "$expected"; then echo "PASS: $desc"
|
|
12
|
+
else echo "FAIL: $desc (missing [$expected] in: $out)"; FAIL=1; fi
|
|
13
|
+
}
|
|
14
|
+
assert_eq() {
|
|
15
|
+
local desc="$1" expected="$2" actual="$3"
|
|
16
|
+
if [ "$expected" = "$actual" ]; then echo "PASS: $desc"
|
|
17
|
+
else echo "FAIL: $desc (expected [$expected] got [$actual])"; FAIL=1; fi
|
|
18
|
+
}
|
|
19
|
+
new_project() { local d; d="$(mktemp -d "$TMP_ROOT/p.XXXXXX")"; (cd "$d" && pwd); }
|
|
20
|
+
|
|
21
|
+
# === promote-sources.sh =========================================================
|
|
22
|
+
P="$(new_project)"
|
|
23
|
+
assert_eq "sources: no devlog -> NO_SOURCES" "NO_SOURCES" "$(CLAUDE_PROJECT_DIR="$P" bash "$SCRIPT_DIR/promote-sources.sh")"
|
|
24
|
+
|
|
25
|
+
mkdir -p "$P/.devlog"
|
|
26
|
+
cat > "$P/.devlog/devlog.md" <<'EOF'
|
|
27
|
+
## Round 1 — 2026-09-01T10:00:00+0800
|
|
28
|
+
|
|
29
|
+
### Summary
|
|
30
|
+
x
|
|
31
|
+
|
|
32
|
+
### Status
|
|
33
|
+
DONE
|
|
34
|
+
EOF
|
|
35
|
+
assert_eq "sources: devlog without index/checkpoint -> NO_SOURCES" "NO_SOURCES" "$(CLAUDE_PROJECT_DIR="$P" bash "$SCRIPT_DIR/promote-sources.sh")"
|
|
36
|
+
|
|
37
|
+
cat >> "$P/.devlog/devlog.md" <<'EOF'
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
## Checkpoint fake inside fence
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Checkpoint(Round 1-1)
|
|
44
|
+
### 決策
|
|
45
|
+
- always run shellcheck
|
|
46
|
+
|
|
47
|
+
## Kept 索引
|
|
48
|
+
- `devlog.topic-a.md`:Round 1-1,kept_at 2026-09-05T00:00:00+0800,topic a
|
|
49
|
+
- `devlog.ghost.md`:Round 2-2,kept_at 2026-09-05T00:00:00+0800,gone
|
|
50
|
+
|
|
51
|
+
## Lessons 索引
|
|
52
|
+
- `devlog.lessons.lock-files.md`:1 則,最新一則「x。」(updated_at 2026-09-01T00:00:00+0800)
|
|
53
|
+
EOF
|
|
54
|
+
printf 'kept\n' > "$P/.devlog/devlog.topic-a.md"
|
|
55
|
+
printf '# Lessons: lock-files\n' > "$P/.devlog/devlog.lessons.lock-files.md"
|
|
56
|
+
OUT="$(CLAUDE_PROJECT_DIR="$P" bash "$SCRIPT_DIR/promote-sources.sh")"
|
|
57
|
+
assert_line "sources: kept file exists" "FILE=$P/.devlog/devlog.topic-a.md KIND=kept EXISTS=1" "$OUT"
|
|
58
|
+
assert_line "sources: kept ghost row" "FILE=$P/.devlog/devlog.ghost.md KIND=kept EXISTS=0" "$OUT"
|
|
59
|
+
assert_line "sources: lessons file" "FILE=$P/.devlog/devlog.lessons.lock-files.md KIND=lessons EXISTS=1" "$OUT"
|
|
60
|
+
CP_LINE="$(grep -n '^## Checkpoint(Round 1-1)' "$P/.devlog/devlog.md" | cut -d: -f1)"
|
|
61
|
+
assert_line "sources: real checkpoint line" "CHECKPOINT=$P/.devlog/devlog.md:$CP_LINE" "$OUT"
|
|
62
|
+
assert_eq "sources: fenced checkpoint ignored" "1" "$(printf '%s\n' "$OUT" | grep -c '^CHECKPOINT=')"
|
|
63
|
+
|
|
64
|
+
# === promote-target.sh ==========================================================
|
|
65
|
+
P="$(new_project)"
|
|
66
|
+
assert_eq "target: empty project -> CLAUDE.md" "TARGET=$P/CLAUDE.md" "$(CLAUDE_PROJECT_DIR="$P" bash "$SCRIPT_DIR/promote-target.sh")"
|
|
67
|
+
|
|
68
|
+
printf '@AGENTS.md\n\n' > "$P/CLAUDE.md"
|
|
69
|
+
assert_eq "target: CLAUDE.md only imports AGENTS.md -> AGENTS.md" "TARGET=$P/AGENTS.md" "$(CLAUDE_PROJECT_DIR="$P" bash "$SCRIPT_DIR/promote-target.sh")"
|
|
70
|
+
|
|
71
|
+
printf '# Rules\n\n@AGENTS.md\n' > "$P/CLAUDE.md"
|
|
72
|
+
assert_eq "target: CLAUDE.md with own content -> CLAUDE.md" "TARGET=$P/CLAUDE.md" "$(CLAUDE_PROJECT_DIR="$P" bash "$SCRIPT_DIR/promote-target.sh")"
|
|
73
|
+
|
|
74
|
+
P="$(new_project)"
|
|
75
|
+
mkdir -p "$P/.codex"; printf '{}\n' > "$P/.codex/hooks.json"
|
|
76
|
+
assert_eq "target: codex-only (hooks.json) -> AGENTS.md" "TARGET=$P/AGENTS.md" "$(CLAUDE_PROJECT_DIR="$P" bash "$SCRIPT_DIR/promote-target.sh")"
|
|
77
|
+
|
|
78
|
+
P="$(new_project)"
|
|
79
|
+
mkdir -p "$P/.agents/skills/devlog-start"
|
|
80
|
+
assert_eq "target: codex-only (skills) -> AGENTS.md" "TARGET=$P/AGENTS.md" "$(CLAUDE_PROJECT_DIR="$P" bash "$SCRIPT_DIR/promote-target.sh")"
|
|
81
|
+
|
|
82
|
+
P="$(new_project)"
|
|
83
|
+
printf '# Mine\n\n<!-- devlog-tracker:rules:begin -->\n## devlog-tracker 沉澱的規範\n\n- a\n- b\n<!-- devlog-tracker:rules:end -->\n' > "$P/CLAUDE.md"
|
|
84
|
+
OUT="$(CLAUDE_PROJECT_DIR="$P" bash "$SCRIPT_DIR/promote-target.sh")"
|
|
85
|
+
assert_line "target: existing block counted" "EXISTING=2" "$OUT"
|
|
86
|
+
|
|
87
|
+
# === promote-write.sh ==========================================================
|
|
88
|
+
BEGIN_MARK='<!-- devlog-tracker:rules:begin -->'
|
|
89
|
+
END_MARK='<!-- devlog-tracker:rules:end -->'
|
|
90
|
+
P="$(new_project)"
|
|
91
|
+
RULES="$TMP_ROOT/rules.txt"
|
|
92
|
+
|
|
93
|
+
# --- target missing -> created with header ---------------------------------------
|
|
94
|
+
printf ' 改核心腳本後跑 run-tests.sh \n\n- 一般 PR 不改版號\n' > "$RULES"
|
|
95
|
+
OUT="$(CLAUDE_PROJECT_DIR="$P" bash "$SCRIPT_DIR/promote-write.sh" "$P/CLAUDE.md" "$RULES")"
|
|
96
|
+
assert_eq "write: new file result" "ADDED=2 SKIPPED_DUP=0 TARGET=$P/CLAUDE.md" "$OUT"
|
|
97
|
+
EXPECTED="$(printf '%s\n## devlog-tracker 沉澱的規範\n\n- 改核心腳本後跑 run-tests.sh\n- 一般 PR 不改版號\n%s' "$BEGIN_MARK" "$END_MARK")"
|
|
98
|
+
assert_eq "write: new file content (trimmed, '- ' prefixed, blank skipped)" "$EXPECTED" "$(cat "$P/CLAUDE.md")"
|
|
99
|
+
|
|
100
|
+
# --- existing content without block -> appended after a blank line -------------------
|
|
101
|
+
P="$(new_project)"
|
|
102
|
+
printf '# Mine\nkeep me' > "$P/CLAUDE.md"
|
|
103
|
+
printf 'rule one\n' > "$RULES"
|
|
104
|
+
CLAUDE_PROJECT_DIR="$P" bash "$SCRIPT_DIR/promote-write.sh" "$P/CLAUDE.md" "$RULES" >/dev/null
|
|
105
|
+
EXPECTED="$(printf '# Mine\nkeep me\n\n%s\n## devlog-tracker 沉澱的規範\n\n- rule one\n%s' "$BEGIN_MARK" "$END_MARK")"
|
|
106
|
+
assert_eq "write: appended after existing content (missing trailing newline handled)" "$EXPECTED" "$(cat "$P/CLAUDE.md")"
|
|
107
|
+
|
|
108
|
+
# --- existing block -> inserted before end mark; content after block kept -----------
|
|
109
|
+
printf '\n## After\ntail\n' >> "$P/CLAUDE.md"
|
|
110
|
+
printf 'rule two\n' > "$RULES"
|
|
111
|
+
CLAUDE_PROJECT_DIR="$P" bash "$SCRIPT_DIR/promote-write.sh" "$P/CLAUDE.md" "$RULES" >/dev/null
|
|
112
|
+
EXPECTED="$(printf '# Mine\nkeep me\n\n%s\n## devlog-tracker 沉澱的規範\n\n- rule one\n- rule two\n%s\n\n## After\ntail' "$BEGIN_MARK" "$END_MARK")"
|
|
113
|
+
assert_eq "write: appended inside existing block" "$EXPECTED" "$(cat "$P/CLAUDE.md")"
|
|
114
|
+
|
|
115
|
+
# --- exact duplicates (existing + within input) skipped; file unchanged -------------
|
|
116
|
+
BEFORE="$(cat "$P/CLAUDE.md")"
|
|
117
|
+
printf -- '- rule one\nrule two\n' > "$RULES"
|
|
118
|
+
OUT="$(CLAUDE_PROJECT_DIR="$P" bash "$SCRIPT_DIR/promote-write.sh" "$P/CLAUDE.md" "$RULES")"
|
|
119
|
+
assert_eq "write: all duplicates" "ADDED=0 SKIPPED_DUP=2 TARGET=$P/CLAUDE.md" "$OUT"
|
|
120
|
+
assert_eq "write: file unchanged on all-dup" "$BEFORE" "$(cat "$P/CLAUDE.md")"
|
|
121
|
+
printf 'rule three\nrule three\n' > "$RULES"
|
|
122
|
+
OUT="$(CLAUDE_PROJECT_DIR="$P" bash "$SCRIPT_DIR/promote-write.sh" "$P/CLAUDE.md" "$RULES")"
|
|
123
|
+
assert_eq "write: duplicate within input counted once" "ADDED=1 SKIPPED_DUP=1 TARGET=$P/CLAUDE.md" "$OUT"
|
|
124
|
+
|
|
125
|
+
# --- symlink target: write-in-place preserves the link, rules land in the real file --
|
|
126
|
+
P="$(new_project)"
|
|
127
|
+
printf '# Base\n' > "$P/AGENTS.md"
|
|
128
|
+
ln -s AGENTS.md "$P/CLAUDE.md"
|
|
129
|
+
printf 'rule via symlink\n' > "$RULES"
|
|
130
|
+
CLAUDE_PROJECT_DIR="$P" bash "$SCRIPT_DIR/promote-write.sh" "$P/CLAUDE.md" "$RULES" >/dev/null
|
|
131
|
+
if [ -L "$P/CLAUDE.md" ]; then echo "PASS: write: symlink target stays a symlink"
|
|
132
|
+
else echo "FAIL: write: symlink was replaced by a regular file"; FAIL=1; fi
|
|
133
|
+
if grep -qF "rule via symlink" "$P/AGENTS.md"; then echo "PASS: write: rules land in the symlink's real target"
|
|
134
|
+
else echo "FAIL: write: rules missing from AGENTS.md"; FAIL=1; fi
|
|
135
|
+
|
|
136
|
+
# --- mode is preserved on a plain (non-symlink) target -------------------------------
|
|
137
|
+
P="$(new_project)"
|
|
138
|
+
printf '# Mode\n' > "$P/CLAUDE.md"
|
|
139
|
+
chmod 640 "$P/CLAUDE.md"
|
|
140
|
+
printf 'rule mode\n' > "$RULES"
|
|
141
|
+
CLAUDE_PROJECT_DIR="$P" bash "$SCRIPT_DIR/promote-write.sh" "$P/CLAUDE.md" "$RULES" >/dev/null
|
|
142
|
+
assert_eq "write: mode preserved (640)" "-rw-r-----" "$(ls -l "$P/CLAUDE.md" | cut -c1-10)"
|
|
143
|
+
|
|
144
|
+
# --- lock released ------------------------------------------------------------------
|
|
145
|
+
mkdir -p "$P/.devlog"
|
|
146
|
+
CLAUDE_PROJECT_DIR="$P" bash "$SCRIPT_DIR/promote-write.sh" "$P/CLAUDE.md" "$RULES" >/dev/null
|
|
147
|
+
[ ! -e "$P/.devlog/.lock" ] && echo "PASS: write: lock released" || { echo "FAIL: write: .lock left behind"; FAIL=1; }
|
|
148
|
+
|
|
149
|
+
# --- usage errors ---------------------------------------------------------------------
|
|
150
|
+
bash "$SCRIPT_DIR/promote-write.sh" >/dev/null 2>&1
|
|
151
|
+
RC=$?
|
|
152
|
+
assert_eq "write: no args exits 2" "2" "$RC"
|
|
153
|
+
bash "$SCRIPT_DIR/promote-write.sh" "$P/CLAUDE.md" "$TMP_ROOT/missing.txt" >/dev/null 2>&1
|
|
154
|
+
RC=$?
|
|
155
|
+
assert_eq "write: missing rules file exits 2" "2" "$RC"
|
|
156
|
+
|
|
157
|
+
if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
|
|
158
|
+
else echo "Some checks FAILED."; exit 1; fi
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Self-check for report-devlog.sh (docs/design/read-side-and-promote.md B).
|
|
3
|
+
set -uo pipefail
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
TMP_ROOT="$(mktemp -d)"
|
|
6
|
+
trap 'rm -rf "$TMP_ROOT"' EXIT
|
|
7
|
+
export CLAUDE_PROJECT_DIR="$TMP_ROOT"
|
|
8
|
+
DEVLOG_DIR="$TMP_ROOT/.devlog"
|
|
9
|
+
FAIL=0
|
|
10
|
+
assert_line() {
|
|
11
|
+
local desc="$1" expected="$2" out="$3"
|
|
12
|
+
if printf '%s\n' "$out" | grep -qxF "$expected"; then echo "PASS: $desc"
|
|
13
|
+
else echo "FAIL: $desc (missing line [$expected] in: $out)"; FAIL=1; fi
|
|
14
|
+
}
|
|
15
|
+
assert_no_line_prefix() {
|
|
16
|
+
local desc="$1" prefix="$2" out="$3"
|
|
17
|
+
if printf '%s\n' "$out" | grep -q "^$prefix"; then echo "FAIL: $desc (found $prefix)"; FAIL=1
|
|
18
|
+
else echo "PASS: $desc"; fi
|
|
19
|
+
}
|
|
20
|
+
jcheck() {
|
|
21
|
+
local desc="$1" json="$2" expr="$3"
|
|
22
|
+
if ! command -v node >/dev/null 2>&1; then echo "SKIP: $desc (no node)"; return 0; fi
|
|
23
|
+
if printf '%s' "$json" | node -e "const d=JSON.parse(require('fs').readFileSync(0,'utf8')); process.exit(($expr)?0:1)"; then
|
|
24
|
+
echo "PASS: $desc"
|
|
25
|
+
else echo "FAIL: $desc"; FAIL=1; fi
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# --- No .devlog -> NOT_STARTED ----------------------------------------------
|
|
29
|
+
OUT="$(bash "$SCRIPT_DIR/report-devlog.sh")"
|
|
30
|
+
assert_line "no .devlog -> NOT_STARTED" "NOT_STARTED" "$OUT"
|
|
31
|
+
|
|
32
|
+
# --- Fixture ------------------------------------------------------------------
|
|
33
|
+
mkdir -p "$DEVLOG_DIR"
|
|
34
|
+
{
|
|
35
|
+
cat <<'EOF'
|
|
36
|
+
## Round 1 — 2026-09-01T10:00:00+0800
|
|
37
|
+
|
|
38
|
+
### User Input
|
|
39
|
+
secret-input-text
|
|
40
|
+
|
|
41
|
+
### Summary
|
|
42
|
+
EOF
|
|
43
|
+
printf 'first "quoted" \\ back\ttab 中文\n'
|
|
44
|
+
cat <<'EOF'
|
|
45
|
+
|
|
46
|
+
### Handoff
|
|
47
|
+
#### 現況
|
|
48
|
+
x
|
|
49
|
+
|
|
50
|
+
### Status
|
|
51
|
+
DONE
|
|
52
|
+
|
|
53
|
+
## Round 2 — 2026-09-02T10:00:00+0800
|
|
54
|
+
|
|
55
|
+
### Summary
|
|
56
|
+
blocked one
|
|
57
|
+
```
|
|
58
|
+
## Round 9 fake inside fence
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Status
|
|
62
|
+
BLOCKED
|
|
63
|
+
|
|
64
|
+
## Checkpoint(Round 1-2)
|
|
65
|
+
### 決策
|
|
66
|
+
- keep it
|
|
67
|
+
|
|
68
|
+
## Round 3 — 2026-09-03T10:00:00+0800
|
|
69
|
+
|
|
70
|
+
### Summary
|
|
71
|
+
in progress
|
|
72
|
+
|
|
73
|
+
### 段落 探索
|
|
74
|
+
seg body
|
|
75
|
+
|
|
76
|
+
### Status
|
|
77
|
+
INTERRUPTED
|
|
78
|
+
[reason: dangling:next_prompt]
|
|
79
|
+
|
|
80
|
+
## Kept 索引
|
|
81
|
+
- `devlog.topic-a.md`:Round 5-5,kept_at 2026-09-05T00:00:00+0800,topic a
|
|
82
|
+
EOF
|
|
83
|
+
} > "$DEVLOG_DIR/devlog.md"
|
|
84
|
+
cat > "$DEVLOG_DIR/devlog.archive.md" <<'EOF'
|
|
85
|
+
## Round 0 — 2026-08-31T10:00:00+0800
|
|
86
|
+
|
|
87
|
+
### Summary
|
|
88
|
+
archived
|
|
89
|
+
|
|
90
|
+
### Status
|
|
91
|
+
DONE
|
|
92
|
+
EOF
|
|
93
|
+
cat > "$DEVLOG_DIR/devlog.topic-a.md" <<'EOF'
|
|
94
|
+
## Round 5 — 2026-09-05T10:00:00+0800
|
|
95
|
+
|
|
96
|
+
### Summary
|
|
97
|
+
kept topic
|
|
98
|
+
|
|
99
|
+
### Status
|
|
100
|
+
DONE
|
|
101
|
+
EOF
|
|
102
|
+
cat > "$DEVLOG_DIR/devlog.feat-x.md" <<'EOF'
|
|
103
|
+
## Round 1 — 2026-09-04T10:00:00+0800
|
|
104
|
+
|
|
105
|
+
### Summary
|
|
106
|
+
branch work
|
|
107
|
+
|
|
108
|
+
### Status
|
|
109
|
+
IN_PROGRESS
|
|
110
|
+
EOF
|
|
111
|
+
printf '# Lessons: foo\n\n## 2026-09-01T00:00:00+0800\nlesson.\n' > "$DEVLOG_DIR/devlog.lessons.foo.md"
|
|
112
|
+
|
|
113
|
+
# --- Default KEY=VALUE --------------------------------------------------------
|
|
114
|
+
OUT="$(bash "$SCRIPT_DIR/report-devlog.sh")"
|
|
115
|
+
assert_line "branch" "BRANCH=main" "$OUT"
|
|
116
|
+
assert_line "total rounds = archive + main, fenced fake ignored" "ROUNDS_TOTAL=4" "$OUT"
|
|
117
|
+
assert_line "main rounds" "ROUNDS_MAIN=3" "$OUT"
|
|
118
|
+
assert_line "archive rounds" "ROUNDS_ARCHIVE=1" "$OUT"
|
|
119
|
+
assert_line "done count" "STATUS_DONE=2" "$OUT"
|
|
120
|
+
assert_line "in-progress count (branch file not scanned)" "STATUS_IN_PROGRESS=0" "$OUT"
|
|
121
|
+
assert_line "blocked count" "STATUS_BLOCKED=1" "$OUT"
|
|
122
|
+
assert_line "interrupted count despite reason line" "STATUS_INTERRUPTED=1" "$OUT"
|
|
123
|
+
assert_line "blocked ratio" "BLOCKED_RATIO=25" "$OUT"
|
|
124
|
+
assert_line "checkpoints" "CHECKPOINTS=1" "$OUT"
|
|
125
|
+
assert_line "kept topics" "KEPT_TOPICS=1" "$OUT"
|
|
126
|
+
assert_line "lessons topics" "LESSONS_TOPICS=1" "$OUT"
|
|
127
|
+
assert_line "first round at" "FIRST_ROUND_AT=2026-08-31T10:00:00+0800" "$OUT"
|
|
128
|
+
assert_line "last round at" "LAST_ROUND_AT=2026-09-03T10:00:00+0800" "$OUT"
|
|
129
|
+
assert_no_line_prefix "no advisory state -> no LESSONS_ADVISORY" "LESSONS_ADVISORY=" "$OUT"
|
|
130
|
+
|
|
131
|
+
printf '%s\n' '{"count": 2, "threshold": 3}' > "$DEVLOG_DIR/.lessons-advisory-state"
|
|
132
|
+
OUT="$(bash "$SCRIPT_DIR/report-devlog.sh")"
|
|
133
|
+
assert_line "advisory state present" "LESSONS_ADVISORY=2/3" "$OUT"
|
|
134
|
+
rm -f "$DEVLOG_DIR/.lessons-advisory-state"
|
|
135
|
+
|
|
136
|
+
# --- Empty devlog dir: zeros and none ----------------------------------------
|
|
137
|
+
EMPTY_ROOT="$(mktemp -d)"
|
|
138
|
+
mkdir -p "$EMPTY_ROOT/.devlog"
|
|
139
|
+
OUT="$(CLAUDE_PROJECT_DIR="$EMPTY_ROOT" bash "$SCRIPT_DIR/report-devlog.sh")"
|
|
140
|
+
assert_line "empty: zero rounds" "ROUNDS_TOTAL=0" "$OUT"
|
|
141
|
+
assert_line "empty: zero ratio" "BLOCKED_RATIO=0" "$OUT"
|
|
142
|
+
assert_line "empty: first none" "FIRST_ROUND_AT=none" "$OUT"
|
|
143
|
+
rm -rf "$EMPTY_ROOT"
|
|
144
|
+
|
|
145
|
+
# --- Unknown arg -> exit 2 ------------------------------------------------------
|
|
146
|
+
bash "$SCRIPT_DIR/report-devlog.sh" --bogus >/dev/null 2>&1
|
|
147
|
+
RC=$?
|
|
148
|
+
if [ "$RC" -eq 2 ]; then echo "PASS: unknown arg exits 2"; else echo "FAIL: unknown arg exit $RC"; FAIL=1; fi
|
|
149
|
+
|
|
150
|
+
# --- --json (no rounds) -------------------------------------------------------
|
|
151
|
+
J="$(bash "$SCRIPT_DIR/report-devlog.sh" --json)"
|
|
152
|
+
jcheck "json: started + counts are numbers" "$J" 'd.started===true && d.rounds_total===4 && d.status_blocked===1 && d.blocked_ratio===25'
|
|
153
|
+
jcheck "json: branch + times" "$J" 'd.branch==="main" && d.first_round_at==="2026-08-31T10:00:00+0800"'
|
|
154
|
+
jcheck "json: no advisory -> null" "$J" 'd.lessons_advisory===null'
|
|
155
|
+
jcheck "json: no rounds array without --rounds" "$J" '!("rounds" in d) && !("checkpoint_blocks" in d)'
|
|
156
|
+
|
|
157
|
+
# --- --json --rounds ------------------------------------------------------------
|
|
158
|
+
J="$(bash "$SCRIPT_DIR/report-devlog.sh" --json --rounds)"
|
|
159
|
+
jcheck "rounds: 4 entries, archive first" "$J" 'd.rounds.length===4 && d.rounds[0].branch==="archive" && d.rounds[0].n===0'
|
|
160
|
+
jcheck "rounds: escaping survives quote/backslash/tab/CJK" "$J" 'd.rounds[1].summary.includes("\"quoted\"") && d.rounds[1].summary.includes("\\") && d.rounds[1].summary.includes("\t") && d.rounds[1].summary.includes("中文")'
|
|
161
|
+
jcheck "rounds: fenced fake heading stays inside summary" "$J" 'd.rounds[2].summary.includes("## Round 9 fake inside fence")'
|
|
162
|
+
jcheck "rounds: status parsed despite reason line" "$J" 'd.rounds[3].status==="INTERRUPTED"'
|
|
163
|
+
jcheck "rounds: segments captured" "$J" 'd.rounds[3].segments.length===1 && d.rounds[3].segments[0]==="段落 探索\nseg body"'
|
|
164
|
+
jcheck "rounds: handoff keeps #### lines" "$J" 'd.rounds[1].handoff.startsWith("#### 現況")'
|
|
165
|
+
jcheck "rounds: line numbers" "$J" 'd.rounds[1].line===1 && d.rounds[1].file==="devlog.md"'
|
|
166
|
+
jcheck "rounds: no input by default" "$J" 'd.rounds.every(r => !("input" in r))'
|
|
167
|
+
jcheck "checkpoint_blocks: one block with body" "$J" 'd.checkpoint_blocks.length===1 && d.checkpoint_blocks[0].heading==="Checkpoint(Round 1-2)" && d.checkpoint_blocks[0].body.includes("keep it")'
|
|
168
|
+
|
|
169
|
+
J="$(bash "$SCRIPT_DIR/report-devlog.sh" --json --rounds --with-input)"
|
|
170
|
+
jcheck "with-input: input field present" "$J" 'd.rounds[1].input==="secret-input-text"'
|
|
171
|
+
|
|
172
|
+
# --- --json on NOT_STARTED ------------------------------------------------------
|
|
173
|
+
NS_ROOT="$(mktemp -d)"
|
|
174
|
+
J="$(CLAUDE_PROJECT_DIR="$NS_ROOT" bash "$SCRIPT_DIR/report-devlog.sh" --json)"
|
|
175
|
+
jcheck "json NOT_STARTED" "$J" 'd.started===false'
|
|
176
|
+
rm -rf "$NS_ROOT"
|
|
177
|
+
|
|
178
|
+
# --- --all-branches ---------------------------------------------------------------
|
|
179
|
+
OUT="$(bash "$SCRIPT_DIR/report-devlog.sh" --all-branches)"
|
|
180
|
+
assert_line "all-branches: branch file added, kept file excluded" "ROUNDS_TOTAL=5" "$OUT"
|
|
181
|
+
assert_line "all-branches: in-progress from branch file" "STATUS_IN_PROGRESS=1" "$OUT"
|
|
182
|
+
assert_line "all-branches: last round from branch file" "LAST_ROUND_AT=2026-09-04T10:00:00+0800" "$OUT"
|
|
183
|
+
J="$(bash "$SCRIPT_DIR/report-devlog.sh" --json --rounds --all-branches)"
|
|
184
|
+
jcheck "all-branches: feat-x rounds tagged" "$J" 'd.rounds.some(r => r.branch==="feat-x" && r.summary==="branch work")'
|
|
185
|
+
jcheck "all-branches: no kept or lessons rounds" "$J" 'd.rounds.every(r => r.branch!=="topic-a" && !r.branch.startsWith("lessons."))'
|
|
186
|
+
|
|
187
|
+
# --- control chars outside \t\r\n (e.g. pasted ANSI \033) escape to \uXXXX ------
|
|
188
|
+
CTRL_ROOT="$(mktemp -d)"
|
|
189
|
+
mkdir -p "$CTRL_ROOT/.devlog"
|
|
190
|
+
printf '## Round 1 — 2026-09-06T10:00:00+0800\n\n### Summary\nboom\033[31mred\n\n### Status\nDONE\n' \
|
|
191
|
+
> "$CTRL_ROOT/.devlog/devlog.md"
|
|
192
|
+
J="$(CLAUDE_PROJECT_DIR="$CTRL_ROOT" bash "$SCRIPT_DIR/report-devlog.sh" --json --rounds)"
|
|
193
|
+
jcheck "control char (ESC) escapes to \\u001b, JSON still parses" "$J" 'd.rounds[0].summary.includes("\x1b")'
|
|
194
|
+
rm -rf "$CTRL_ROOT"
|
|
195
|
+
|
|
196
|
+
if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
|
|
197
|
+
else echo "Some checks FAILED."; exit 1; fi
|
|
@@ -652,6 +652,13 @@ else
|
|
|
652
652
|
fi
|
|
653
653
|
rm -f "$DEVLOG_DIR/.checkpoint-state"
|
|
654
654
|
|
|
655
|
+
# --- 19b: report / timeline are admin (read-side) commands too -------------
|
|
656
|
+
for ADMIN_CMD in report timeline; do
|
|
657
|
+
rm -f "$DEVLOG_DIR/.round-current.md" "$DEVLOG_DIR/.round-open"
|
|
658
|
+
printf '{"prompt":"<command-name>/devlog-tracker:%s</command-name>"}' "$ADMIN_CMD" | bash "$SCRIPT_DIR/round-start.sh"
|
|
659
|
+
assert_file_absent "admin $ADMIN_CMD: no .round-current.md" "$DEVLOG_DIR/.round-current.md"
|
|
660
|
+
done
|
|
661
|
+
|
|
655
662
|
# --- 20: a namespaced-but-different command name is not falsely exempted ---
|
|
656
663
|
# "lessons-on" must not match the "lessons" case arm (prefix collision guard).
|
|
657
664
|
LESSONS_ON_PROMPT='<command-name>/devlog-tracker:lessons-on</command-name><command-message>lessons-on</command-message><command-args></command-args>'
|
|
@@ -677,6 +684,15 @@ else
|
|
|
677
684
|
fi
|
|
678
685
|
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open" "$DEVLOG_DIR/.turn-start" "$DEVLOG_DIR/.round-current.md"
|
|
679
686
|
|
|
687
|
+
# --- 21b: /devlog-tracker:pr and :promote are NOT exempt (outward / file writes)
|
|
688
|
+
for WORK_CMD in pr promote; do
|
|
689
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open" "$DEVLOG_DIR/.turn-start" "$DEVLOG_DIR/.round-current.md"
|
|
690
|
+
printf '{"prompt":"<command-name>/devlog-tracker:%s</command-name>"}' "$WORK_CMD" | bash "$SCRIPT_DIR/round-start.sh"
|
|
691
|
+
CUR_BODY="$(cat "$DEVLOG_DIR/.round-current.md" 2>/dev/null || echo '')"
|
|
692
|
+
assert_contains "$WORK_CMD command still opens a Round" "## Round 1 —" "$CUR_BODY"
|
|
693
|
+
done
|
|
694
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open" "$DEVLOG_DIR/.turn-start" "$DEVLOG_DIR/.round-current.md"
|
|
695
|
+
|
|
680
696
|
# --- workspace claim on next prompt ----------------------------------------
|
|
681
697
|
WS="$TMP_ROOT/ws"
|
|
682
698
|
mkdir -p "$WS/.devlog"
|
|
@@ -1050,6 +1066,55 @@ assert_not_contains "lessons off: no BLOCKED-accumulation advisory" "[Lessons Mo
|
|
|
1050
1066
|
rm -rf "$NOLES_DIR"
|
|
1051
1067
|
export CLAUDE_PROJECT_DIR="$TMP_ROOT"
|
|
1052
1068
|
|
|
1069
|
+
# --- lessons: background agent / workflow task-notification advisory ------
|
|
1070
|
+
TN_DIR="$(mktemp -d)"
|
|
1071
|
+
mkdir -p "$TN_DIR/.devlog"
|
|
1072
|
+
touch "$TN_DIR/.devlog/.enabled" "$TN_DIR/.devlog/.lessons-enabled"
|
|
1073
|
+
export CLAUDE_PROJECT_DIR="$TN_DIR"
|
|
1074
|
+
tn_notif() {
|
|
1075
|
+
printf '{"prompt":"<task-notification><task-id>t9</task-id><status>%s</status><summary>Agent \\"x\\" finished</summary></task-notification>"}' "$1"
|
|
1076
|
+
}
|
|
1077
|
+
tn_reset() {
|
|
1078
|
+
rm -f "$TN_DIR/.devlog/.round-open" "$TN_DIR/.devlog/.round-current.md"
|
|
1079
|
+
printf '## Round 1 — 2026-09-23T00:00:00+0800\n\n### User Input\n```text\nx\n```\n\n### Summary\ns\n\n### Status\nDONE\n' > "$TN_DIR/.devlog/devlog.md"
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
tn_reset
|
|
1083
|
+
printf '%s\n' '{"count": 0, "threshold": 3}' > "$TN_DIR/.devlog/.lessons-advisory-state"
|
|
1084
|
+
OUT="$(tn_notif completed | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
|
|
1085
|
+
assert_contains "task-notif completed: sub agent advisory printed" "[Lessons Mode 提示] 背景任務完成(status=completed)" "$OUT"
|
|
1086
|
+
grep -q '"count": 0' "$TN_DIR/.devlog/.lessons-advisory-state" && echo "PASS: task-notif completed does not bump counter" || { echo "FAIL: completed bumped counter"; FAIL=1; }
|
|
1087
|
+
|
|
1088
|
+
tn_reset
|
|
1089
|
+
OUT="$(tn_notif failed | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
|
|
1090
|
+
assert_contains "task-notif failed: advisory printed" "背景任務完成(status=failed)" "$OUT"
|
|
1091
|
+
grep -q '"count": 1' "$TN_DIR/.devlog/.lessons-advisory-state" && echo "PASS: task-notif failed bumps counter" || { echo "FAIL: failed did not bump counter"; FAIL=1; }
|
|
1092
|
+
|
|
1093
|
+
tn_reset
|
|
1094
|
+
printf '%s\n' '{"count": 2, "threshold": 3}' > "$TN_DIR/.devlog/.lessons-advisory-state"
|
|
1095
|
+
OUT="$(tn_notif killed | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
|
|
1096
|
+
assert_contains "task-notif killed at threshold: shared nudge printed" "流程訊號已累積出現 3 次" "$OUT"
|
|
1097
|
+
grep -q '"count": 0' "$TN_DIR/.devlog/.lessons-advisory-state" && echo "PASS: task-notif killed resets counter at threshold" || { echo "FAIL: killed did not reset counter"; FAIL=1; }
|
|
1098
|
+
|
|
1099
|
+
tn_reset
|
|
1100
|
+
printf '%s\n' '{"round": 1, "ticks_since_checkin": 0, "max_silent_ticks": 5}' > "$TN_DIR/.devlog/.span-open"
|
|
1101
|
+
OUT="$(tn_notif completed | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
|
|
1102
|
+
assert_contains "task-notif during span: advisory still printed" "背景任務完成(status=completed)" "$OUT"
|
|
1103
|
+
rm -f "$TN_DIR/.devlog/.span-open"
|
|
1104
|
+
|
|
1105
|
+
tn_reset
|
|
1106
|
+
rm -f "$TN_DIR/.devlog/.lessons-enabled" "$TN_DIR/.devlog/.lessons-advisory-state"
|
|
1107
|
+
OUT="$(tn_notif failed | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
|
|
1108
|
+
assert_not_contains "task-notif with lessons off: no advisory" "[Lessons Mode 提示]" "$OUT"
|
|
1109
|
+
[ ! -f "$TN_DIR/.devlog/.lessons-advisory-state" ] && echo "PASS: task-notif with lessons off leaves no advisory-state" || { echo "FAIL: advisory-state created while lessons off"; FAIL=1; }
|
|
1110
|
+
|
|
1111
|
+
touch "$TN_DIR/.devlog/.lessons-enabled"
|
|
1112
|
+
tn_reset
|
|
1113
|
+
OUT="$(printf '%s' '{"prompt":"plain message"}' | bash "$SCRIPT_DIR/round-start.sh" 2>/dev/null)"
|
|
1114
|
+
assert_not_contains "plain prompt: no background-task advisory" "背景任務完成" "$OUT"
|
|
1115
|
+
rm -rf "$TN_DIR"
|
|
1116
|
+
export CLAUDE_PROJECT_DIR="$TMP_ROOT"
|
|
1117
|
+
|
|
1053
1118
|
if [ "$FAIL" -eq 0 ]; then
|
|
1054
1119
|
echo "All checks passed."
|
|
1055
1120
|
exit 0
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Self-check for timeline-devlog.sh (docs/design/read-side-and-promote.md C).
|
|
3
|
+
set -uo pipefail
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
TMP_ROOT="$(mktemp -d)"
|
|
6
|
+
trap 'rm -rf "$TMP_ROOT"' EXIT
|
|
7
|
+
export CLAUDE_PROJECT_DIR="$TMP_ROOT"
|
|
8
|
+
DEVLOG_DIR="$TMP_ROOT/.devlog"
|
|
9
|
+
FAIL=0
|
|
10
|
+
pass() { echo "PASS: $1"; }
|
|
11
|
+
fail() { echo "FAIL: $1"; FAIL=1; }
|
|
12
|
+
|
|
13
|
+
# --- NO_NODE: node missing from PATH (checked before anything else) ----------
|
|
14
|
+
EMPTY_BIN="$TMP_ROOT/empty-bin"
|
|
15
|
+
mkdir -p "$EMPTY_BIN"
|
|
16
|
+
BASH_BIN="$(command -v bash)"
|
|
17
|
+
OUT="$(PATH="$EMPTY_BIN" "$BASH_BIN" "$SCRIPT_DIR/timeline-devlog.sh" 2>&1)"
|
|
18
|
+
[ "$OUT" = "NO_NODE" ] && pass "no node -> NO_NODE" || fail "no node -> NO_NODE (got $OUT)"
|
|
19
|
+
|
|
20
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
21
|
+
echo "SKIP: remaining timeline checks need node"
|
|
22
|
+
if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0; else echo "Some checks FAILED."; exit 1; fi
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# --- NOT_STARTED --------------------------------------------------------------
|
|
26
|
+
OUT="$(bash "$SCRIPT_DIR/timeline-devlog.sh")"
|
|
27
|
+
[ "$OUT" = "NOT_STARTED" ] && pass "no .devlog -> NOT_STARTED" || fail "NOT_STARTED (got $OUT)"
|
|
28
|
+
[ ! -e "$DEVLOG_DIR" ] && pass "NOT_STARTED creates nothing" || fail "NOT_STARTED created .devlog"
|
|
29
|
+
|
|
30
|
+
# --- End to end -----------------------------------------------------------------
|
|
31
|
+
mkdir -p "$DEVLOG_DIR"
|
|
32
|
+
cat > "$DEVLOG_DIR/devlog.md" <<'EOF2'
|
|
33
|
+
## Round 1 — 2026-09-01T10:00:00+0800
|
|
34
|
+
|
|
35
|
+
### User Input
|
|
36
|
+
secret-input-text
|
|
37
|
+
|
|
38
|
+
### Summary
|
|
39
|
+
<script>alert(1)</script> 中文摘要
|
|
40
|
+
|
|
41
|
+
### Status
|
|
42
|
+
DONE
|
|
43
|
+
EOF2
|
|
44
|
+
OUT="$(bash "$SCRIPT_DIR/timeline-devlog.sh")"
|
|
45
|
+
case "$OUT" in
|
|
46
|
+
"OUT=$(cd "$DEVLOG_DIR" && pwd)/timeline.html") pass "default OUT path is absolute .devlog/timeline.html" ;;
|
|
47
|
+
*) fail "default OUT (got $OUT)" ;;
|
|
48
|
+
esac
|
|
49
|
+
HTML="$DEVLOG_DIR/timeline.html"
|
|
50
|
+
grep -q '中文摘要' "$HTML" && pass "summary rendered" || fail "summary missing"
|
|
51
|
+
grep -q '<script>alert' "$HTML" && fail "raw script tag leaked" || pass "script tag escaped"
|
|
52
|
+
grep -q 'secret-input-text' "$HTML" && fail "User Input leaked" || pass "User Input not included"
|
|
53
|
+
[ ! -e "$HTML.tmp" ] && pass "no leftover tmp file" || fail "leftover $HTML.tmp"
|
|
54
|
+
|
|
55
|
+
# --- --out ------------------------------------------------------------------------
|
|
56
|
+
OUT="$(bash "$SCRIPT_DIR/timeline-devlog.sh" --out "$TMP_ROOT/custom.html")"
|
|
57
|
+
[ -s "$TMP_ROOT/custom.html" ] && pass "--out writes the given path" || fail "--out ($OUT)"
|
|
58
|
+
|
|
59
|
+
# --- unknown arg ------------------------------------------------------------------
|
|
60
|
+
bash "$SCRIPT_DIR/timeline-devlog.sh" --bogus >/dev/null 2>&1
|
|
61
|
+
RC=$?
|
|
62
|
+
[ "$RC" -eq 2 ] && pass "unknown arg exits 2" || fail "unknown arg exit $RC"
|
|
63
|
+
|
|
64
|
+
if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
|
|
65
|
+
else echo "Some checks FAILED."; exit 1; fi
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# /devlog-tracker:timeline and `npx devlog-tracker timeline`
|
|
3
|
+
# (docs/design/read-side-and-promote.md C): report-devlog.sh --json --rounds
|
|
4
|
+
# piped into timeline-render.js, written to .devlog/timeline.html (gitignored).
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
|
|
7
|
+
_src="${BASH_SOURCE[0]}"
|
|
8
|
+
SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
|
|
9
|
+
|
|
10
|
+
# Checked before sourcing anything: Claude plugin users may not have Node.
|
|
11
|
+
command -v node >/dev/null 2>&1 || { echo "NO_NODE"; exit 0; }
|
|
12
|
+
|
|
13
|
+
# shellcheck source=devlog-path.sh
|
|
14
|
+
. "$SCRIPT_DIR/devlog-path.sh"
|
|
15
|
+
|
|
16
|
+
ALL=0
|
|
17
|
+
OUT=""
|
|
18
|
+
while [ $# -gt 0 ]; do
|
|
19
|
+
case "$1" in
|
|
20
|
+
--all-branches) ALL=1 ;;
|
|
21
|
+
--out) shift; OUT="${1:-}" ;;
|
|
22
|
+
*) echo "UNKNOWN_ARG=$1" >&2; exit 2 ;;
|
|
23
|
+
esac
|
|
24
|
+
shift
|
|
25
|
+
done
|
|
26
|
+
|
|
27
|
+
devlog_resolve_paths "${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}"
|
|
28
|
+
[ -d "$DEVLOG_DIR" ] || { echo "NOT_STARTED"; exit 0; }
|
|
29
|
+
[ -n "$OUT" ] || OUT="$DEVLOG_DIR/timeline.html"
|
|
30
|
+
|
|
31
|
+
ARGS=(--json --rounds)
|
|
32
|
+
[ "$ALL" -eq 1 ] && ARGS+=(--all-branches)
|
|
33
|
+
|
|
34
|
+
JSON_TMP="$(mktemp "${TMPDIR:-/tmp}/devlog-timeline.XXXXXX")" || exit 1
|
|
35
|
+
trap 'rm -f "$JSON_TMP" "$OUT.tmp"' EXIT
|
|
36
|
+
bash "$SCRIPT_DIR/report-devlog.sh" "${ARGS[@]}" > "$JSON_TMP" || exit 1
|
|
37
|
+
node "$SCRIPT_DIR/timeline-render.js" < "$JSON_TMP" > "$OUT.tmp" || exit 1
|
|
38
|
+
mv "$OUT.tmp" "$OUT" || exit 1
|
|
39
|
+
|
|
40
|
+
OUT_DIR="$(cd "$(dirname "$OUT")" && pwd)"
|
|
41
|
+
echo "OUT=$OUT_DIR/$(basename "$OUT")"
|