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,28 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SubagentStart hook(Claude Code):Lessons Mode 開著時,把「可以自己呼叫
|
|
3
|
+
# lessons-append.sh」的說明注入 sub agent/Workflow agent 的 context
|
|
4
|
+
# (docs/design/lessons-mode.md「sub agent/workflow 情境」)。
|
|
5
|
+
# sub agent 的 Bash 裡沒有 CLAUDE_PROJECT_DIR,isolation: "worktree" 時 cwd
|
|
6
|
+
# 還是 worktree(沒有 .devlog/),所以專案目錄跟腳本路徑都在這裡寫死成
|
|
7
|
+
# 主專案的絕對路徑。fail-open:任何問題都 exit 0、不輸出。
|
|
8
|
+
|
|
9
|
+
set -uo pipefail
|
|
10
|
+
|
|
11
|
+
_src="${BASH_SOURCE[0]}"
|
|
12
|
+
HOOKS_DIR="$(cd "${_src%/*}" && pwd)"
|
|
13
|
+
# shellcheck source=json-field.sh
|
|
14
|
+
. "$HOOKS_DIR/json-field.sh"
|
|
15
|
+
PROJECT_DIR="${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}"
|
|
16
|
+
[ -f "$PROJECT_DIR/.devlog/.enabled" ] || exit 0
|
|
17
|
+
[ -f "$PROJECT_DIR/.devlog/.lessons-enabled" ] || exit 0
|
|
18
|
+
PROJECT_DIR="$(cd "$PROJECT_DIR" 2>/dev/null && pwd)" || exit 0
|
|
19
|
+
cat >/dev/null 2>&1 || true
|
|
20
|
+
|
|
21
|
+
MSG="[devlog-tracker Lessons Mode] 這個專案開著 Lessons Mode:記錄開發**過程**踩過的坑(不是架構知識)。如果這次任務中你繞了一大圈才找到對的做法、先前的 fix/claim 被驗證推翻、或卡在某個值得後人避開的坑,可以直接記一筆(非強制,沒有就不用記):
|
|
22
|
+
|
|
23
|
+
DEVLOG_PROJECT_DIR='${PROJECT_DIR}' bash '${HOOKS_DIR}/lessons-append.sh' --topic '<kebab-case 主題,2–4 段>' --text '<一段自由散文:卡在哪、怎麼解開、下次怎麼避免>'
|
|
24
|
+
|
|
25
|
+
路徑都是絕對路徑,在 worktree 裡也照原樣用,不要改成相對路徑。有記的話,在最終回報裡用一句話說明記了哪個主題;腳本回報 NEW_TOPIC 時可改用它列出的既有主題重跑。"
|
|
26
|
+
|
|
27
|
+
printf '{"hookSpecificOutput":{"hookEventName":"SubagentStart","additionalContext":"%s"}}\n' "$(json_escape "$MSG")"
|
|
28
|
+
exit 0
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Context for /devlog-tracker:pr (docs/design/read-side-and-promote.md A).
|
|
3
|
+
# Plain read only: git is only queried, and gh is only asked `auth status`
|
|
4
|
+
# and `pr view` -- creating/editing the PR is the command doc's job, after
|
|
5
|
+
# the user confirms.
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
|
|
8
|
+
_src="${BASH_SOURCE[0]}"
|
|
9
|
+
SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
|
|
10
|
+
# shellcheck source=devlog-md.sh
|
|
11
|
+
. "$SCRIPT_DIR/devlog-md.sh"
|
|
12
|
+
# shellcheck source=devlog-path.sh
|
|
13
|
+
. "$SCRIPT_DIR/devlog-path.sh"
|
|
14
|
+
|
|
15
|
+
PROJECT_DIR="$(cd "${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}" 2>/dev/null && pwd)" || { echo "NOT_A_REPO"; exit 0; }
|
|
16
|
+
git -C "$PROJECT_DIR" rev-parse --git-dir >/dev/null 2>&1 || { echo "NOT_A_REPO"; exit 0; }
|
|
17
|
+
|
|
18
|
+
BRANCH="$(git -C "$PROJECT_DIR" symbolic-ref --quiet --short HEAD 2>/dev/null || echo '')"
|
|
19
|
+
case "$BRANCH" in
|
|
20
|
+
'') echo "DETACHED_HEAD"; exit 0 ;;
|
|
21
|
+
[Mm][Aa][Ii][Nn]|[Mm][Aa][Ss][Tt][Ee][Rr]) echo "ON_DEFAULT_BRANCH"; exit 0 ;;
|
|
22
|
+
esac
|
|
23
|
+
|
|
24
|
+
BASE=""
|
|
25
|
+
BASE_REF=""
|
|
26
|
+
ORIGIN_HEAD="$(git -C "$PROJECT_DIR" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null || echo '')"
|
|
27
|
+
if [ -n "$ORIGIN_HEAD" ]; then
|
|
28
|
+
BASE="${ORIGIN_HEAD#origin/}"
|
|
29
|
+
BASE_REF="$ORIGIN_HEAD"
|
|
30
|
+
else
|
|
31
|
+
for candidate in main master; do
|
|
32
|
+
if git -C "$PROJECT_DIR" rev-parse --verify --quiet "refs/heads/$candidate" >/dev/null; then
|
|
33
|
+
BASE="$candidate"
|
|
34
|
+
BASE_REF="$candidate"
|
|
35
|
+
break
|
|
36
|
+
fi
|
|
37
|
+
done
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
echo "BRANCH=$BRANCH"
|
|
41
|
+
[ -n "$BASE" ] || { echo "NO_BASE"; exit 0; }
|
|
42
|
+
echo "BASE=$BASE"
|
|
43
|
+
echo "BASE_REF=$BASE_REF"
|
|
44
|
+
|
|
45
|
+
devlog_resolve_paths "$PROJECT_DIR"
|
|
46
|
+
echo "DEVLOG_FILE=$DEVLOG_FILE"
|
|
47
|
+
ROUNDS=""
|
|
48
|
+
if [ -f "$DEVLOG_FILE" ]; then
|
|
49
|
+
ROUNDS="$(devlog_list_round_starts "$DEVLOG_FILE" | awk '{ printf "%s%s", (n++ ? " " : ""), $1 }')"
|
|
50
|
+
fi
|
|
51
|
+
if [ -n "$ROUNDS" ]; then echo "ROUNDS=$ROUNDS"; else echo "NO_BRANCH_DEVLOG"; fi
|
|
52
|
+
|
|
53
|
+
COMMITS="$(git -C "$PROJECT_DIR" rev-list --count "$BASE_REF..HEAD" 2>/dev/null || echo 0)"
|
|
54
|
+
echo "COMMITS=$COMMITS"
|
|
55
|
+
|
|
56
|
+
GH=no
|
|
57
|
+
PR=none
|
|
58
|
+
if command -v gh >/dev/null 2>&1 && (cd "$PROJECT_DIR" && gh auth status >/dev/null 2>&1); then
|
|
59
|
+
GH=yes
|
|
60
|
+
n="$(cd "$PROJECT_DIR" && gh pr view --json number -q .number 2>/dev/null || echo '')"
|
|
61
|
+
case "$n" in ''|*[!0-9]*) ;; *) PR="$n" ;; esac
|
|
62
|
+
fi
|
|
63
|
+
echo "GH=$GH"
|
|
64
|
+
echo "PR=$PR"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Sources for /devlog-tracker:promote (docs/design/read-side-and-promote.md D):
|
|
3
|
+
# kept files, lessons files, and Checkpoint headings in the current branch's
|
|
4
|
+
# devlog. Plain read only. compact never moves Checkpoints, so the archive has
|
|
5
|
+
# none to list.
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
|
|
8
|
+
_src="${BASH_SOURCE[0]}"
|
|
9
|
+
SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
|
|
10
|
+
# shellcheck source=devlog-md.sh
|
|
11
|
+
. "$SCRIPT_DIR/devlog-md.sh"
|
|
12
|
+
# shellcheck source=devlog-path.sh
|
|
13
|
+
. "$SCRIPT_DIR/devlog-path.sh"
|
|
14
|
+
|
|
15
|
+
PROJECT_DIR="$(cd "${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}" 2>/dev/null && pwd)" || { echo "NO_SOURCES"; exit 0; }
|
|
16
|
+
devlog_resolve_paths "$PROJECT_DIR"
|
|
17
|
+
MAIN="$DEVLOG_FILE"
|
|
18
|
+
[ -f "$MAIN" ] || { echo "NO_SOURCES"; exit 0; }
|
|
19
|
+
|
|
20
|
+
OUT=""
|
|
21
|
+
add() { OUT="$OUT$1"$'\n'; }
|
|
22
|
+
exists_flag() { if [ -f "$1" ]; then printf '1'; else printf '0'; fi; }
|
|
23
|
+
|
|
24
|
+
while IFS= read -r name; do
|
|
25
|
+
[ -n "$name" ] || continue
|
|
26
|
+
f="$DEVLOG_DIR/devlog.$name.md"
|
|
27
|
+
add "FILE=$f KIND=kept EXISTS=$(exists_flag "$f")"
|
|
28
|
+
done < <(devlog_kept_index_lines "$MAIN" | sed -nE 's/.*`devlog\.([^`]+)\.md`.*/\1/p')
|
|
29
|
+
|
|
30
|
+
while IFS= read -r topic; do
|
|
31
|
+
[ -n "$topic" ] || continue
|
|
32
|
+
f="$DEVLOG_DIR/devlog.lessons.$topic.md"
|
|
33
|
+
add "FILE=$f KIND=lessons EXISTS=$(exists_flag "$f")"
|
|
34
|
+
done < <(devlog_lessons_index_lines "$MAIN" | sed -nE 's/.*`devlog\.lessons\.([^`]+)\.md`.*/\1/p')
|
|
35
|
+
|
|
36
|
+
while IFS= read -r ln; do
|
|
37
|
+
[ -n "$ln" ] && add "CHECKPOINT=$MAIN:$ln"
|
|
38
|
+
done < <(awk '/^[ \t]*```/ { fence = !fence; next } !fence && /^## Checkpoint/ { print NR }' "$MAIN")
|
|
39
|
+
|
|
40
|
+
if [ -z "$OUT" ]; then echo "NO_SOURCES"; else printf '%s' "$OUT"; fi
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Picks the file /devlog-tracker:promote writes rules into
|
|
3
|
+
# (docs/design/read-side-and-promote.md D). Plain read only.
|
|
4
|
+
# 1. Codex-only project (no CLAUDE.md, Codex hooks or devlog skills present) -> AGENTS.md
|
|
5
|
+
# 2. CLAUDE.md that is nothing but "@AGENTS.md" -> AGENTS.md
|
|
6
|
+
# 3. otherwise -> CLAUDE.md
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
|
|
9
|
+
PROJECT_DIR="$(cd "${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}" 2>/dev/null && pwd)" || exit 1
|
|
10
|
+
CLAUDE_MD="$PROJECT_DIR/CLAUDE.md"
|
|
11
|
+
AGENTS_MD="$PROJECT_DIR/AGENTS.md"
|
|
12
|
+
BEGIN_MARK='<!-- devlog-tracker:rules:begin -->'
|
|
13
|
+
END_MARK='<!-- devlog-tracker:rules:end -->'
|
|
14
|
+
|
|
15
|
+
HAS_CODEX=0
|
|
16
|
+
[ -f "$PROJECT_DIR/.codex/hooks.json" ] && HAS_CODEX=1
|
|
17
|
+
for d in "$PROJECT_DIR"/.agents/skills/devlog-*; do
|
|
18
|
+
[ -d "$d" ] && HAS_CODEX=1
|
|
19
|
+
done
|
|
20
|
+
|
|
21
|
+
TARGET="$CLAUDE_MD"
|
|
22
|
+
if [ ! -f "$CLAUDE_MD" ] && [ "$HAS_CODEX" -eq 1 ]; then
|
|
23
|
+
TARGET="$AGENTS_MD"
|
|
24
|
+
elif [ -f "$CLAUDE_MD" ] && [ "$(tr -d '[:space:]' < "$CLAUDE_MD")" = "@AGENTS.md" ]; then
|
|
25
|
+
TARGET="$AGENTS_MD"
|
|
26
|
+
fi
|
|
27
|
+
echo "TARGET=$TARGET"
|
|
28
|
+
|
|
29
|
+
if [ -f "$TARGET" ] && grep -qxF "$BEGIN_MARK" "$TARGET"; then
|
|
30
|
+
n="$(awk -v b="$BEGIN_MARK" -v e="$END_MARK" '
|
|
31
|
+
$0 == b { g = 1; next }
|
|
32
|
+
$0 == e { g = 0 }
|
|
33
|
+
g && /^- / { c++ }
|
|
34
|
+
END { print c + 0 }
|
|
35
|
+
' "$TARGET")"
|
|
36
|
+
echo "EXISTING=$n"
|
|
37
|
+
fi
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Writes rules chosen in /devlog-tracker:promote into the managed block
|
|
3
|
+
# <!-- devlog-tracker:rules:begin/end --> of <target>
|
|
4
|
+
# (docs/design/read-side-and-promote.md D). Append-only: never edits or
|
|
5
|
+
# removes a rule already in the block, never touches anything outside it.
|
|
6
|
+
# Exact-line duplicates (against the block, and within the input) are skipped.
|
|
7
|
+
# Usage: promote-write.sh <target> <rules-file>
|
|
8
|
+
set -uo pipefail
|
|
9
|
+
|
|
10
|
+
_src="${BASH_SOURCE[0]}"
|
|
11
|
+
SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
|
|
12
|
+
# shellcheck source=devlog-path.sh
|
|
13
|
+
. "$SCRIPT_DIR/devlog-path.sh"
|
|
14
|
+
|
|
15
|
+
TARGET="${1:-}"
|
|
16
|
+
RULES="${2:-}"
|
|
17
|
+
if [ -z "$TARGET" ] || [ ! -f "$RULES" ]; then
|
|
18
|
+
echo "USAGE: promote-write.sh <target> <rules-file>" >&2
|
|
19
|
+
exit 2
|
|
20
|
+
fi
|
|
21
|
+
BEGIN_MARK='<!-- devlog-tracker:rules:begin -->'
|
|
22
|
+
END_MARK='<!-- devlog-tracker:rules:end -->'
|
|
23
|
+
|
|
24
|
+
devlog_resolve_paths "${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}"
|
|
25
|
+
NEW="$(mktemp "${TMPDIR:-/tmp}/devlog-promote.XXXXXX")" || exit 1
|
|
26
|
+
EXISTING="$(mktemp "${TMPDIR:-/tmp}/devlog-promote.XXXXXX")" || exit 1
|
|
27
|
+
TO_ADD="$(mktemp "${TMPDIR:-/tmp}/devlog-promote.XXXXXX")" || exit 1
|
|
28
|
+
trap 'rm -f "$NEW" "$EXISTING" "$TO_ADD" "$TARGET.tmp"; devlog_lock_release' EXIT
|
|
29
|
+
[ -d "$DEVLOG_DIR" ] && devlog_lock_acquire
|
|
30
|
+
|
|
31
|
+
# Normalise: trim, drop blank lines, ensure a "- " list prefix.
|
|
32
|
+
awk '
|
|
33
|
+
{ sub(/^[ \t]+/, ""); sub(/[ \t]+$/, "") }
|
|
34
|
+
$0 != "" { if ($0 !~ /^- /) $0 = "- " $0; print }
|
|
35
|
+
' "$RULES" > "$NEW"
|
|
36
|
+
|
|
37
|
+
if [ -f "$TARGET" ]; then
|
|
38
|
+
awk -v b="$BEGIN_MARK" -v e="$END_MARK" '$0 == b { g = 1; next } $0 == e { g = 0 } g' "$TARGET" > "$EXISTING"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# FILENAME, not NR==FNR: an empty $EXISTING would make NR==FNR true for $NEW too.
|
|
42
|
+
awk -v ex="$EXISTING" 'FILENAME == ex { seen[$0] = 1; next } !seen[$0]++' "$EXISTING" "$NEW" > "$TO_ADD"
|
|
43
|
+
|
|
44
|
+
TOTAL="$(awk 'END { print NR }' "$NEW")"
|
|
45
|
+
ADDED="$(awk 'END { print NR }' "$TO_ADD")"
|
|
46
|
+
SKIPPED=$((TOTAL - ADDED))
|
|
47
|
+
|
|
48
|
+
if [ "$ADDED" -gt 0 ]; then
|
|
49
|
+
if [ -f "$TARGET" ] && grep -qxF "$BEGIN_MARK" "$TARGET" && grep -qxF "$END_MARK" "$TARGET"; then
|
|
50
|
+
awk -v e="$END_MARK" -v add="$TO_ADD" '
|
|
51
|
+
$0 == e && !done { while ((getline l < add) > 0) print l; done = 1 }
|
|
52
|
+
{ print }
|
|
53
|
+
' "$TARGET" > "$TARGET.tmp" || exit 1
|
|
54
|
+
else
|
|
55
|
+
{
|
|
56
|
+
if [ -s "$TARGET" ]; then
|
|
57
|
+
cat "$TARGET"
|
|
58
|
+
[ -n "$(tail -c 1 "$TARGET")" ] && printf '\n'
|
|
59
|
+
printf '\n'
|
|
60
|
+
fi
|
|
61
|
+
printf '%s\n## devlog-tracker 沉澱的規範\n\n' "$BEGIN_MARK"
|
|
62
|
+
cat "$TO_ADD"
|
|
63
|
+
printf '%s\n' "$END_MARK"
|
|
64
|
+
} > "$TARGET.tmp" || exit 1
|
|
65
|
+
fi
|
|
66
|
+
# Write back in place (not mv): TARGET may be a symlink (e.g. CLAUDE.md ->
|
|
67
|
+
# AGENTS.md) or have a non-default mode; mv would replace it with a plain
|
|
68
|
+
# 644 file and break the link. $TARGET.tmp is fully built above already,
|
|
69
|
+
# so this is the only step that touches TARGET.
|
|
70
|
+
cat "$TARGET.tmp" > "$TARGET" || exit 1
|
|
71
|
+
rm -f "$TARGET.tmp"
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
echo "ADDED=$ADDED SKIPPED_DUP=$SKIPPED TARGET=$TARGET"
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Stats + structured Round data for /devlog-tracker:report, `npx devlog-tracker
|
|
3
|
+
# report`, and timeline-devlog.sh (docs/design/read-side-and-promote.md B).
|
|
4
|
+
# Plain read only: never edits a devlog file. devlog_resolve_paths may still
|
|
5
|
+
# do its documented first-resolve rename, same as every other reader.
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
|
|
8
|
+
_src="${BASH_SOURCE[0]}"
|
|
9
|
+
SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
|
|
10
|
+
# shellcheck source=json-field.sh
|
|
11
|
+
. "$SCRIPT_DIR/json-field.sh"
|
|
12
|
+
# shellcheck source=devlog-md.sh
|
|
13
|
+
. "$SCRIPT_DIR/devlog-md.sh"
|
|
14
|
+
# shellcheck source=devlog-path.sh
|
|
15
|
+
. "$SCRIPT_DIR/devlog-path.sh"
|
|
16
|
+
|
|
17
|
+
JSON=0
|
|
18
|
+
ROUNDS=0
|
|
19
|
+
ALL=0
|
|
20
|
+
WITH_INPUT=0
|
|
21
|
+
for arg in "$@"; do
|
|
22
|
+
case "$arg" in
|
|
23
|
+
--json) JSON=1 ;;
|
|
24
|
+
--rounds) ROUNDS=1 ;;
|
|
25
|
+
--all-branches) ALL=1 ;;
|
|
26
|
+
--with-input) WITH_INPUT=1 ;;
|
|
27
|
+
*) echo "UNKNOWN_ARG=$arg" >&2; exit 2 ;;
|
|
28
|
+
esac
|
|
29
|
+
done
|
|
30
|
+
|
|
31
|
+
devlog_resolve_paths "${DEVLOG_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-.}}"
|
|
32
|
+
if [ ! -d "$DEVLOG_DIR" ]; then
|
|
33
|
+
if [ "$JSON" -eq 1 ]; then echo '{"started":false}'; else echo "NOT_STARTED"; fi
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# devlog.md -> main, devlog.archive.md -> archive, devlog.<x>.md -> <x>.
|
|
38
|
+
branch_of_file() {
|
|
39
|
+
local leaf="${1##*/}"
|
|
40
|
+
case "$leaf" in
|
|
41
|
+
devlog.md) printf 'main' ;;
|
|
42
|
+
devlog.archive.md) printf 'archive' ;;
|
|
43
|
+
*) leaf="${leaf#devlog.}"; printf '%s' "${leaf%.md}" ;;
|
|
44
|
+
esac
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Archive first (oldest), then the branch file(s).
|
|
48
|
+
FILES=()
|
|
49
|
+
[ -f "$DEVLOG_DIR/devlog.archive.md" ] && FILES+=("$DEVLOG_DIR/devlog.archive.md")
|
|
50
|
+
if [ "$ALL" -eq 1 ]; then
|
|
51
|
+
shopt -s nullglob
|
|
52
|
+
KEPT_NAMES=" "
|
|
53
|
+
for f in "$DEVLOG_DIR"/devlog*.md; do
|
|
54
|
+
while IFS= read -r name; do
|
|
55
|
+
[ -n "$name" ] && KEPT_NAMES="$KEPT_NAMES$name "
|
|
56
|
+
done < <(devlog_kept_index_lines "$f" | sed -nE 's/.*`devlog\.([^`]+)\.md`.*/\1/p')
|
|
57
|
+
done
|
|
58
|
+
for f in "$DEVLOG_DIR"/devlog*.md; do
|
|
59
|
+
case "${f##*/}" in devlog.archive.md|devlog.lessons.*.md) continue ;; esac
|
|
60
|
+
case "$KEPT_NAMES" in *" $(branch_of_file "$f") "*) continue ;; esac
|
|
61
|
+
FILES+=("$f")
|
|
62
|
+
done
|
|
63
|
+
shopt -u nullglob
|
|
64
|
+
else
|
|
65
|
+
[ -f "$DEVLOG_FILE" ] && FILES+=("$DEVLOG_FILE")
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
SCAN="$(mktemp "${TMPDIR:-/tmp}/devlog-report.XXXXXX")" || exit 1
|
|
69
|
+
trap 'rm -f "$SCAN"' EXIT
|
|
70
|
+
for f in ${FILES[@]+"${FILES[@]}"}; do
|
|
71
|
+
awk -v branch="$(branch_of_file "$f")" -v label="${f##*/}" -v with_input="$WITH_INPUT" \
|
|
72
|
+
-f "$SCRIPT_DIR/report-scan.awk" "$f" >> "$SCAN"
|
|
73
|
+
done
|
|
74
|
+
|
|
75
|
+
count_status() { awk -F'\t' -v s="$1" '$1 == "R" && $2 == s { c++ } END { print c + 0 }' "$SCAN"; }
|
|
76
|
+
BRANCH="$(branch_of_file "$DEVLOG_FILE")"
|
|
77
|
+
ROUNDS_TOTAL="$(awk -F'\t' '$1 == "R" { c++ } END { print c + 0 }' "$SCAN")"
|
|
78
|
+
ROUNDS_ARCHIVE="$(awk -F'\t' '$1 == "R" && $4 == "archive" { c++ } END { print c + 0 }' "$SCAN")"
|
|
79
|
+
ROUNDS_MAIN=$((ROUNDS_TOTAL - ROUNDS_ARCHIVE))
|
|
80
|
+
STATUS_DONE="$(count_status DONE)"
|
|
81
|
+
STATUS_IN_PROGRESS="$(count_status IN_PROGRESS)"
|
|
82
|
+
STATUS_BLOCKED="$(count_status BLOCKED)"
|
|
83
|
+
STATUS_INTERRUPTED="$(count_status INTERRUPTED)"
|
|
84
|
+
BLOCKED_RATIO=0
|
|
85
|
+
[ "$ROUNDS_TOTAL" -gt 0 ] && BLOCKED_RATIO=$((STATUS_BLOCKED * 100 / ROUNDS_TOTAL))
|
|
86
|
+
CHECKPOINTS="$(awk -F'\t' '$1 == "C" { c++ } END { print c + 0 }' "$SCAN")"
|
|
87
|
+
KEPT_TOPICS=0
|
|
88
|
+
if [ -f "$DEVLOG_FILE" ]; then
|
|
89
|
+
KEPT_TOPICS="$(devlog_kept_index_lines "$DEVLOG_FILE" | grep -c '^- `devlog\.' || true)"
|
|
90
|
+
fi
|
|
91
|
+
LESSONS_TOPICS=0
|
|
92
|
+
for f in "$DEVLOG_DIR"/devlog.lessons.*.md; do
|
|
93
|
+
[ -f "$f" ] && LESSONS_TOPICS=$((LESSONS_TOPICS + 1))
|
|
94
|
+
done
|
|
95
|
+
ADVISORY=""
|
|
96
|
+
if [ -f "$DEVLOG_DIR/.lessons-advisory-state" ]; then
|
|
97
|
+
a_count="$(json_int_get "$DEVLOG_DIR/.lessons-advisory-state" count)"
|
|
98
|
+
a_max="$(json_int_get "$DEVLOG_DIR/.lessons-advisory-state" threshold)"
|
|
99
|
+
ADVISORY="${a_count:-0}/${a_max:-3}"
|
|
100
|
+
fi
|
|
101
|
+
FIRST_ROUND_AT="$(awk -F'\t' '$1 == "R" && $3 != "" { print $3 }' "$SCAN" | sort | head -1)"
|
|
102
|
+
LAST_ROUND_AT="$(awk -F'\t' '$1 == "R" && $3 != "" { print $3 }' "$SCAN" | sort | tail -1)"
|
|
103
|
+
[ -n "$FIRST_ROUND_AT" ] || FIRST_ROUND_AT=none
|
|
104
|
+
[ -n "$LAST_ROUND_AT" ] || LAST_ROUND_AT=none
|
|
105
|
+
|
|
106
|
+
if [ "$JSON" -eq 0 ]; then
|
|
107
|
+
printf 'BRANCH=%s\n' "$BRANCH"
|
|
108
|
+
printf 'ROUNDS_TOTAL=%s\nROUNDS_MAIN=%s\nROUNDS_ARCHIVE=%s\n' "$ROUNDS_TOTAL" "$ROUNDS_MAIN" "$ROUNDS_ARCHIVE"
|
|
109
|
+
printf 'STATUS_DONE=%s\nSTATUS_IN_PROGRESS=%s\nSTATUS_BLOCKED=%s\nSTATUS_INTERRUPTED=%s\n' \
|
|
110
|
+
"$STATUS_DONE" "$STATUS_IN_PROGRESS" "$STATUS_BLOCKED" "$STATUS_INTERRUPTED"
|
|
111
|
+
printf 'BLOCKED_RATIO=%s\nCHECKPOINTS=%s\nKEPT_TOPICS=%s\nLESSONS_TOPICS=%s\n' \
|
|
112
|
+
"$BLOCKED_RATIO" "$CHECKPOINTS" "$KEPT_TOPICS" "$LESSONS_TOPICS"
|
|
113
|
+
[ -n "$ADVISORY" ] && printf 'LESSONS_ADVISORY=%s\n' "$ADVISORY"
|
|
114
|
+
printf 'FIRST_ROUND_AT=%s\nLAST_ROUND_AT=%s\n' "$FIRST_ROUND_AT" "$LAST_ROUND_AT"
|
|
115
|
+
exit 0
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
jstr() {
|
|
119
|
+
if [ -z "$1" ] || [ "$1" = none ]; then printf 'null'
|
|
120
|
+
else printf '"%s"' "$(json_escape "$1")"; fi
|
|
121
|
+
}
|
|
122
|
+
{
|
|
123
|
+
printf '{"started":true,"branch":"%s"' "$(json_escape "$BRANCH")"
|
|
124
|
+
printf ',"rounds_total":%s,"rounds_main":%s,"rounds_archive":%s' "$ROUNDS_TOTAL" "$ROUNDS_MAIN" "$ROUNDS_ARCHIVE"
|
|
125
|
+
printf ',"status_done":%s,"status_in_progress":%s,"status_blocked":%s,"status_interrupted":%s' \
|
|
126
|
+
"$STATUS_DONE" "$STATUS_IN_PROGRESS" "$STATUS_BLOCKED" "$STATUS_INTERRUPTED"
|
|
127
|
+
printf ',"blocked_ratio":%s,"checkpoints":%s,"kept_topics":%s,"lessons_topics":%s' \
|
|
128
|
+
"$BLOCKED_RATIO" "$CHECKPOINTS" "$KEPT_TOPICS" "$LESSONS_TOPICS"
|
|
129
|
+
printf ',"lessons_advisory":%s,"first_round_at":%s,"last_round_at":%s' \
|
|
130
|
+
"$(jstr "$ADVISORY")" "$(jstr "$FIRST_ROUND_AT")" "$(jstr "$LAST_ROUND_AT")"
|
|
131
|
+
if [ "$ROUNDS" -eq 1 ]; then
|
|
132
|
+
printf ',"rounds":['
|
|
133
|
+
awk -F'\t' '$1 == "R" { printf "%s%s", (n++ ? "," : ""), $5 }' "$SCAN"
|
|
134
|
+
printf '],"checkpoint_blocks":['
|
|
135
|
+
awk -F'\t' '$1 == "C" { printf "%s%s", (n++ ? "," : ""), $3 }' "$SCAN"
|
|
136
|
+
printf ']'
|
|
137
|
+
fi
|
|
138
|
+
printf '}\n'
|
|
139
|
+
}
|
|
140
|
+
exit 0
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# One pass over one devlog file for report-devlog.sh
|
|
2
|
+
# (docs/design/read-side-and-promote.md B). POSIX awk only (mawk / BSD awk).
|
|
3
|
+
# Vars: branch, label (file leaf name), with_input (0|1).
|
|
4
|
+
# Emits one line per block:
|
|
5
|
+
# R<TAB>status<TAB>at<TAB>branch<TAB>round-json
|
|
6
|
+
# C<TAB>branch<TAB>checkpoint-json
|
|
7
|
+
# Headings inside ``` fences are content, not structure (same rule as
|
|
8
|
+
# devlog-md.sh).
|
|
9
|
+
BEGIN {
|
|
10
|
+
# Build the 0x01-0x1F control bytes once; \t \r \n get their short escapes
|
|
11
|
+
# below, the rest (e.g. pasted ANSI \033) fall back to \u00XX.
|
|
12
|
+
for (ci = 1; ci <= 31; ci++) { cchar[ci] = sprintf("%c", ci); cesc[ci] = sprintf("\\u%04x", ci) }
|
|
13
|
+
}
|
|
14
|
+
function esc(s, ci) {
|
|
15
|
+
gsub(/\\/, "\\\\", s); gsub(/"/, "\\\"", s)
|
|
16
|
+
gsub(/\t/, "\\t", s); gsub(/\r/, "\\r", s); gsub(/\n/, "\\n", s)
|
|
17
|
+
for (ci = 1; ci <= 31; ci++) if (index(s, cchar[ci]) > 0) gsub(cchar[ci], cesc[ci], s)
|
|
18
|
+
return s
|
|
19
|
+
}
|
|
20
|
+
function trim_nl(s) { sub(/^\n+/, "", s); sub(/\n+$/, "", s); return s }
|
|
21
|
+
function flush_section() {
|
|
22
|
+
if (sec == "") return
|
|
23
|
+
if (sec == "segment") segs[nseg++] = trim_nl(buf)
|
|
24
|
+
else part[sec] = trim_nl(buf)
|
|
25
|
+
sec = ""; buf = ""
|
|
26
|
+
}
|
|
27
|
+
function flush_block( i, st, js) {
|
|
28
|
+
flush_section()
|
|
29
|
+
if (kind == "round") {
|
|
30
|
+
st = part["status"]; sub(/\n.*/, "", st); gsub(/[ \t`*]/, "", st)
|
|
31
|
+
if (st !~ /^(DONE|IN_PROGRESS|BLOCKED|INTERRUPTED)$/) st = ""
|
|
32
|
+
js = "{\"branch\":\"" esc(branch) "\",\"file\":\"" esc(label) "\",\"line\":" rline ",\"n\":" rn
|
|
33
|
+
js = js ",\"at\":\"" esc(at) "\",\"status\":\"" st "\""
|
|
34
|
+
js = js ",\"summary\":\"" esc(part["summary"]) "\",\"reply\":\"" esc(part["reply"]) "\""
|
|
35
|
+
js = js ",\"handoff\":\"" esc(part["handoff"]) "\",\"segments\":["
|
|
36
|
+
for (i = 0; i < nseg; i++) js = js (i ? "," : "") "\"" esc(segs[i]) "\""
|
|
37
|
+
js = js "]"
|
|
38
|
+
if (with_input) js = js ",\"input\":\"" esc(part["input"]) "\""
|
|
39
|
+
js = js "}"
|
|
40
|
+
printf "R\t%s\t%s\t%s\t%s\n", st, at, branch, js
|
|
41
|
+
} else if (kind == "checkpoint") {
|
|
42
|
+
printf "C\t%s\t{\"branch\":\"%s\",\"file\":\"%s\",\"line\":%d,\"heading\":\"%s\",\"body\":\"%s\"}\n", \
|
|
43
|
+
branch, esc(branch), esc(label), cline, esc(cheading), esc(trim_nl(buf_cp))
|
|
44
|
+
}
|
|
45
|
+
kind = ""; split("", part); split("", segs); nseg = 0; buf_cp = ""
|
|
46
|
+
}
|
|
47
|
+
/^[ \t]*```/ { fence = !fence }
|
|
48
|
+
!fence && /^## / {
|
|
49
|
+
flush_block()
|
|
50
|
+
if ($0 ~ /^## Round [0-9]+/) {
|
|
51
|
+
kind = "round"; rline = NR
|
|
52
|
+
rn = $0; sub(/^## Round /, "", rn); sub(/[^0-9].*$/, "", rn)
|
|
53
|
+
at = ""
|
|
54
|
+
if (index($0, "— ")) at = substr($0, index($0, "— ") + length("— "))
|
|
55
|
+
} else if ($0 ~ /^## Checkpoint/) {
|
|
56
|
+
kind = "checkpoint"; cline = NR; cheading = substr($0, 4)
|
|
57
|
+
}
|
|
58
|
+
next
|
|
59
|
+
}
|
|
60
|
+
kind == "checkpoint" { buf_cp = buf_cp $0 "\n"; next }
|
|
61
|
+
kind == "round" && !fence && /^### / {
|
|
62
|
+
flush_section()
|
|
63
|
+
h = substr($0, 5)
|
|
64
|
+
if (h ~ /^User Input[ \t]*$/) sec = "input"
|
|
65
|
+
else if (h ~ /^Summary[ \t]*$/) sec = "summary"
|
|
66
|
+
else if (h ~ /^Reply[ \t]*$/) sec = "reply"
|
|
67
|
+
else if (h ~ /^Handoff[ \t]*$/) sec = "handoff"
|
|
68
|
+
else if (h ~ /^Status[ \t]*$/) sec = "status"
|
|
69
|
+
else if (h ~ /^段落 /) { sec = "segment"; buf = h "\n"; next }
|
|
70
|
+
else sec = "other"
|
|
71
|
+
next
|
|
72
|
+
}
|
|
73
|
+
kind == "round" && sec != "" { buf = buf $0 "\n" }
|
|
74
|
+
END { flush_block() }
|
|
@@ -95,7 +95,7 @@ case "$PROMPT" in
|
|
|
95
95
|
;;
|
|
96
96
|
esac
|
|
97
97
|
case "$CMD_NAME" in
|
|
98
|
-
devlog-tracker:checkpoint|devlog-tracker:clean|devlog-tracker:compact|devlog-tracker:keep|devlog-tracker:lessons|devlog-tracker:lessons-drift|devlog-tracker:lessons-off|devlog-tracker:lessons-on|devlog-tracker:overview|devlog-tracker:pause|devlog-tracker:search|devlog-tracker:segment-watch|devlog-tracker:span|devlog-tracker:start|devlog-tracker:status)
|
|
98
|
+
devlog-tracker:checkpoint|devlog-tracker:clean|devlog-tracker:compact|devlog-tracker:keep|devlog-tracker:lessons|devlog-tracker:lessons-drift|devlog-tracker:lessons-off|devlog-tracker:lessons-on|devlog-tracker:overview|devlog-tracker:pause|devlog-tracker:report|devlog-tracker:search|devlog-tracker:segment-watch|devlog-tracker:span|devlog-tracker:start|devlog-tracker:status|devlog-tracker:timeline)
|
|
99
99
|
exit 0
|
|
100
100
|
;;
|
|
101
101
|
esac
|
|
@@ -123,6 +123,20 @@ if [ "$TASK_NOTIF" -eq 1 ]; then
|
|
|
123
123
|
else
|
|
124
124
|
PROMPT="$NOTIF_SUMMARY"
|
|
125
125
|
fi
|
|
126
|
+
|
|
127
|
+
# Lessons Mode:背景 sub agent/Workflow 完成時機械提示主 session 檢查
|
|
128
|
+
# sub agent 訊號(docs/design/lessons-mode.md「sub agent/workflow 情境」);
|
|
129
|
+
# failed/killed 另外算進共用計數器。span 開著也照印——長任務正是會派
|
|
130
|
+
# 背景 agent 的場景。
|
|
131
|
+
if [ -f "$DEVLOG_DIR/.lessons-enabled" ]; then
|
|
132
|
+
printf '\n[Lessons Mode 提示] 背景任務完成(status=%s)。讀完回報後檢查:verify 推翻先前的 fix/claim、sub agent 自陳繞路、多個 agent 卡在類似問題、成果被打回票——有的話可考慮用 lessons-append.sh 記一筆(sub agent 若已自己記過就不用重複),非強制。\n' "${NOTIF_STATUS:-unknown}"
|
|
133
|
+
case "$NOTIF_STATUS" in
|
|
134
|
+
failed|killed)
|
|
135
|
+
lessons_advisory_migrate "$DEVLOG_DIR"
|
|
136
|
+
lessons_advisory_bump "$ADVISORY_FILE"
|
|
137
|
+
;;
|
|
138
|
+
esac
|
|
139
|
+
fi
|
|
126
140
|
fi
|
|
127
141
|
|
|
128
142
|
if [ "$SPAN_SKIP" -eq 1 ]; then
|
|
@@ -70,5 +70,10 @@ assert_eq "slugify basic" "foo-bar" "$(slugify 'foo bar')"
|
|
|
70
70
|
assert_eq "slugify collapses runs" "foo-bar" "$(slugify 'foo bar')"
|
|
71
71
|
assert_eq "slugify trims edges" "foo" "$(slugify ' foo ')"
|
|
72
72
|
|
|
73
|
+
assert_eq "json_escape quotes and backslash" 'a \"q\" \\ b' "$(json_escape 'a "q" \ b')"
|
|
74
|
+
assert_eq "json_escape tab" 'x\ty' "$(json_escape "$(printf 'x\ty')")"
|
|
75
|
+
assert_eq "json_escape newline" 'l1\nl2' "$(json_escape "$(printf 'l1\nl2')")"
|
|
76
|
+
assert_eq "json_escape CJK passthrough" '中文' "$(json_escape '中文')"
|
|
77
|
+
|
|
73
78
|
if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
|
|
74
79
|
else echo "Some checks FAILED."; exit 1; fi
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Self-check for lessons-subagent-start.sh (SubagentStart) and
|
|
3
|
+
# lessons-subagent-done.sh (PostToolUse Agent|Task). Run:
|
|
4
|
+
# bash core/scripts/tests/test-lessons-subagent-hooks.sh
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
8
|
+
TMP_ROOT="$(mktemp -d)"
|
|
9
|
+
trap 'rm -rf "$TMP_ROOT"' EXIT
|
|
10
|
+
|
|
11
|
+
export CLAUDE_PROJECT_DIR="$TMP_ROOT"
|
|
12
|
+
DEVLOG_DIR="$TMP_ROOT/.devlog"
|
|
13
|
+
mkdir -p "$DEVLOG_DIR"
|
|
14
|
+
|
|
15
|
+
FAIL=0
|
|
16
|
+
assert_contains() {
|
|
17
|
+
local desc="$1" needle="$2" haystack="$3"
|
|
18
|
+
case "$haystack" in
|
|
19
|
+
*"$needle"*) echo "PASS: $desc" ;;
|
|
20
|
+
*) echo "FAIL: $desc (missing: $needle)"; FAIL=1 ;;
|
|
21
|
+
esac
|
|
22
|
+
}
|
|
23
|
+
assert_empty() {
|
|
24
|
+
local desc="$1" out="$2"
|
|
25
|
+
if [ -z "$out" ]; then echo "PASS: $desc"; else echo "FAIL: $desc (got: $out)"; FAIL=1; fi
|
|
26
|
+
}
|
|
27
|
+
assert_valid_json() {
|
|
28
|
+
local desc="$1" out="$2"
|
|
29
|
+
if ! command -v jq >/dev/null 2>&1; then echo "SKIP: $desc (no jq)"; return 0; fi
|
|
30
|
+
if printf '%s' "$out" | jq -e . >/dev/null 2>&1; then echo "PASS: $desc"; else echo "FAIL: $desc (invalid JSON: $out)"; FAIL=1; fi
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
START_IN='{"session_id":"s1","cwd":"/elsewhere/.claude/worktrees/agent-x","agent_id":"a1","agent_type":"general-purpose","hook_event_name":"SubagentStart"}'
|
|
34
|
+
SYNC_DONE='{"session_id":"s1","hook_event_name":"PostToolUse","tool_name":"Agent","tool_input":{"prompt":"p"},"tool_response":{"status":"completed","content":[{"type":"text","text":"done"}]}}'
|
|
35
|
+
ASYNC_DONE='{"session_id":"s1","hook_event_name":"PostToolUse","tool_name":"Agent","tool_input":{"prompt":"p"},"tool_response":{"isAsync":true,"status":"async_launched","agentId":"a1"}}'
|
|
36
|
+
NESTED_DONE='{"session_id":"s1","agent_id":"a1","hook_event_name":"PostToolUse","tool_name":"Agent","tool_input":{"prompt":"p"},"tool_response":{"status":"completed"}}'
|
|
37
|
+
|
|
38
|
+
# --- disabled: both hooks silent -------------------------------------------
|
|
39
|
+
OUT="$(printf '%s' "$START_IN" | bash "$SCRIPT_DIR/lessons-subagent-start.sh")"
|
|
40
|
+
assert_empty "start: .enabled absent -> silent" "$OUT"
|
|
41
|
+
OUT="$(printf '%s' "$SYNC_DONE" | bash "$SCRIPT_DIR/lessons-subagent-done.sh")"
|
|
42
|
+
assert_empty "done: .enabled absent -> silent" "$OUT"
|
|
43
|
+
|
|
44
|
+
touch "$DEVLOG_DIR/.enabled"
|
|
45
|
+
OUT="$(printf '%s' "$START_IN" | bash "$SCRIPT_DIR/lessons-subagent-start.sh")"
|
|
46
|
+
assert_empty "start: lessons off -> silent" "$OUT"
|
|
47
|
+
OUT="$(printf '%s' "$SYNC_DONE" | bash "$SCRIPT_DIR/lessons-subagent-done.sh")"
|
|
48
|
+
assert_empty "done: lessons off -> silent" "$OUT"
|
|
49
|
+
|
|
50
|
+
touch "$DEVLOG_DIR/.lessons-enabled"
|
|
51
|
+
|
|
52
|
+
# --- SubagentStart: inject lessons-append.sh instructions with absolute paths
|
|
53
|
+
OUT="$(printf '%s' "$START_IN" | bash "$SCRIPT_DIR/lessons-subagent-start.sh")"
|
|
54
|
+
assert_valid_json "start: output is valid JSON" "$OUT"
|
|
55
|
+
assert_contains "start: hookEventName SubagentStart" '"hookEventName":"SubagentStart"' "$OUT"
|
|
56
|
+
assert_contains "start: additionalContext present" '"additionalContext":' "$OUT"
|
|
57
|
+
assert_contains "start: pins the main project dir, not the agent cwd" "DEVLOG_PROJECT_DIR='$TMP_ROOT'" "$OUT"
|
|
58
|
+
assert_contains "start: absolute lessons-append.sh path" "$SCRIPT_DIR/lessons-append.sh" "$OUT"
|
|
59
|
+
assert_contains "start: asks to mention it in the final report" "最終回報" "$OUT"
|
|
60
|
+
|
|
61
|
+
# relative CLAUDE_PROJECT_DIR still resolves to an absolute path
|
|
62
|
+
(cd "$TMP_ROOT" && OUT="$(printf '%s' "$START_IN" | CLAUDE_PROJECT_DIR=. bash "$SCRIPT_DIR/lessons-subagent-start.sh")"
|
|
63
|
+
case "$OUT" in *"DEVLOG_PROJECT_DIR='$TMP_ROOT'"*|*"DEVLOG_PROJECT_DIR='$(pwd -P)'"*) echo "PASS: start: relative project dir made absolute" ;;
|
|
64
|
+
*) echo "FAIL: start: relative project dir not made absolute ($OUT)"; exit 1 ;; esac) || FAIL=1
|
|
65
|
+
|
|
66
|
+
# path needing JSON escaping stays valid JSON
|
|
67
|
+
QDIR="$TMP_ROOT/we\"ird"
|
|
68
|
+
mkdir -p "$QDIR/.devlog"
|
|
69
|
+
touch "$QDIR/.devlog/.enabled" "$QDIR/.devlog/.lessons-enabled"
|
|
70
|
+
OUT="$(printf '%s' "$START_IN" | CLAUDE_PROJECT_DIR="$QDIR" bash "$SCRIPT_DIR/lessons-subagent-start.sh")"
|
|
71
|
+
assert_valid_json "start: quote in project path still valid JSON" "$OUT"
|
|
72
|
+
|
|
73
|
+
# --- PostToolUse(Agent): foreground completion only -------------------------
|
|
74
|
+
OUT="$(printf '%s' "$SYNC_DONE" | bash "$SCRIPT_DIR/lessons-subagent-done.sh")"
|
|
75
|
+
assert_valid_json "done: output is valid JSON" "$OUT"
|
|
76
|
+
assert_contains "done: hookEventName PostToolUse" '"hookEventName":"PostToolUse"' "$OUT"
|
|
77
|
+
assert_contains "done: sub agent advisory" "[Lessons Mode 提示] sub agent 完成" "$OUT"
|
|
78
|
+
|
|
79
|
+
OUT="$(printf '%s' "$ASYNC_DONE" | bash "$SCRIPT_DIR/lessons-subagent-done.sh")"
|
|
80
|
+
assert_empty "done: async launch -> silent (task-notification covers it)" "$OUT"
|
|
81
|
+
|
|
82
|
+
OUT="$(printf '%s' "$NESTED_DONE" | bash "$SCRIPT_DIR/lessons-subagent-done.sh")"
|
|
83
|
+
assert_empty "done: nested (agent_id set) -> silent" "$OUT"
|
|
84
|
+
|
|
85
|
+
OUT="$(printf '%s' 'not json' | bash "$SCRIPT_DIR/lessons-subagent-done.sh")"
|
|
86
|
+
RC=$?
|
|
87
|
+
[ "$RC" -eq 0 ] && echo "PASS: done: garbage stdin fails open" || { echo "FAIL: done: garbage stdin exit $RC"; FAIL=1; }
|
|
88
|
+
|
|
89
|
+
if [ "$FAIL" -eq 0 ]; then
|
|
90
|
+
echo "All checks passed."
|
|
91
|
+
exit 0
|
|
92
|
+
else
|
|
93
|
+
echo "Some checks FAILED."
|
|
94
|
+
exit 1
|
|
95
|
+
fi
|