devlog-tracker 0.30.0 → 0.31.1
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 +22 -6
- package/README.zh-TW.md +22 -6
- package/bin/devlog-tracker.js +14 -1
- 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/devlog-lock.sh +10 -0
- package/core/scripts/json-field.sh +16 -0
- package/core/scripts/lessons-subagent-start.sh +2 -10
- 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 +1 -1
- package/core/scripts/tests/test-close-open-round.sh +54 -0
- package/core/scripts/tests/test-devlog-lock.sh +44 -0
- package/core/scripts/tests/test-json-field.sh +5 -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 +28 -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 +1 -1
|
@@ -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
|
|
@@ -125,7 +125,19 @@ hello world
|
|
|
125
125
|
```
|
|
126
126
|
EOF
|
|
127
127
|
printf '%s\n' '{"round": 1, "opened_at": "2026-09-09T12:00:00+08:00"}' > "$DEVLOG_DIR/.round-open"
|
|
128
|
+
# round-start.sh holds .devlog/.lock while it runs close-open-round.sh as a
|
|
129
|
+
# child; the child must not wait out the 2s contention timeout on its own
|
|
130
|
+
# parent's lock.
|
|
131
|
+
HEAL_START="$(date +%s)"
|
|
128
132
|
printf '%s' '{"prompt":"second"}' | bash "$SCRIPT_DIR/round-start.sh"
|
|
133
|
+
HEAL_ELAPSED=$(( $(date +%s) - HEAL_START ))
|
|
134
|
+
if [ "$HEAL_ELAPSED" -le 1 ]; then
|
|
135
|
+
echo "PASS: dangling heal doesn't block on parent's lock (${HEAL_ELAPSED}s)"
|
|
136
|
+
else
|
|
137
|
+
echo "FAIL: dangling heal took ${HEAL_ELAPSED}s (child waited on parent's lock)"
|
|
138
|
+
FAIL=1
|
|
139
|
+
fi
|
|
140
|
+
assert_file_absent "dangling heal releases .lock" "$DEVLOG_DIR/.lock"
|
|
129
141
|
MAIN_BODY="$(cat "$DEVLOG_DIR/devlog.md")"
|
|
130
142
|
CUR_BODY="$(cat "$DEVLOG_DIR/.round-current.md" 2>/dev/null || echo '')"
|
|
131
143
|
assert_contains "Round 2 heading in .round-current.md" "## Round 2 —" "$CUR_BODY"
|
|
@@ -652,6 +664,13 @@ else
|
|
|
652
664
|
fi
|
|
653
665
|
rm -f "$DEVLOG_DIR/.checkpoint-state"
|
|
654
666
|
|
|
667
|
+
# --- 19b: report / timeline are admin (read-side) commands too -------------
|
|
668
|
+
for ADMIN_CMD in report timeline; do
|
|
669
|
+
rm -f "$DEVLOG_DIR/.round-current.md" "$DEVLOG_DIR/.round-open"
|
|
670
|
+
printf '{"prompt":"<command-name>/devlog-tracker:%s</command-name>"}' "$ADMIN_CMD" | bash "$SCRIPT_DIR/round-start.sh"
|
|
671
|
+
assert_file_absent "admin $ADMIN_CMD: no .round-current.md" "$DEVLOG_DIR/.round-current.md"
|
|
672
|
+
done
|
|
673
|
+
|
|
655
674
|
# --- 20: a namespaced-but-different command name is not falsely exempted ---
|
|
656
675
|
# "lessons-on" must not match the "lessons" case arm (prefix collision guard).
|
|
657
676
|
LESSONS_ON_PROMPT='<command-name>/devlog-tracker:lessons-on</command-name><command-message>lessons-on</command-message><command-args></command-args>'
|
|
@@ -677,6 +696,15 @@ else
|
|
|
677
696
|
fi
|
|
678
697
|
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open" "$DEVLOG_DIR/.turn-start" "$DEVLOG_DIR/.round-current.md"
|
|
679
698
|
|
|
699
|
+
# --- 21b: /devlog-tracker:pr and :promote are NOT exempt (outward / file writes)
|
|
700
|
+
for WORK_CMD in pr promote; do
|
|
701
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open" "$DEVLOG_DIR/.turn-start" "$DEVLOG_DIR/.round-current.md"
|
|
702
|
+
printf '{"prompt":"<command-name>/devlog-tracker:%s</command-name>"}' "$WORK_CMD" | bash "$SCRIPT_DIR/round-start.sh"
|
|
703
|
+
CUR_BODY="$(cat "$DEVLOG_DIR/.round-current.md" 2>/dev/null || echo '')"
|
|
704
|
+
assert_contains "$WORK_CMD command still opens a Round" "## Round 1 —" "$CUR_BODY"
|
|
705
|
+
done
|
|
706
|
+
rm -f "$DEVLOG_DIR/devlog.md" "$DEVLOG_DIR/.round-open" "$DEVLOG_DIR/.turn-start" "$DEVLOG_DIR/.round-current.md"
|
|
707
|
+
|
|
680
708
|
# --- workspace claim on next prompt ----------------------------------------
|
|
681
709
|
WS="$TMP_ROOT/ws"
|
|
682
710
|
mkdir -p "$WS/.devlog"
|
|
@@ -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")"
|