#!/usr/bin/env python3 """OpenSpec Journal — append-only interaction log for spec-driven changes. Run with no arguments for the full usage contract. """ from __future__ import annotations import argparse import json import sys from datetime import datetime, timezone from pathlib import Path VERSION = "0.2.0" MAX_SUMMARY = 200 EVENT_PHASE = { "change.created": "proposal", "artifact.added": "authoring", "artifact.revised": "authoring", "mode.chosen": "apply", "task.start": "apply", "task.complete": "apply", "task.blocked": "apply", "verifier.result": "apply", "decision": "any", "handoff": "any", "archive": "archive", "skill.invoked": "any", "agent.spawned": "any", "context.compacted": "any", "turn.start": "any", "turn.end": "any", } REQUIRED_FIELDS = { "change.created": [], "artifact.added": ["ref"], "artifact.revised": ["ref", "input", "output"], "mode.chosen": ["mode", "input"], "task.start": ["ref"], "task.complete": ["ref", "mode", "input", "output"], "task.blocked": ["ref", "input", "output"], "verifier.result": ["ref", "note", "input", "output"], "decision": ["input", "output"], "handoff": ["input", "output"], "archive": [], "skill.invoked": ["name"], "agent.spawned": ["count", "kind"], "context.compacted": [], "turn.start": ["input"], "turn.end": ["output"], } NOTE_VALUES = {"verifier.result": {"pass", "concerns", "fail"}} MODE_VALUES = {"mode.chosen": {"direct", "team"}} EVENT_DESCRIPTION = { "change.created": "OpenSpec change directory just came into being", "artifact.added": "Wrote a new artifact file (proposal/design/specs/tasks)", "artifact.revised": "Edited an existing artifact after it was first written", "mode.chosen": "Apply phase started; declares direct vs team execution", "task.start": "Began work on a specific task id from tasks.md", "task.complete": "Finished a task; checkbox flipped to [x]", "task.blocked": "Paused a task on an external dependency or unknown", "verifier.result": "Independent verifier returned a verdict for a task", "decision": "Load-bearing structural call inside a turn", "handoff": "Last line before stopping; first line on resume", "archive": "Change moved into openspec/changes/archive/", "skill.invoked": "User-invoked skill ran against this change", "agent.spawned": "Fan-out subagent group launched (e.g. team:debate)", "context.compacted": "PreCompact hook fired (autoemit, not by skills)", "turn.start": "Bookend opened; logged BEFORE work in response to a prompt", "turn.end": "Bookend closed; logged AFTER finishing the prompt", } FIELD_DESCRIPTION = { "ref": "Task id, file path, or artifact id this event refers to", "input": "What initiated this event (≤200 chars; the user/actor's ask)", "output": "What actually changed or was decided (≤200 chars)", "mode": "Execution mode for the apply run: direct | team", "note": "Verifier verdict: pass | concerns | fail", "name": "Skill identifier (e.g. showboat, retro, priya)", "count": "Integer count of subagents spawned in this fan-out", "kind": "Label for the fan-out group (e.g. lens-scan, debate, spike)", } USAGE = """\ OpenSpec Journal — interaction log for spec-driven changes WHAT Append-only JSONL at openspec/changes//journal.jsonl. Captures interaction points (decisions, task transitions, verifier results), not full session transcripts. Committed alongside code; travels with the change into openspec/changes/archive/-/journal.jsonl at archive time. ORDERING Events describe things that have happened, not things about to happen. Log AFTER the action completes. In particular, `change.created` is logged AFTER `openspec new change ` succeeds — the change directory must exist before the helper can append. USAGE openspec-journal.py [k=v ...] openspec-journal.py show [--limit N] openspec-journal.py doctor openspec-journal.py --schema openspec-journal.py --version EVENTS (fixed vocabulary) change.created artifact.added artifact.revised mode.chosen task.start task.complete task.blocked verifier.result decision handoff archive skill.invoked agent.spawned context.compacted turn.start turn.end TURN BOOKENDING (when CWD is inside an active change) Log `turn.start input=""` BEFORE doing work. Log `turn.end output=""` AFTER finishing. Two writes per turn. Captures intent and result on every prompt, not only when files change. Skip only if no active change exists. KEY=VALUE FIELDS Common to most events: ref= Task number, file path, or artifact id input= What the user/actor initiated, ≤200 chars (REJECTED if longer) output= What actually changed/resulted, ≤200 chars (REJECTED if longer) actor= Optional free-form: implementer | verifier | lead | user phase=

Optional override; default derived from event Event-specific structured fields (machine-readable, no length limit): mode= direct | team (mode.chosen, task.complete) note= pass | concerns | fail (verifier.result) name= showboat | retro | priya | … (skill.invoked) count= Number of subagents spawned (agent.spawned) kind=