| name | session-log |
| description | Structured decision records for session logs and merge logs at phase boundaries |
| category | Infrastructure |
| tags | ["logging","session","merge-log","decision-record","infrastructure"] |
| triggers | ["session log","log session"] |
Session Log — Infrastructure Skill
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
scripts/extract_session_log.py
Append-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.py
Detects 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).
python3 scripts/sanitize_session_log.py session-log.md session-log.md
python3 scripts/sanitize_session_log.py input.md output.md
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, CapabilityGap, 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()
Persistence pipeline (3 best-effort steps; failures log warnings but never raise):
- append markdown →
openspec/changes/<change-id>/session-log.md
- sanitize in-place → runs
sanitize_session_log.py on the file
- coordinator write →
try_handoff_write via coordination_bridge, OR fallback to openspec/changes/<change-id>/handoffs/<phase-slug>-<N>.json
Round-trip APIs:
record.render_markdown() → markdown (preserves architectural: and supersedes: inline spans)
parse_markdown(text, change_id=...) → reconstructs PhaseRecord
record.to_handoff_payload() → dict matching HandoffService.write arguments
PhaseRecord.from_handoff_payload(d, change_id=..., phase_name=...) → reconstructs
Why 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.
Phase Entry Template
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>
### Capability Gaps Observed
- **<failure_type>**: <capability_gap description> (skill: <affected_skill>, severity: <low|medium|high|critical>)
### 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, Capability Gaps Observed, Open Questions, Completed Work, In Progress, Next Steps, Relevant Files, Context.
The five added sections (Capability Gaps Observed, 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.
Capability Gaps Observed captures gaps the agent noticed during the phase. Each entry uses the D4 shared tag schema (failure_type, capability_gap, affected_skill, severity). On write_both(), each gap is also emitted to episodic memory with source:session-log. See docs/guides/memory-conventions.md for the full tag schema.
When no decisions were made (e.g., validation passed cleanly): include Context, write "No significant decisions required" in Decisions, omit other sections.
Tagging decisions for the per-capability index
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 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.
Append-Sanitize-Verify Flow
Every skill follows this 3-step pattern:
- APPEND: Agent writes phase entry to session-log.md (or merge-log)
- SANITIZE: Run
sanitize_session_log.py on the file (in-place)
- VERIFY: Agent reads sanitized output and checks:
- All phase entry sections are present (or intentionally omitted)
- No
[REDACTED:*] markers in prose where original had no secrets
- Markdown structure is intact
- If over-redacted: rewrite without the triggering pattern, re-sanitize (one attempt max)
- If sanitization exits non-zero: do NOT commit, log warning, continue workflow
python3 "<skill-base-dir>/../session-log/scripts/sanitize_session_log.py" \
"openspec/changes/<change-id>/session-log.md" \
"openspec/changes/<change-id>/session-log.md"
Phase Names
| 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) |
Integration
This skill is called by workflow skills at phase boundaries:
- Skills that commit: plan-feature, implement-feature, cleanup-feature, iterate-on-plan, iterate-on-implementation — include session-log.md in existing commit
- Skills that need a dedicated commit: validate-feature — commit session-log.md separately
- Merge log: merge-pull-requests writes to
docs/merge-logs/YYYY-MM-DD.md
Tests
skills/.venv/bin/python -m pytest skills/session-log/scripts/ -v