skills/session-log/SKILL.md
Structured decision records for session logs and merge logs at phase boundaries
npx skillsauth add jankneumann/agentic-coding-tools session-logInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Provides utilities for appending structured decision records to session-log.md (per-change) and docs/merge-logs/YYYY-MM-DD.md (per-merge-session). Each workflow skill calls these at phase boundaries to build a living decision record.
scripts/extract_session_log.pyAppend-based utilities for session log and merge log management.
Functions:
append_phase_entry(change_id, phase_name, content, session_log_path=None) — Append a phase entry to session-log.md. Creates the file with header if missing. Returns the Path.append_merge_entry(date, content, merge_log_path=None) — Append a merge session entry to a dated merge-log file. Creates the file with header if missing. Returns the Path.count_phase_iterations(phase_prefix, session_log_path) — Count existing iteration entries for auto-incrementing <N> in phase names like "Plan Iteration 2".generate_self_summary_prompt(change_id) — Reference template for structuring phase entries (optional utility).Usage from Python:
from extract_session_log import append_phase_entry, count_phase_iterations
n = count_phase_iterations("Plan Iteration", "openspec/changes/my-change/session-log.md") + 1
append_phase_entry("my-change", f"Plan Iteration {n}", content)
Usage from SKILL.md (agent writes directly):
Skills embed the phase entry template directly. The agent writes the markdown content, then the skill runs sanitization. The Python functions are optional consistency helpers.
scripts/sanitize_session_log.pyDetects and redacts secrets, high-entropy strings, and environment-specific paths from session log content before it is committed to git. Supports in-place operation (same path for input and output).
# In-place sanitization (recommended)
python3 scripts/sanitize_session_log.py session-log.md session-log.md
# Separate output
python3 scripts/sanitize_session_log.py input.md output.md
# Dry run
python3 scripts/sanitize_session_log.py input.md output.md --dry-run
What gets redacted: AWS keys, GitHub tokens, Anthropic/OpenAI keys, connection strings, private key headers, password fields, high-entropy strings (>4.5 bits/char, >20 chars).
What is preserved: Git SHAs, UUIDs, OpenSpec change-ids, file paths (normalized), kebab-case identifiers.
Exit codes: 0 = success, 1 = sanitization error (do NOT commit the output).
scripts/phase_record.py (preferred API)Unified data model that renders to both session-log markdown AND coordinator handoff JSON from a single in-memory object. Use this in place of append_phase_entry for new code.
Dataclasses: PhaseRecord, Decision, Alternative, TradeOff, FileRef, PhaseWriteResult.
Usage from Python (skill entry-point):
from phase_record import PhaseRecord, Decision, FileRef
record = PhaseRecord(
change_id="my-change",
phase_name="Plan",
agent_type="claude_code",
summary="Selected approach A; deferred X.",
decisions=[
Decision(title="Use approach A", rationale="...", capability="skill-workflow"),
],
relevant_files=[FileRef(path="src/foo.py", description="entrypoint")],
)
result = record.write_both()
# result.markdown_path → Path to session-log.md
# result.handoff_id → coordinator handoff_id (or None on fallback)
# result.handoff_local_path → openspec/changes/<id>/handoffs/<phase>-<N>.json on fallback
# result.warnings → list of soft-failure messages (each step is best-effort)
Persistence pipeline (3 best-effort steps; failures log warnings but never raise):
openspec/changes/<change-id>/session-log.mdsanitize_session_log.py on the filetry_handoff_write via coordination_bridge, OR fallback to openspec/changes/<change-id>/handoffs/<phase-slug>-<N>.jsonRound-trip APIs:
record.render_markdown() → markdown (preserves architectural: and supersedes: inline spans)parse_markdown(text, change_id=...) → reconstructs PhaseRecordrecord.to_handoff_payload() → dict matching HandoffService.write argumentsPhaseRecord.from_handoff_payload(d, change_id=..., phase_name=...) → reconstructsWhy prefer this over append_phase_entry: append_phase_entry is now a deprecation-warned shim that internally constructs a minimal PhaseRecord and calls write_both(). Using PhaseRecord(...).write_both() directly gives you structured fields (Decisions, Alternatives, Trade-offs, Completed Work, etc.) instead of free-form markdown content, which the coordinator can consume via read_handoff and the next phase can hydrate via PhaseRecord.from_handoff_payload.
Each workflow phase appends a section following this structure:
---
## Phase: <phase-name> (<YYYY-MM-DD>)
**Agent**: <agent-type> | **Session**: <session-id-or-N/A>
### Decisions
1. **<Decision title>** `architectural: <capability>` — <rationale>
2. **<Routine decision title>** — <rationale>
### Alternatives Considered
- <Alternative>: rejected because <reason>
### Trade-offs
- Accepted <X> over <Y> because <reason>
### Open Questions
- [ ] <unresolved question>
### Completed Work
- <concrete deliverable landed in this phase>
### In Progress
- <work started but not finished>
### Next Steps
- <what the next phase should pick up>
### Relevant Files
- `<path/to/file.py>` — <short description of why this file matters>
### Context
<2-3 sentences: what was the goal, what happened>
Section names must be identical across all skills: Decisions, Alternatives Considered, Trade-offs, Open Questions, Completed Work, In Progress, Next Steps, Relevant Files, Context.
The four added sections (Completed Work, In Progress, Next Steps, Relevant Files) are optional — omit them when empty. They mirror the structured PhaseRecord fields written to the coordinator handoff (see skills/session-log/scripts/phase_record.py), so a skill using PhaseRecord(...).write_both() produces matching content in both session-log.md and handoff_documents.
When no decisions were made (e.g., validation passed cleanly): include Context, write "No significant decisions required" in Decisions, omit other sections.
Decisions that shape how a capability behaves across multiple changes SHOULD carry an inline `architectural: <capability>` tag so they are picked up by the per-capability decision index (docs/decisions/<capability>.md, regenerated by make decisions).
Syntax — a backtick-delimited inline code span placed after the title and before the — delimiter:
1. **Pin worktrees during overnight pauses** `architectural: software-factory-tooling` — prevents GC during idle
2. **Use `--` separator for parallel agent branches** `architectural: software-factory-tooling` — `/` would collide with parent feature branch ref
3. **Extend Phase Entry with archetype hints** `architectural: skill-workflow` — needed for archetype routing
<capability> MUST be a kebab-case identifier (lowercase letters, digits, hyphens; 2–52 chars) that matches a directory name under openspec/specs/. Invalid capabilities (unknown name, bad format) are skipped in non-strict mode and fail CI in --strict mode.
When to tag — decisions that will be referenced, extended, or reversed by later work. Think: "would someone doing archaeology on this capability six months from now care why we chose this?"
When NOT to tag — routine engineering choices that do not outlive the current change (library selection within a dominant pattern, choice of internal helper name, etc.). Untagged decisions remain valid session-log entries — they just do not appear in the per-capability index.
Supersession — if a later decision reverses an earlier one, add a second inline span `supersedes: <earlier-change-id>#D<n>`:
1. **Replace Beads with built-in tracker** `architectural: agent-coordinator` `supersedes: 2026-02-xx-add-beads-integration#D1` — reduces vendor surface
The decision index then renders bidirectional Supersedes / Superseded by links between the two entries. #D<n> is the decision's 1-indexed position within its phase entry (e.g., #D1 for the first bullet in Decisions).
If the target change has multiple phases that both carry a D<n> at the same bullet position (e.g., Plan D1 AND Implementation D1), use the phased form `supersedes: <change-id>#<phase-slug>/D<n>` — e.g., `supersedes: 2026-02-10-example#plan/D1` — to disambiguate. The phase slug is the phase name lowercased with spaces replaced by hyphens. The bare form is accepted when unambiguous (target change has only one phase at that bullet index); when ambiguous, the emitter emits a warning and skips the link rather than silently marking every matching phase as superseded.
Every skill follows this 3-step pattern:
sanitize_session_log.py on the file (in-place)[REDACTED:*] markers in prose where original had no secretspython3 "<skill-base-dir>/../session-log/scripts/sanitize_session_log.py" \
"openspec/changes/<change-id>/session-log.md" \
"openspec/changes/<change-id>/session-log.md"
| Skill | Phase Name |
|-------|-----------|
| plan-feature | Plan |
| iterate-on-plan | Plan Iteration <N> |
| implement-feature | Implementation |
| iterate-on-implementation | Implementation Iteration <N> |
| validate-feature | Validation |
| cleanup-feature | Cleanup |
| merge-pull-requests | (uses merge-log, not session-log) |
This skill is called by workflow skills at phase boundaries:
docs/merge-logs/YYYY-MM-DD.mdskills/.venv/bin/python -m pytest skills/session-log/scripts/ -v
development
Open the artifacts relevant to a review (OpenSpec proposal, branch changes, or explicit paths) in VS Code, in a curated read-order, in the right worktree.
tools
Render and seed coordinator-owned task status block in OpenSpec tasks.md
testing
User-invocable skill that omits the tail block
tools
Missing several required keys