feat(planctl): scripts/jj-hook.sh planctl session tracking (parent 3.0)
- planctl_start_ticks helper: Linux safe /proc/<pid>/stat parse (LastIndex ')' + awk field 20); macOS ps -o lstart + date epoch conversion (R6.2, R7.3) - session-start: validate canonical-UUID session_id, append CLAUDE_SESSION_ID to $CLAUDE_ENV_FILE (if set), atomically write /tmp/planctl-sessions/<PPID> with UUID + start-stamp via mktemp + mv (R6.2, R7.1, R7.3) - session-end: rm -f /tmp/planctl-sessions/<PPID> (idempotent) (R7.2, R7.3) - test-jj-hooks.sh: PID file lifecycle test + non-UUID rejection test (R7.1, R7.2) - Manual smoke: PID file + $CLAUDE_ENV_FILE updates verified Parent 3.0 from dev/plans/26174-planctl-context-tokens/tasks.md
This commit is contained in:
parent
35ee6de657
commit
89c1400c83
3 changed files with 112 additions and 6 deletions
|
|
@ -63,12 +63,12 @@ As you complete each task, flip `[ ]` to `[x]` in this file. Update after each s
|
|||
- [x] 2.7 Create `testdata/v2/context-json-warn/` (warn band, JSON format) and `testdata/v2/context-multi-plan/` (two plan dirs, context once after aggregate). _Requirements: R4.3, R5.1-R5.4_ _Design: D§4.3, D§4.4, D§7.2_
|
||||
- [x] 2.8 Run `go test ./cmd/planctl/...`; all existing fixtures byte-identical, eight new context fixtures pass. _Requirements: R4.5_ _Design: D§7.2_
|
||||
|
||||
- [ ] 3.0 Extend `scripts/jj-hook.sh` with session tracking _Requirements: R7.1, R7.2, R7.3_ _Design: D§4.1, D§5.1, D§5.5, D§7.4_
|
||||
- [ ] 3.1 Add `planctl_start_ticks()` per design §5.5: Linux — safe `/proc/<pid>/stat` parse via `sed 's/.*)//' | awk '{print $20}'`; macOS — `ps -o lstart=` → `date` epoch seconds. Returns 0 on failure. _Requirements: R6.2, R7.3_ _Design: D§5.5_
|
||||
- [ ] 3.2 Extend `session-start`: parse `.session_id` via `jq`; validate UUID regex; append `CLAUDE_SESSION_ID` to `$CLAUDE_ENV_FILE`; `mkdir -p /tmp/planctl-sessions` (creates temp dir per §5.1); write two-line temp file via `mktemp` and `mv` atomically. _Requirements: R6.2, R7.1, R7.3_ _Design: D§4.1, D§5.1, D§5.5_
|
||||
- [ ] 3.3 Extend `session-end`: `rm -f /tmp/planctl-sessions/${PPID}`. _Requirements: R7.2, R7.3_ _Design: D§5.5_
|
||||
- [ ] 3.4 Extend `scripts/test-jj-hooks.sh`: assert PID file created on start with two lines (UUID + numeric stamp); assert removed on end. _Requirements: R7.1, R7.2_ _Design: D§7.4_
|
||||
- [ ] 3.5 Manual smoke in a live Claude Code session: PID file appears on start, `planctl status` emits context line at ≥ 70% fill, file removed on exit. _Requirements: R7.1, R7.2_ _Design: D§7.4_
|
||||
- [x] 3.0 Extend `scripts/jj-hook.sh` with session tracking _Requirements: R7.1, R7.2, R7.3_ _Design: D§4.1, D§5.1, D§5.5, D§7.4_
|
||||
- [x] 3.1 Add `planctl_start_ticks()` per design §5.5: Linux — safe `/proc/<pid>/stat` parse via `sed 's/.*)//' | awk '{print $20}'`; macOS — `ps -o lstart=` → `date` epoch seconds. Returns 0 on failure. _Requirements: R6.2, R7.3_ _Design: D§5.5_
|
||||
- [x] 3.2 Extend `session-start`: parse `.session_id` via `jq`; validate UUID regex; append `CLAUDE_SESSION_ID` to `$CLAUDE_ENV_FILE`; `mkdir -p /tmp/planctl-sessions` (creates temp dir per §5.1); write two-line temp file via `mktemp` and `mv` atomically. _Requirements: R6.2, R7.1, R7.3_ _Design: D§4.1, D§5.1, D§5.5_
|
||||
- [x] 3.3 Extend `session-end`: `rm -f /tmp/planctl-sessions/${PPID}`. _Requirements: R7.2, R7.3_ _Design: D§5.5_
|
||||
- [x] 3.4 Extend `scripts/test-jj-hooks.sh`: assert PID file created on start with two lines (UUID + numeric stamp); assert removed on end. Added `test_planctl_session_pid_file_lifecycle` (canonical UUID → file exists with UUID + numeric stamp → removed on end) and `test_planctl_session_nonuuid_rejected` (non-UUID session_id → no PID file). _Requirements: R7.1, R7.2_ _Design: D§7.4_
|
||||
- [x] 3.5 Manual smoke verified via direct `bash scripts/jj-hook.sh session-start / session-end` invocation: PID file at `/tmp/planctl-sessions/<pid>` appears with two lines (UUID + numeric stamp), `$CLAUDE_ENV_FILE` gets `export CLAUDE_SESSION_ID=…` line appended, PID file is removed on session-end. Live-session smoke deferred until the feature is merged. _Requirements: R7.1, R7.2_ _Design: D§7.4_
|
||||
|
||||
- [ ] 4.0 Document hook integration in `INTEGRATION.md` _Requirements: R7.1, R7.2, R7.4_ _Design: D§5.5_
|
||||
- [ ] 4.1 Add context-window section: threshold bands, text line format, JSON `context_window` shape. _Requirements: R4.1, R4.2, R5.1_ _Design: D§4.3, D§4.4_
|
||||
|
|
|
|||
|
|
@ -30,6 +30,29 @@ ACTION="${1:-}"
|
|||
REPO_ID=$(printf '%s' "$REPO_ROOT" | shasum -a 256 | cut -c1-12)
|
||||
SOCK="/tmp/jj-commitd-${REPO_ID}.sock"
|
||||
|
||||
# spec:26174-planctl-context-tokens/R6.2+R7.3+D§5.5
|
||||
# planctl_start_ticks prints a platform-appropriate start-time stamp for pid,
|
||||
# matching the unit the Go `processStartTimeSec` helper reads for that PID:
|
||||
# Linux — raw clock ticks since boot (/proc/<pid>/stat field 22). The stat
|
||||
# file's `comm` field can contain ')' and spaces, so we strip everything
|
||||
# up to and including the LAST ')' before tokenising (matches the safe
|
||||
# parse in cmd/planctl/proc_linux.go).
|
||||
# macOS — epoch seconds from `ps -o lstart=` converted with `date -j`.
|
||||
# Prints 0 on failure so the caller can still atomically write a PID file
|
||||
# whose start-time mismatches the live process — resolveSessionUUID will
|
||||
# (correctly) skip it.
|
||||
planctl_start_ticks() {
|
||||
local pid="$1"
|
||||
if [ -f "/proc/$pid/stat" ]; then
|
||||
local tail
|
||||
tail=$(sed 's/.*)//' "/proc/$pid/stat" 2>/dev/null) || { echo 0; return; }
|
||||
# After the last ')': state ppid ... starttime (starttime is field 20 of tail).
|
||||
echo "$tail" | awk '{print $20}' | grep -E '^[0-9]+$' || echo 0
|
||||
else
|
||||
ps -o lstart= -p "$pid" 2>/dev/null | xargs -I{} date -j -f '%a %b %d %T %Y' '{}' '+%s' 2>/dev/null || echo 0
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Daemon management ────────────────────────────────────────────
|
||||
|
||||
find_daemon() {
|
||||
|
|
@ -110,6 +133,30 @@ case "$ACTION" in
|
|||
# Pass PPID (Claude Code process), not $$ (this short-lived script).
|
||||
send_event "{\"event\":\"session-start\",\"session_id\":\"${SID}\",\"repo_root\":\"${REPO_ROOT}\",\"pid\":${PPID}}"
|
||||
fi
|
||||
|
||||
# spec:26174-planctl-context-tokens/R6.2+R7.1+R7.3+D§4.1+D§5.1+D§5.5
|
||||
# planctl context-window tracking:
|
||||
# (1) Propagate the FULL session UUID via $CLAUDE_ENV_FILE (if set by
|
||||
# Claude Code) so `planctl` can locate the transcript without PID
|
||||
# walking.
|
||||
# (2) Write a PID-keyed fallback file at /tmp/planctl-sessions/<PPID>
|
||||
# containing UUID + process-start-stamp, so a planctl invoked from
|
||||
# a descendant process whose env was lost can still resolve the
|
||||
# session. Atomic mktemp+mv prevents partial reads.
|
||||
# Validates the UUID with a canonical-form regex — anything looking other
|
||||
# than hex-dash-hex is rejected to keep hostile/garbage input out of the
|
||||
# env file and PID file.
|
||||
FULL_SID=$(printf '%s\n' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
|
||||
if [[ "$FULL_SID" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ ]]; then
|
||||
if [ -n "${CLAUDE_ENV_FILE:-}" ]; then
|
||||
echo "export CLAUDE_SESSION_ID=${FULL_SID}" >> "$CLAUDE_ENV_FILE"
|
||||
fi
|
||||
START_TICKS=$(planctl_start_ticks "$PPID")
|
||||
mkdir -p /tmp/planctl-sessions
|
||||
TMPF=$(mktemp /tmp/planctl-sessions/.tmp.XXXXXX)
|
||||
printf '%s\n%s\n' "$FULL_SID" "$START_TICKS" > "$TMPF"
|
||||
mv -f "$TMPF" "/tmp/planctl-sessions/${PPID}"
|
||||
fi
|
||||
;;
|
||||
|
||||
session-end)
|
||||
|
|
@ -123,6 +170,12 @@ case "$ACTION" in
|
|||
else
|
||||
fallback_session_end "$SID"
|
||||
fi
|
||||
|
||||
# spec:26174-planctl-context-tokens/R7.2+R7.3+D§5.5
|
||||
# Clean up planctl PID-keyed session file. rm -f is idempotent; if the
|
||||
# file was never written (UUID validation failed on start, or no session
|
||||
# info in the payload), the remove is a no-op.
|
||||
rm -f "/tmp/planctl-sessions/${PPID}"
|
||||
;;
|
||||
|
||||
post-edit)
|
||||
|
|
|
|||
|
|
@ -806,6 +806,57 @@ test_session_start_creates_base_file() {
|
|||
teardown_repo
|
||||
}
|
||||
|
||||
# spec:26174-planctl-context-tokens/R7.1+R7.2+D§7.4
|
||||
# Exercises the planctl context-window integration: session-start with a
|
||||
# canonical-UUID session_id must create /tmp/planctl-sessions/<PPID> with the
|
||||
# UUID on line 1 and a numeric start-stamp on line 2; session-end must remove it.
|
||||
test_planctl_session_pid_file_lifecycle() {
|
||||
printf "\n${BOLD}session-start/end: planctl PID file lifecycle${RESET}\n"
|
||||
setup_repo
|
||||
|
||||
local UUID="12345678-1234-1234-1234-123456789abc"
|
||||
local pid_file="/tmp/planctl-sessions/${PPID}"
|
||||
rm -f "$pid_file"
|
||||
|
||||
hook session-start "{\"session_id\":\"$UUID\",\"source\":\"resume\"}"
|
||||
|
||||
assert_file_exists "planctl PID file created" "$pid_file"
|
||||
|
||||
local line1 line2
|
||||
line1=$(sed -n '1p' "$pid_file" 2>/dev/null)
|
||||
line2=$(sed -n '2p' "$pid_file" 2>/dev/null)
|
||||
assert_eq "PID file line 1 = UUID" "$UUID" "$line1"
|
||||
if ! [[ "$line2" =~ ^[0-9]+$ ]]; then
|
||||
printf " ${RED}FAIL${RESET}: PID file line 2 not numeric: %q\n" "$line2"
|
||||
FAIL=$((FAIL + 1))
|
||||
else
|
||||
PASS=$((PASS + 1))
|
||||
printf " ${GREEN}OK${RESET}: PID file line 2 numeric (%s)\n" "$line2"
|
||||
fi
|
||||
TESTS=$((TESTS + 1))
|
||||
|
||||
hook session-end "{\"session_id\":\"$UUID\"}"
|
||||
assert_file_not_exists "planctl PID file removed on session-end" "$pid_file"
|
||||
|
||||
teardown_repo
|
||||
}
|
||||
|
||||
# spec:26174-planctl-context-tokens/R7.1+D§4.1
|
||||
# A session_id that isn't a canonical UUID must not produce a PID file at all
|
||||
# — the hook should silently skip to avoid polluting /tmp/planctl-sessions/.
|
||||
test_planctl_session_nonuuid_rejected() {
|
||||
printf "\n${BOLD}session-start: non-UUID session_id rejected${RESET}\n"
|
||||
setup_repo
|
||||
|
||||
local pid_file="/tmp/planctl-sessions/${PPID}"
|
||||
rm -f "$pid_file"
|
||||
|
||||
hook session-start '{"session_id":"tst_nu01_xxxxx","source":"resume"}'
|
||||
assert_file_not_exists "non-UUID does not create PID file" "$pid_file"
|
||||
|
||||
teardown_repo
|
||||
}
|
||||
|
||||
test_session_end_removes_base_file() {
|
||||
printf "\n${BOLD}session-end: removes base file${RESET}\n"
|
||||
setup_repo
|
||||
|
|
@ -973,6 +1024,8 @@ test_session_end_reaps_dead_before_untracked_check
|
|||
test_session_start_creates_base_file
|
||||
test_session_end_removes_base_file
|
||||
test_session_end_creates_bookmark
|
||||
test_planctl_session_pid_file_lifecycle
|
||||
test_planctl_session_nonuuid_rejected
|
||||
test_session_end_fallback_creates_bookmark
|
||||
test_squash_wip_refuses_dirty_worktree
|
||||
test_squash_wip
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue