Summary
In a long-running project, journal/sessions.jsonl records "slice":"none" and "task":"none" on nearly every session-end entry, even while real slices/tasks were active. session_id is also always "unknown". The SessionEnd hook is faithful — the bug is upstream: the lifecycle skills don't keep active_slice/active_task in STATE.md frontmatter in sync, and the hook reads session_id from the wrong source.
Evidence (real project, ~12 entries)
{"timestamp":"2026-05-30T23:08:27Z","event":"session-end","milestone":"M004","slice":"none","task":"none","session_id":"unknown"}
{"timestamp":"2026-05-31T00:41:28Z","event":"session-end","milestone":"M005","slice":"none","task":"none","session_id":"unknown"}
milestone tracks correctly; slice/task are stuck at none.
Root cause #1 — STATE.md frontmatter drift (primary)
hooks/session-end reads three frontmatter fields verbatim:
get_field() { sed -n "s/^$1: *//p" "$STATE_FILE" 2>/dev/null | head -1; }
CURRENT_MILESTONE=$(get_field current_milestone)
ACTIVE_SLICE=$(get_field active_slice)
ACTIVE_TASK=$(get_field active_task)
The hook is correct. But git shows the skills only reliably advance current_milestone — active_slice/active_task are left at the literal string none:
| Commit (era) |
current_milestone |
active_slice |
active_task |
| init |
none |
none |
none |
| M002 |
M002 |
S05 |
none |
| M005 |
M005 |
none |
none |
| M006 (now) |
M006 |
S01 |
T01 |
So at M005 a slice was clearly active, but the frontmatter said none, and the journal dutifully recorded none. The :-none fallback in the hook hides nothing here — the field literally contained none.
Fix: plan-task, slice-milestone, and summarize-task must write back active_slice/active_task to STATE.md frontmatter as part of their state transitions (set on entry, clear to none only when genuinely idle). This is the same SSOT discipline already applied to current_milestone.
Root cause #2 — session_id always "unknown" (secondary)
--arg session_id "${CLAUDE_SESSION_ID:-unknown}"
CLAUDE_SESSION_ID is not part of the hook environment. Claude Code delivers session_id (and cwd, hook_event_name, etc.) as a JSON payload on stdin, not as an env var. The hook should parse stdin, e.g.:
INPUT=$(cat)
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // "unknown"')
As written, every entry will read unknown.
Suggested acceptance
- A session ended mid-task records the actual
S##/T## in sessions.jsonl.
session_id reflects the real Claude Code session id.
- Skills that change slice/task state update the
STATE.md frontmatter in the same step.
Summary
In a long-running project,
journal/sessions.jsonlrecords"slice":"none"and"task":"none"on nearly everysession-endentry, even while real slices/tasks were active.session_idis also always"unknown". The SessionEnd hook is faithful — the bug is upstream: the lifecycle skills don't keepactive_slice/active_taskinSTATE.mdfrontmatter in sync, and the hook readssession_idfrom the wrong source.Evidence (real project, ~12 entries)
{"timestamp":"2026-05-30T23:08:27Z","event":"session-end","milestone":"M004","slice":"none","task":"none","session_id":"unknown"} {"timestamp":"2026-05-31T00:41:28Z","event":"session-end","milestone":"M005","slice":"none","task":"none","session_id":"unknown"}milestonetracks correctly;slice/taskare stuck atnone.Root cause #1 — STATE.md frontmatter drift (primary)
hooks/session-endreads three frontmatter fields verbatim:The hook is correct. But
gitshows the skills only reliably advancecurrent_milestone—active_slice/active_taskare left at the literal stringnone:So at M005 a slice was clearly active, but the frontmatter said
none, and the journal dutifully recordednone. The:-nonefallback in the hook hides nothing here — the field literally containednone.Fix:
plan-task,slice-milestone, andsummarize-taskmust write backactive_slice/active_tasktoSTATE.mdfrontmatter as part of their state transitions (set on entry, clear tononeonly when genuinely idle). This is the same SSOT discipline already applied tocurrent_milestone.Root cause #2 — session_id always "unknown" (secondary)
--arg session_id "${CLAUDE_SESSION_ID:-unknown}"CLAUDE_SESSION_IDis not part of the hook environment. Claude Code deliverssession_id(andcwd,hook_event_name, etc.) as a JSON payload on stdin, not as an env var. The hook should parse stdin, e.g.:As written, every entry will read
unknown.Suggested acceptance
S##/T##insessions.jsonl.session_idreflects the real Claude Code session id.STATE.mdfrontmatter in the same step.