Skip to content

Design Doc 2026 07 28_v0.17.1 release

Rick Hightower edited this page Jul 28, 2026 · 1 revision

Snapshot of release v0.17.1 (2026-07-28) — frozen record. The current version is Design-Doc.

Worklog — Software Design Document

1. Document Overview

Purpose. Describe the design of worklog, a local-first, git-native work-tracking layer for agentic coding, as actually implemented in this repository at v0.17.1.

Audience. Junior developers who need implementation-level guidance; project managers who need scope, dependencies, risks, and behavior.

Scope. The bin/ CLI and its Python modules, the append-only event log under .work/, the git hooks, the typed adapter contract and shipped adapters, the Claude Code plugin packaging, GitHub Actions CI, the generated/frozen document artifacts under docs/, and the Information Architecture (IA) reader plane under docs/.index/ (inventory, sidecars, rendered pages, traceability graph).

Out of scope. The prose content of individual skills (plugin/skills/*, .claude/skills/*) beyond their contracts with the deterministic core; the harness (Claude Code) itself; the separate UI repo (wiki_ticket_sdd_ui) cancelled from this work log in the v0.13.0 cycle.

Related documents. docs/worklog-spec.md (v1.9, the normative spec), docs/adr/0001..0003 (Architecture Decision Records), docs/plans/ (seventeen dated plan documents — the why record, most recently docs/plans/2026-07-25-wiki-driven-integration-guides.md), docs/migrations/0001-type-split.md and 0002-ia-content-model.md, adapters/README.md (adapter authoring rules), docs/user_guide/ (task-oriented guides), docs/integrations/ (eleven per-system setup guides plus an index, new in v0.16.0), and the companion docs/designs/current_code_walkthrough.md.

Definitions.

Term Meaning
ULID Universally Unique Lexicographically Sortable Identifier: 48-bit ms timestamp + 80-bit entropy, Crockford base32, 26 chars. Lexicographic sort == time sort.
Fold Deriving item state by replaying the event log in ev order (bin/fold.py).
Compaction The only file rewrite: replaces folded history with snapshot events plus a compact watermark (bin/compact.py).
Canonical hash sha256(canonical_json(HASH_FIELDS))[:16] (bin/canonical.py) — the sync change-detector.
Marker The worklog:<ULID> token embedded in every pushed ticket body; the idempotency key.
Adapter A single executable translating canonical JSON to one tracker's CLI; dumb by contract.
Skill Prose instructions the harness model executes; the non-deterministic edge of the system.
LWW Last-writer-wins, per field, ordered by ev.
wiki_key Stable logical page identity (e.g. plan/ia-content-model, design/current-design-doc); the key published.json already used, formalized in the IA model.
truth_state current / snapshot / superseded / archived — orthogonal to lifecycle status; tells readers whether a page is live truth or historical evidence.
Sidecar docs/.index/<wiki_key>.yml — metadata for frozen docs that must never be edited in place.
Reader plane Generated navigation under docs/.index/ (inventory, rendered Home/Sidebar/indexes, publish manifest, traceability graph).

Assumptions are labeled Assumption; unverified points are Open Question; everything else in this document is Confirmed against the repository at the commit in the frontmatter.

2. Executive Summary

Worklog makes work-in-progress visible without a server. Work items live as an append-only JSONL event log inside the repository (.work/todo.jsonl, .work/done.jsonl); every derived artifact — the roadmap, status reports, Mermaid visualizations, ticket pushes, wiki pages, and (since v0.13.0, extended in v0.14.0) the IA reader plane — is computed from that log and the committed docs tree. Three properties drive the design (spec §1): visible WIP, plans produce tickets, and a generic core with pluggable edges.

Main workflows:

  • Track work: worklog add/update/close/reopen appends events; worklog list/show/fold reads derived state.
  • Capture plans: exiting plan mode triggers a hook that forces worklog plan-capture, which writes a frozen plan document and creates an epic plus its tasks in one pass, then ia-index refreshes the reader plane.
  • Generate the roadmap: worklog roadmap-render is a pure function of the log; a pre-commit hook regenerates and diffs it, so a stale or hand-edited roadmap cannot be committed.
  • Sync tickets: worklog sync drives a typed dispatcher (bin/sync_dispatch.py) that pushes/pulls through a per-tracker adapter executable, with idempotency, echo suppression, and conflict recording enforced in the dispatcher, never in adapters. Rich ticket bodies come from worklog ticket-body (traceability projection).
  • Report status: worklog status --emit-facts produces deterministic facts; a skill writes the prose; worklog status --write freezes the report.
  • IA / content model (v0.13.0, extended v0.14.0): worklog ia-normalize backfills wiki_key + truth_state (sidecars for frozen docs, in-place for live docs); ia-inventory / ia-render / ia-graph materialize the reader plane and traceability graph; trace-check reports unlinked evidence; gates are warn-level until Phase 5. v0.14.0 extends the reader plane past documents to artifact pages — one generated page per work item, PR, and release (bin/ia_render.py — render_item_page()/render_release_page()/ render_pr_page()), reusing the same graph edges rather than storing new fields; worklog ia-ticket <ULID> previews a ticket page without a full render pass. The published-page manifest grew from 51 entries to 258.
  • Integration guides (v0.16.0): a new prose-only edge, integration-guide (plugin/skills/integration-guide/SKILL.md), points an agent at a wiki-hosted setup guide for one of eleven named systems — the four SDD tools this repo composes with (Superpowers, GSD, SpecKit, OpenSpec) and seven ticket/wiki systems only one of which (GitHub) has a real adapter (Jira, Confluence, GitHub, GitLab, Azure DevOps, AWS CodeCatalyst, Google Cloud DevOps). It ships zero new Python: WebFetch reads the live Integration-<Name> wiki page (with a redirect-to-Home detector, since a GitHub wiki 200s a missing slug instead of 404ing), and on fetch failure or a failed verification falls back to a bundled local copy, docs/integrations/fallback-<key>.md (eleven files + docs/integrations/README.md index, registered through the existing worklog wiki-add + wiki-publish pipeline — no new publish path).

Major components: the worklog CLI (the API, 1206 lines), fold.py (state derivation), compact.py (the only rewriter, CI-only), render_roadmap.py + viz_mermaid.py (generated docs), sync_dispatch.py + adapters/ (ticket sync), ia.py / ia_render.py (670 lines) / ia_graph.py (302 lines) (reader plane + graph + artifact pages), adr.py/plan_capture.py/canonical.py/ulid.py (support modules), git hooks, GitHub Actions, the Claude Code plugin, and (v0.16.0) the integration-guide skill + docs/integrations/ content — the first edge in this repo built entirely from prose and existing primitives, no new bin/ code.

External dependencies: git (union merge), the gh CLI (GitHub adapter and merge-when-green), Python 3 stdlib only — zero third-party runtime dependencies (Confirmed: no requirements file; tests/ use stdlib unittest).

Primary risks: hosted platforms don't run merge drivers server-side (PR-level conflicts on the log), wall-clock LWW ties, compaction as the single state-rewriting operation (mitigated by fold-equality verification), and IA gates still warn-only (Phase 5 hard-fail promotion still open).

3. Requirements Summary

Functional requirements (Confirmed, from spec §2 and the implementation):

# Requirement Component
R1 Append-only, mergeable event log of work items bin/worklog — append(), .gitattributes union merge
R2 Deterministic state derivation from the log bin/fold.py — fold()
R3 Generated, CI-guarded roadmap bin/render_roadmap.py, hooks/pre-commit
R4 Plan capture: plan doc + epic + tasks in one operation bin/worklog — cmd_plan_capture(), bin/plan_capture.py
R5 Idempotent bidirectional ticket sync bin/sync_dispatch.py, bin/canonical.py, adapters/*
R6 Deterministic ingestion of remote changes bin/ulid.py — deterministic(), bin/worklog — cmd_ingest()
R7 Conflict recording, never silent overwrite cmd_conflict(), fold._apply_mutations() conflict clearing
R8 Log compaction preserving fold equality bin/compact.py — compact(), .github/workflows/compact.yml
R9 Frozen status reports with deterministic facts bin/worklog — cmd_status(), _status_facts()
R10 Work taxonomy (level/kind/milestone) with write-time validation check_taxonomy(), fold._normalize_taxonomy(), hooks/pre-commit
R11 Wiki publish ledger with stable page identity _register_published(), .work/published.json
R12 ADRs with once-written bodies and supersede chains bin/adr.py, `worklog adr new
R13 Green-gates merging, never bypassed plugin/scripts/merge-when-green.sh, ADR-0003
R14 IA content model: wiki_key, truth_state, inventory, reader plane, traceability bin/ia.py, ia_render.py, ia_graph.py; plan docs/plans/2026-07-22-ia-content-model.md; migration 0002
R15 Branch discipline: commits never land directly on main/master; every commit message references a worklog item or ticket hooks/pre-commit branch-guard block, hooks/commit-msg; plan docs/plans/2026-07-25-branch-discipline-hooks.md

Non-functional: no runtime dependencies beyond Python 3 + git; appends atomic under PIPE_BUF (body capped at MAX_BODY = 2048, bin/worklog line 31); corrupt input never fatal (fold.read_lines()); CI coverage floor ≥80% on bin/*.py (.github/workflows/worklog.yml, --fail-under=80), target 95%; local-only degradation when edge tooling is missing (spec invariant 15.10); IA index artifacts are byte-deterministic (no wall clock) so freshness gates can regenerate-and-diff.

Availability/scalability: single-repo, single-team scale by design; compaction bounds log growth (spec §7 trigger: nightly or >5000 lines). Disaster recovery is git itself: the log is committed history.

4. System Context

Actors: developers and AI agents (writers via the CLI), the compactor (CI), the sync actor, remote trackers, the wiki, and readers (PMs reading generated docs and the IA Home/indexes). Trust boundary: everything inside the repo is trusted; adapters and gh cross the network boundary; skills run in the harness model.

flowchart TD
    subgraph Humans_and_Agents["Developers + AI agents"]
        DEV["Developer / Claude Code session"]
    end
    subgraph Repo["Git repository (trust boundary)"]
        CLI["bin/worklog CLI"]
        LOG[(".work/todo.jsonl + done.jsonl<br/>append-only event log")]
        DOCS["docs/ generated + frozen artifacts"]
        IDX["docs/.index/ reader plane<br/>inventory · sidecars · graph · rendered"]
        HOOKS["git hooks + Claude Code hooks"]
    end
    subgraph CI["GitHub Actions"]
        INV["worklog-invariants + coverage"]
        COMPACT["nightly compact (main only)"]
    end
    subgraph External["External systems"]
        TRACKER["Ticket tracker (GitHub Issues via gh)"]
        WIKI["GitHub Wiki"]
    end
    DEV -->|subcommands| CLI
    CLI -->|"append() only writer"| LOG
    CLI -->|render / capture / status / ia-*| DOCS
    CLI -->|ia-normalize / ia-render / ia-graph| IDX
    HOOKS -->|gate commits & sessions| DEV
    LOG --> INV
    COMPACT -->|"rewrite (verified)"| LOG
    CLI -->|"sync via adapter subprocess"| TRACKER
    DOCS -->|wiki-publish skill| WIKI
    IDX -->|publish-manifest + banners| WIKI
Loading

How to read it: every state change funnels through the CLI into the log; CI and hooks are gates, not writers (except the nightly compactor). The tracker and wiki are mirrors — the log is the source of truth (spec §2 non-goals). The IA plane is generated from docs + fold; it never writes the event log.

Inputs: CLI invocations, adapter pull output (NDJSON), plan drafts, status prose on stdin. Outputs: log events, docs/roadmap.md, docs/plans/*, docs/status/*, docs/adr/*, docs/.index/*, ticket pushes, sync report lines.

5. High-Level Architecture

Layers, all Confirmed from imports:

  • Identity: ulid.py (no imports from siblings).
  • State: fold.py (imports hashlib, json only).
  • CLI/API: bin/worklog imports ulid, fold; lazily imports render_roadmap, plan_capture, compact, sync_dispatch, adr, ia, ia_render, ia_graph.
  • Rendering: render_roadmap.py imports ulid, fold; viz_mermaid.py imports ulid, fold and is imported lazily by render_roadmap.render().
  • Sync: sync_dispatch.py imports canonical; drives adapters and the CLI as subprocesses.
  • IA plane: ia.py imports fold; ia_render.py / ia_graph.py import ia (and fold-derived items where needed).
  • Automation: hooks/ (git + Claude Code), .github/workflows/, plugin/ (packaged copies of the same scripts).
flowchart LR
    subgraph bin["bin/ (stdlib-only Python)"]
        WL["worklog<br/>CLI entry, only log writer"]
        FOLD["fold.py<br/>state = fold(events)"]
        ULID["ulid.py<br/>random + deterministic IDs"]
        CANON["canonical.py<br/>canonical hash"]
        RR["render_roadmap.py"]
        VIZ["viz_mermaid.py"]
        PC["plan_capture.py"]
        CP["compact.py"]
        SD["sync_dispatch.py"]
        ADR["adr.py"]
        IA["ia.py<br/>wiki_key · inventory · normalize"]
        IAR["ia_render.py<br/>Home · Sidebar · manifest"]
        IAG["ia_graph.py<br/>graph · link-pr · trace"]
    end
    subgraph edges["Edges"]
        AD["adapters/github, adapters/fake"]
        SK["skills (prose, harness-executed)"]
    end
    WL --> FOLD
    WL --> ULID
    WL --> RR
    WL --> PC
    WL --> CP
    WL --> SD
    WL --> ADR
    WL --> IA
    WL --> IAR
    WL --> IAG
    RR --> FOLD
    RR --> VIZ
    CP --> FOLD
    SD --> CANON
    SD -->|subprocess| AD
    SD -->|"subprocess: worklog link/ingest/conflict"| WL
    IA --> FOLD
    IAR --> IA
    IAG --> IA
    SK -->|invoke| WL
Loading

How to read it: arrows are imports or subprocess calls. Note the loop SD → WL: the dispatcher never appends to the log directly; it shells back into worklog (invariant 15.4, sync_dispatch.py — Dispatcher.worklog(), lines 178–188).

Data flow of the event log + IA plane:

flowchart TD
    A["worklog add/update/close/link/ingest/conflict"] -->|"single O_APPEND write + newline"| T[(".work/todo.jsonl")]
    B["other branches / other clones"] -->|"git union merge<br/>(.gitattributes)"| T
    T --> F["fold(): parse-tolerant, dedupe by ev,<br/>sort by ev, watermark, apply LWW"]
    D[(".work/done.jsonl")] --> F
    F --> ST["derived item state"]
    ST --> RM["docs/roadmap.md (regenerated, diffed by hook)"]
    ST --> FACTS["status facts JSON"]
    ST --> SYNC["sync push scope + canonical hashes"]
    ST --> GRAPH["docs/.index/_graph.json"]
    DOCS["docs/** + published.json"] --> INV["docs/.index/_inventory.json"]
    INV --> REND["docs/.index/rendered/* + publish-manifest"]
    N["nightly CI compactor"] -->|"snapshots + watermark,<br/>only after fold(new)==fold(old)"| T
    N -->|append closed snapshots| D
Loading

Assumptions/failure behavior: union merge duplicates and scrambles lines — the fold's dedupe/sort absorbs that; a corrupt line costs itself only (fold.read_lines()). IA artifacts are pure functions of committed files; a stale index is a gate warning (soon hard fail), not silent drift.

Deployment architecture is §27; trust boundaries §22.

6. Architectural Decisions

The repo keeps ADRs in docs/adr/; this section summarizes and links (per ADR tooling, bin/adr.py).

ADR-0001 — Append-only event log + fold + union merge (accepted). State is never stored; it is a fold over immutable events, merged by git's built-in union driver. Alternatives rejected: state files (merge conflicts clobber teammates), CRDTs (correct but heavy). Consequences absorbed: arbitrary post-merge line order and duplicates (fold sorts/dedupes), wall-clock LWW ties, and the hosted-platform caveat (GitHub does not run merge drivers server-side — spec §8.1).

ADR-0002 — Skill-based edges hardened by a typed adapter contract (accepted). The 1.2 spec's per-system adapter binaries never shipped; v1.4 moved integration to skills (the model already knows these systems); v1.6 restored the three invariants that had regressed to prose — idempotency, pull parsing, capability degradation — as code in the dispatcher, with adapters kept as generated dumb translators. tests/test_adapter_contract.py — test_adapters_contain_no_invariant_logic() bans sha256, last_pushed_hash, canonical_json, search, and sync-state from every adapter source.

ADR-0003 — Green-gates merging (accepted). PRs merge only when every check is green; merge-when-green.sh polls (default 300 s, 24 attempts) and treats "no gates reporting" as not passing (exit 4 timeout). Never --admin.

IA content model (plan + migration 0002, not yet a numbered ADR). Two planes: storage stays path-organized by how docs are produced; navigation is a generated reader plane keyed by wiki_key. Frozen docs never receive in-place metadata edits — sidecars hold additive fields (invariants 15.8/15.9). Doc schema and entity schema are deliberately split (schema/doc.schema.json vs schema/entity.schema.json, mirrored in bin/ia.py constants) so work items can enter the graph without pretending to be documents (Confirmed: schema split item 01KY5QV5G0 / #111).

Artifact pages (v0.14.0, plan docs/plans/2026-07-24-artifact-pages.md). Extends the same reader plane to tickets, PRs, and releases, which previously existed only as bare stub nodes in the graph. Two decisions confirmed with the project owner before build: (1) no new stored fields — a page's hierarchy, linked PRs, and release are derived at render time from existing graph edges (ia_graph.item_links()), not cached into the item or a sidecar, because a stored copy would be a second, driftable source of truth the sidecar rule already forbids; (2) live PR metadata is out of scope this pass — no code anywhere in the repo calls gh pr view, so "files changed" and CI/review status render as "not tracked" on PR pages, and the live sync is filed as a separate follow-up (#138) rather than silently dropped.

Decisions not (yet) in ADRs, recorded in plans/spec:

Decision Rationale Where
Deterministic ULIDs for ingested events Two clones polling the same remote change must append byte-identical lines so dedupe collapses them; a random ev silently reverts local edits bin/ulid.py — deterministic(); spec §10.2; tests/test_ulid.py — TestTheBugThisPrevents
Canonical hash over exactly ten fields Echo suppression and skip-unchanged; changing the field set churns every hash (accepted once, in the taxonomy migration) bin/canonical.py — HASH_FIELDS
Config/policy split, AGENTS.md → CLAUDE.md symlink One policy for every harness; scripts read only .work/config.yml spec §4.1
No YAML library Stdlib-only; naive block scans where needed bin/worklog — _config_system(); ia.parse_front_matter()
Embedded schemas mirrored from schema/ Installed repos ship bin/ without schema/; tests assert the copies match sync_dispatch.py — CAPABILITIES_SCHEMA; ia.REQUIRED_*; tests/test_ia.py — TestSchemaSync
Body cap is a constant, not a setting Derived from PIPE_BUF; "a knob whose only valid value is the default is a trap" .work/config.yml trailing comment; bin/worklog line 31
Frozen artifacts (plans, snapshots, status, ADR bodies) Documents people acted on are history, not caches spec §13.2–13.3; existence refusals; ADR supersede-only
Hooks, not hope "A CLAUDE.md instruction holds maybe 80% of the time. A hook holds 100%." spec §12; hooks/exit-plan-capture.sh
Sidecars over frozen-doc rewrite Would otherwise violate §15.8/§15.9 ia.is_frozen(), ia.normalize()
IA gates warn-only until Phase 5 Soft rollout before hard-fail hooks/pre-commit IA block comments
Branch guard + commit-msg hard-fail immediately, no warn period (v0.15.0) A real incident (13 commits authored straight onto main, diverging from origin/main for hours) — "hooks enforce invariants, not hope"; MERGE_HEAD exempts reconciliation merges, WORKLOG_SKIP_BRANCH_GUARD exempts the three non-commit callers (doctor, CI backstop, integration-test assertions) hooks/pre-commit branch-guard block; hooks/commit-msg; plan docs/plans/2026-07-25-branch-discipline-hooks.md

Conditions to revisit: a Lamport counter if clock-skew LWW ever bites (spec §16); external as an array if multi-tracker is needed; Phase 5 hard-fail promotion and platform render adapters (open item #98).

7. Component Inventory

Component Type Responsibility Inputs Outputs Depends on Failure impact
bin/worklog CLI (1206 lines) All log writes; every subcommand argv, stdin (status prose, plan drafts) log events, docs, stdout ulid, fold, lazy others no writes possible
bin/fold.py Library (278 lines) Derive state; the only interpreter of the log log paths FoldResult stdlib everything downstream
bin/ulid.py Library Random + deterministic ULIDs time/entropy or (system,key,rev,ts) 26-char IDs stdlib ordering/idempotency
bin/canonical.py Library Canonical JSON + 16-hex hash item dict hash stdlib echo suppression
bin/render_roadmap.py Generator Byte-deterministic roadmap log markdown fold, ulid, viz_mermaid roadmap staleness gate
bin/viz_mermaid.py Generator Deps/hierarchy/gantt diagrams, 40-node cap FoldResult, log paths mermaid blocks fold, ulid cosmetic
bin/plan_capture.py Library Parse ## Tasks checkboxes; plan front matter draft text task list, front matter stdlib plan capture
bin/compact.py Batch (CI) The only file rewriter; verified log files rewritten logs fold, ulid, render_roadmap.max_ev worst-case: refused, files untouched
bin/sync_dispatch.py Orchestrator Every sync invariant fold output, adapter I/O pushes, ingests, conflicts, report canonical, adapter, worklog sync only; local-only fallback
bin/adr.py Library ADR parse/validate/scaffold/supersede docs/adr/*.md problems list, scaffolds stdlib ADR gate
bin/ia.py Library (616 lines) wiki_key, truth_state, inventory, normalize, sidecars docs tree, ledger, fold _inventory.json, sidecars, frontmatter patches fold IA gates / publish metadata
bin/ia_render.py Generator (670 lines) Home, Sidebar, indexes, publish-manifest, aliases, artifact pages (v0.14.0): ticket/release/PR pages inventory records, fold items, graph docs/.index/rendered/* (incl. tickets/, releases/, prs/) ia, ia_graph reader plane staleness
bin/ia_graph.py Library (302 lines) Traceability graph, link-pr overlay, ticket-body, trace-check, adjacency projection (v0.14.0): build_adjacency()/item_links() records + fold items _graph.json, sidecar edges ia unlinked evidence
adapters/github/adapter Executable GitHub translation via gh verb + JSON JSON/NDJSON, exit codes 0–5 gh CLI GitHub sync
adapters/fake/adapter Test double Contract-faithful local tracker verb + JSON JSON, state file stdlib CI sync tests
hooks/pre-commit, pre-merge-commit, commit-msg (v0.15.0) Git hooks Newline/schema/taxonomy/roadmap/ADR/IA gates, branch guard + work-reference gate (v0.15.0) staged tree, branch name, commit message pass/fail (+ IA warnings) python3 invariants unenforced locally; commits can drift onto main untraceably
hooks/*.sh (4) Claude Code hooks Session policy: capture, reminder, stop-gate, doctor hook JSON hook JSON git, python3 policy drift
plugin/ Package Skills, /worklog:* commands, hook wiring, script copies mirrors bin/, hooks/ plugin installs
.github/workflows/worklog.yml CI Invariants + tests + coverage ≥80% push/PR pass/fail python3, coverage merge gate
.github/workflows/compact.yml CI Nightly compaction, own commit schedule compact commit compact.py log growth only
plugin/skills/integration-guide/SKILL.md (v0.16.0) Skill (prose only) Resolve a named SDD tool or ticket/wiki system to its wiki page or local fallback request naming one of 11 systems wiki page fetch, or local file read + spoken caveat .work/config.yml (wiki.root_url), WebFetch wrong/stale integration guidance surfaced to the user
docs/integrations/* (v0.16.0) Content (11 fallback files + README index) Offline-safe copy of each system's setup guide hand-authored wiki-add ledger entries, published wiki pages none (static docs) fallback path shows a stale guide until re-published

Owner for all components: the repo (single-team). Scaling model: none needed — per-repo files.

8. End-to-End Workflows

8.1 Track a work item

Trigger: any request that produces work (enforced by hooks/prompt-reminder.sh and the Stop gate in hooks/stop-worklog-check.sh, which blocks ending a session where the tree changed but todo.jsonl did not). Main flow: cmd_add() validates taxonomy (check_taxonomy(): epics are feature/ops only; milestone lives on leaves), builds a create event whose set omits kind when not given — so the fold triages it — and appends. Failure flows: --unplanned without --discovered-during exits; body >2048 B exits (append()). Idempotency: not needed — each add is a new ULID.

8.2 Plan capture

sequenceDiagram
    participant M as Model (plan mode)
    participant H as exit-plan-capture.sh (PostToolUse hook)
    participant W as bin/worklog plan-capture
    participant P as plan_capture.py
    participant L as .work/todo.jsonl
    participant I as ia-index
    M->>H: ExitPlanMode
    H-->>M: non-optional capture instruction (+ ia-index)
    M->>W: --slug s --title t --file draft.md
    W->>P: parse_tasks(draft)
    P-->>W: tasks from "## Tasks" checkboxes
    W->>W: refuse if any docs/plans/YYYY-MM-DD-s.md exists (frozen, invariant 15.8)
    W->>L: create epic (level epic, kind feature)
    loop each task
        W->>L: create task/subtask (parent chain, kind feature)
    end
    W->>W: write plan doc with front matter (epic + item ids)
    M->>I: worklog ia-index
    I-->>M: normalize + inventory + render under docs/.index/
    M->>M: spawn background subagent: ticket-sync + wiki-publish
Loading

Confirmed in cmd_plan_capture() (bin/worklog, lines 339+): indented checkboxes become subtasks parented to the preceding task; captured items get explicit kind:feature. Priority token (P0..P3) optional, default P2 (plan_capture.py — TASK_RE). The ExitPlanMode hook text requires ia-index after capture (hooks/exit-plan-capture.sh).

Invariant 15.8 guard, v0.17.0 fix (Confirmed, bin/worklog — cmd_plan_capture(), lines ~344–367): the guard is slug-scoped, not filename-scoped — it must refuse recapturing a slug regardless of which date's filename it lives under. Two bugs were found and fixed in sequence this release:

  1. The original guard checked only os.path.exists(f"docs/plans/{date}-{slug}.md") with date from time.gmtime() (UTC), while a plan is authored in local time. A plan captured in the evening from a timezone behind UTC looked for tomorrow's UTC-dated filename, found nothing, and silently wrote a duplicate — observed in a downstream repo on 2026-07-26 (23 duplicate work items, caught by hand and rolled back). Fixed by globbing docs/plans/*-{slug}.md across all dates instead of checking one fixed path (PR #198).
  2. That first fix's bare *-{slug}.md glob matched by raw suffix, not field boundary: a search for slug migration also matched an existing 2026-07-01-database-migration.md (real slug database-migration), since that filename also ends in -migration.md — a false-positive refusal of a legitimately new, different slug. Caught in review before merging PR #198. Fixed by anchoring on the fixed YYYY-MM-DD- date shape with re.compile(r"^\d{4}-\d{2}-\d{2}-%s\.md$" % re.escape(slug)) against glob.glob("docs/plans/*.md"), so only an exact slug match after the date field counts. Two regression tests pin both failure modes (test_plan_capture_refuses_a_slug_already_captured_on_another_date, test_plan_capture_does_not_false_positive_on_a_suffix_match, tests/test_integration.py).

8.3 Ticket sync

sequenceDiagram
    participant S as ticket-sync skill
    participant D as sync_dispatch.Dispatcher
    participant A as adapter (subprocess)
    participant T as tracker
    participant W as bin/worklog
    S->>D: worklog sync
    D->>A: capabilities
    A-->>D: JSON (schema-validated; marker template must contain {ulid})
    Note over D: gate: ContractError aborts before any push
    D->>W: fold (subprocess)
    W-->>D: items JSON
    loop each in-scope item (open OR hash-dirty OR --keys)
        D->>D: outbound(): HASH_FIELDS + type degrade; canonical_hash
        alt hash == last_pushed_hash and not forced
            D->>D: skip (idempotent)
        else create/update
            D->>A: push {op, key, marker, item} (retry x3 on exit 4)
            A->>T: gh issue create/edit
            A-->>D: {key, url, rev}
            D->>W: link <ulid> --system --key --url --rev (create only)
            D->>D: last_pushed_hash = hash
        else closed with key
            opt hash-dirty (v0.12.1)
                D->>A: push {op update, final item shape} before close
            end
            D->>A: close <key> <resolution>
        end
    end
    D->>A: pull --since <cursor>
    A-->>D: NDJSON lines
    loop each line
        alt canonical_hash(line) == last_pushed_hash
            D->>D: drop (echo of our own push)
        else only remote moved
            D->>W: ingest <ulid> --system --key --rev --rev-ts-ms --set f=v
            Note over W: deterministic ev = ULID(rev_ts, sha256(system|key|rev))
        else both sides moved
            D->>W: conflict <ulid> --field f --local --remote --remote-rev
        end
    end
    D-->>S: sync report: created=..updated=..conflicts=.. + drift list
Loading

Failure flows (Confirmed, handle_exit()): exit 2 aborts the whole sync ("re-authenticate"); exit 3 clears last_pushed_hash for re-push next run; exit 4 retries with doubling backoff then defers; exit 5 fetches the remote and records per-field conflicts; anything else is drift, continue. No adapter configured → LOCAL_ONLY message, exit 0: a mode, not an error. Orphan/titleless items are never pushed.

Close path (v0.12.1): a closing item whose canonical hash is dirty pushes an update with the final item shape before the close verb (push_items()). Regression test: tests/test_dispatch.py — TestCloseSyncsFields.

Rich ticket bodies (v0.13.0): worklog ticket-body <ulid> projects summary + epic/plan/milestone + graph edges (ia_graph.ticket_body()), used by the issue-description skill before push.

8.4 Compaction (nightly, main only)

Trigger: cron 17 7 * * * (.github/workflows/compact.yml). Flow: refuse if logs have uncommitted changes (_git_refuses()); watermark = max raw ev; skip if todo is already all snapshots; partition open/closed (orphans stay open — "never drop data"); write temp files; verify fold(new) == fold(old) plus trailing newline plus every line parses (_verify()); only then os.replace. CI commits it as its own commit chore(worklog): compact through <ulid>.

v0.13.0 fix: snapshot writes folded state verbatim so a closed orphan no longer aborts verify (item 01KY5HW7KS / #101) — compact._public() keeps the folded shape rather than inventing a cleaner one that diverges from fold.

8.5 Status reports

cmd_status(): --emit-facts prints deterministic JSON facts (windowed by ULID timestamps); the status-report skill writes prose; --write wraps it in front matter carrying the window and through watermark and refuses to overwrite an existing report without --force (frozen, invariant 15.9). Timecards bucket per UTC day and include best-effort git commit subjects.

8.6 Green-gates merge

merge-when-green.sh: poll gh pr checks buckets; fail/cancel → exit 1 (never merge); empty or pending → sleep and retry up to 24×300 s; all green → merge (or advisory print when features.auto_merge_on_green: false or --advisory); timeout → exit 4. "No gates reporting is not gates passing" (ADR-0003).

8.7 IA pipeline (v0.13.0, extended v0.14.0)

sequenceDiagram
    participant Dev as Developer / release / plan-capture
    participant N as ia.normalize
    participant Inv as ia.write_inventory
    participant R as ia_render.write_all
    participant G as ia_graph.write_graph
    participant Hook as pre-commit (warn-only)
    Dev->>N: worklog ia-normalize
    N->>N: classify docs, seed wiki_key from ledger, write sidecars / live FM
    Dev->>Inv: worklog ia-inventory
    Inv-->>Dev: docs/.index/_inventory.json
    Dev->>R: worklog ia-render
    R-->>Dev: rendered/home, Sidebar, indexes, publish-manifest, aliases
    Dev->>G: worklog ia-graph
    G-->>Dev: docs/.index/_graph.json
    Note over Dev: ia-index = normalize + inventory + render
    Hook->>N: ia-normalize --check
    Hook->>Inv: ia-inventory --check
    Hook->>R: ia-render --check
    Hook->>G: trace-check (warn forever)
    Hook-->>Dev: WARNING (soon hard fail) if drift
Loading
  • Normalize (ia.normalize()): frozen docs get additive sidecars; sanctioned-live docs (roadmap, current_* designs, guides, ADR status) get in-place identity fields only. truth_state is recomputed, never pinned from a stale sidecar (DYNAMIC_FIELDS).
  • Inventory: one record per doc with wiki_key, doc_type, truth_state, relationships.
  • Render: question-driven Home, Sidebar, decisions/releases/status indexes, truth banners (ia_render.banner()), deterministic publish-manifest.json and aliases.json.
  • Graph: typed edges (produces, decides, implements, supersedes, verified-by, lands-in, …); link-pr overlays PR/commit edges on item sidecars (event log still owns item state); trace-check lists closed items missing plan/ticket/PR links (--strict at release).
  • Seed (ia-graph --seed): propose-only edges into gitignored suggestions.jsonl — never auto-mutates docs.
  • Artifact pages (v0.14.0): render_all() additionally loops every fold item and every release/pr graph node, calling ia_graph.build_adjacency() once per pass and item_links() per entity — one shared traversal instead of each renderer re-walking the edge list. Ticket pages (render_item_page()) show own description/status, upward hierarchy to the epic, downward children with a done/total progress rollup, linked PRs, and linked release; release pages (render_release_page()) derive a Change Log from milestone-tagged closed items plus their PR edges — not a CHANGELOG.md parser, so the hand-authored changelog stays untouched; PR pages (render_pr_page()) show linked tickets and related release, with changed-files/CI/review status rendered literally as "not tracked" (no gh pr view call exists in this codebase yet — #138). build_manifest() grew a second loop keyed off the tickets/, releases/, prs/ filename prefixes so a future entity type needs only a new prefix, not a new loop.

9. Complex Business Logic

9.1 The fold

Plain language: read every line of both logs, drop what doesn't parse, keep one copy of each event, replay them oldest-first, and let the last write to each field win. Formal rules (Confirmed, bin/fold.py — fold()):

  1. Order is by ev, never file position, never ts; ties break on (actor, sha256(line)) so every machine folds identically (dedupe_and_sort()).
  2. Events at or below the compact watermark are dropped, except snapshot (apply_watermark()).
  3. snapshot replaces state entirely; a duplicate create degrades to an update.
  4. Mutation order within an event: del, then add, then set (_apply_mutations()).
  5. close takes status from set; only a missing/open status defaults to done — a cancelled item must never report as shipped.
  6. reopen clears closed status and resolution.
  7. conflict appends to item._conflicts and changes no state; any later event writing that field clears the conflict.
  8. Events for unknown items become _orphan partial items — reported, never invented, never fatal.
  9. Taxonomy is normalized leniently on create/snapshot: legacy type maps via LEGACY_TYPE_MAP; a create with neither type nor kind folds to kind:triage. Hard validation lives at write time and in the hooks — the fold never crashes on a bad pair.

9.2 Item lifecycle

stateDiagram-v2
    [*] --> todo : create (status todo)
    todo --> in_progress : update
    in_progress --> blocked : update
    blocked --> in_progress : update
    todo --> blocked : update
    in_progress --> done : close (set.status done or default)
    todo --> done : close
    in_progress --> cancelled : close (set.status cancelled)
    todo --> cancelled : close
    done --> todo : reopen (clears resolution)
    cancelled --> todo : reopen
    done --> [*] : compaction moves snapshot to done.jsonl
    cancelled --> [*]
Loading

Note: transitions are not machine-restricted — any update may set any of the three open statuses; the diagram shows the intended flow. Closed→open goes only through worklog reopen (v0.12.0): update --status on a closed item is refused with a pointer to reopen, because only the reopen op also drops the stale resolution. Compaction relocates closed items physically; close itself never moves files (spec §7).

Item-id resolution (v0.17.1). Every transition above is issued against an item named on the command line, and naming is now a single code path: _resolve() (bin/worklog — _resolve(), lines 115–130) folds todo.jsonl + done.jsonl, prefix-matches the argument against the folded item keys, and exits on no match or on more than one. cmd_update() (133), cmd_close() (156), cmd_reopen() (165), cmd_link() (176), cmd_resolve() (242), and cmd_show() (298) all call it and write their event under item["id"], never the caller's string. Before v0.17.1 only reopen/resolve/show resolved at all; close, update, and link wrote under the raw argument, so an id prefix — the form show and list themselves print — appended events under a short key that folded into a phantom item, leaving the named item untouched (worklog 01KYA99TVC, GitHub #123). The lifecycle consequence is specific: cmd_update()'s pre-write lookup was fold(...).items.get(a.item, {}), and {} made check_taxonomy(cur.get("level"), …) see level=None and the cur.get("status") in CLOSED_STATUSES refusal never fire — so a prefixed update --status todo performed exactly the closed→open transition this section says only reopen may perform. _resolve() also converts the arbitrary match[0] pick on an ambiguous prefix into a refusal that names every candidate. Regression suite: tests/test_resolve.py.

9.3 Conflict lifecycle decision table

Local changed since last push Remote changed Dispatcher action
no no skip (hash equal)
no yes worklog ingest (deterministic ev)
yes no push (hash dirty)
yes yes worklog conflict per changed field; never overwrite

Resolution: worklog resolve <item> --field F --take local|remote appends a plain update that outsorts the conflict (cmd_resolve()).

9.4 Roadmap sectioning

section() (render_roadmap.py): Now = P0 or in_progress; Next = P1, or unblocked P2; Later = the rest. Epic milestone is derived from children — unanimous value or the literal string mixed (derived_milestone()) — and shown for Now/Next only.

9.5 truth_state assignment (IA)

ia._assign_truth() / plan lifecycle: live generated docs and current designs are current; dated freezes and status reports are snapshot; a plan whose items are all closed is still snapshot (historical evidence of what was planned); a plan superseded by a later plan (frontmatter or title convention) becomes superseded with reverse superseded_by linking. truth_state is always recomputed on normalize — sidecars may not pin it (DYNAMIC_FIELDS in ia.py).

10. Domain Model

Two record shapes for work: the event (what is stored) and the item (what the fold derives). There are no classes for them — plain dicts, deliberately (shell-debuggable format, ADR-0001).

IA adds document records (merged frontmatter ∪ sidecar) and entity records (items projected for the graph). Doc types: plan, roadmap, roadmap-snapshot, status, design, adr, guide. Entity types today: item only (ENTITY_TYPES in ia.py; release/code-change join when validation needs them).

erDiagram
    EVENT {
        string ev PK "ULID; sort + dedupe key"
        string ts "RFC3339 UTC, debug only"
        string actor "rick | claude | sync | compactor | tracker-name"
        string item FK "target item ULID (absent on compact)"
        string op "create|update|close|reopen|link|conflict|snapshot|compact"
        object set "scalar assignments"
        object add "set-valued additions"
        object del "set-valued removals"
        object src "provenance for ingested events"
        string through "compact watermark only"
    }
    ITEM {
        string id PK "ULID"
        string level "epic|story|task|subtask"
        string kind "feature|bug|ops|triage"
        string milestone "release axis, leaves only"
        string title
        string status "todo|in_progress|blocked|done|cancelled"
        string priority "P0..P3"
        string parent FK
        string_array labels "set-valued"
        string_array depends_on "set-valued"
        bool unplanned
        string discovered_during FK
        string plan "docs/plans path"
        string resolution
    }
    DOC_RECORD {
        string wiki_key PK
        string doc_type
        string truth_state
        string source_path
        object relates_to "typed edges"
    }
    EXTERNAL {
        string system
        string key "tracker key, never the primary key"
        string url
        string rev
        string synced_at
    }
    CONFLICT {
        string field
        string local
        string remote
        string remote_rev
    }
    EVENT }o--|| ITEM : "folds into"
    ITEM ||--o| EXTERNAL : "external (via link op)"
    ITEM ||--o{ CONFLICT : "_conflicts (via conflict op)"
    ITEM |o--o{ ITEM : "parent / depends_on"
    DOC_RECORD }o--o{ ITEM : "produces / implements"
    DOC_RECORD }o--o{ DOC_RECORD : "supersedes / snapshot-of"
Loading

Invariants: the ULID is the primary key, never external.key (spec §5.4); an epic is just an item with level: epic — there is no epic table; unplanned: true requires discovered_during (write-time); private _-prefixed fields never survive into snapshots (compact._public()); wiki_key is unique across the inventory; doc_type and entity_type enums are disjoint (tests/test_ia.py — test_doc_and_entity_types_are_disjoint).

11. Module-by-Module Design

Module Public surface Error handling Testing
bin/worklog argparse subcommands (§14) sys.exit(str) with actionable messages; validation before any write taxonomy, ingest, link, status, snapshot, plugin, IA CLI wrappers
fold.py fold(paths), FoldResult, read_lines, dedupe_and_sort, OPEN/CLOSED_STATUSES, LEGACY_TYPE_MAP never raises on bad data; errors collected in result.errors test_fold
ulid.py new(), deterministic(), encode(), timestamp_ms() ValueError on bad entropy/timestamp test_ulid
canonical.py HASH_FIELDS, canonical_json(), canonical_hash() none needed (pure) via test_dispatch
render_roadmap.py render(), max_ev(), root_epic_id() lenient raw scans test_render_roadmap
viz_mermaid.py render_viz(), deps_graph(), hierarchy(), gantt(), item_dates() skips unparseable, strips mermaid-breaking chars test_viz
plan_capture.py parse_tasks(), front_matter() pure; no I/O test_plan_capture
compact.py compact() SystemExit(1) on refusal/verify-failure; temp files deleted test_compact
sync_dispatch.py Dispatcher, validate(), ContractError, main() exit-code taxonomy; drift notes over exceptions test_dispatch, test_adapter_contract
adr.py check_all(), scaffold(), mark_superseded(), parse_front_matter() ValueError per file; listing stays best-effort test_adr
ia.py classify, resolve_key, normalize, build_inventory, validate_record, parse_front_matter check mode exits 1 with pending list; never mutates frozen bodies test_ia
ia_render.py render_all, write_all, banner, build_manifest, build_aliases check mode reports stale paths test_ia (TestRender)
ia_graph.py build_graph, write_graph, link_pr, trace_check, ticket_body, seed_edges strict trace exits 1; seed is propose-only test_ia (TestGraph)

Coupling notes (Confirmed): no circular imports; viz_mermaid is lazily imported so --viz none costs nothing; adr.validate is a deliberate copy of sync_dispatch.validate ("no import coupling"); IA constants deliberately mirror schema/doc.schema.json + schema/entity.schema.json (tests/test_ia.py — TestSchemaSync). plugin/scripts/ mirrors bin/ and hooks/; tests/test_plugin.py guards the mirror and the version lockstep (bin/worklog line 32: VERSION = "0.16.0").

13. Class-by-Class Design

Only three classes exist (the design favors functions over objects):

fold.FoldResult. State container: items (id → dict), watermark, errors, orphans, skipped, deduped. Methods open_items(), closed_items(), conflicts() — pure filters. No concurrency concerns (built and consumed in-process).

sync_dispatch.Dispatcher. Constructor: (adapter, retry_base_delay=0.5, dry_run=False); loads .work/sync-state.json. Public methods: capabilities() (the gate — schema-validate plus the {ulid} substring check), sync() (capabilities → push → pull → save state → report), push_items(), pull(), report(). State managed: per-item last_pushed_hash, per-system cursors, counters, drift list. Side effects: subprocesses only; in dry_run the state file is never written. Exceptions: ContractError — caught in main() and reported as exit 1.

sync_dispatch.ContractError. "An adapter broke the typed contract; the message names the field."

IA modules stay function-oriented (no classes) for the same shell-debuggability reason as the event dicts.

14. API Design

The CLI is the API. Global flags: --actor (defaults to $USER), --version (worklog 0.17.1, from VERSION at bin/worklog line 32, held in lockstep with plugin/.claude-plugin/plugin.json by tests/test_plugin.py). All log writes go through append() — a single O_APPEND write, newline-terminated, self-healing a missing prior newline. Every subcommand whose first positional is an existing item id resolves it through _resolve() (§9.2): a full ULID or an unambiguous prefix, exiting on no match or ambiguity.

Subcommand Purpose Key arguments Writes Notable exits
add <title> create item --level (default task), --kind (omitted → folds to triage), --milestone, --priority, --parent, --plan, --labels, --unplanned --discovered-during, deprecated --type 1 create event taxonomy violations; unplanned without discovered-during
update <item> mutate --status todo|in_progress|blocked, --priority, --title, --kind, --milestone, --add-label, --del-label 1 update event "nothing to update"; --status refused on closed items → use reopen; no match / ambiguous prefix
close <item> close --status done|cancelled, --resolution 1 close event no match / ambiguous prefix
reopen <item> reopen a closed item id or unambiguous prefix 1 reopen event not closed / no match / ambiguous prefix
link <item> record external identity --system --key required; --url --rev --hash 1 link event no match / ambiguous prefix
ingest <item> ingest remote change --system --key --rev --rev-ts-ms required; --set FIELD=VALUE 1 update event, deterministic ev field/enum whitelist
conflict <item> record both-sides change --field --local --remote --remote-rev 1 conflict event
resolve <item> clear a conflict --field --take local|remote 1 update event no open conflict on field
list / show / fold read state --all; show takes id or unambiguous prefix none show: no match / ambiguous prefix
plan-capture plan doc + epic + tasks --slug --title required, --file, --priority N create events + plan doc existing slug refused, any date (frozen, v0.17.0 fix)
roadmap-render regenerate roadmap --viz deps,hierarchy (default), --no-viz docs/roadmap.md
roadmap-snapshot freeze roadmap copy --name docs/roadmap/<date>_<name>.md existing path refused
status report facts/write --kind daily|weekly|timecard required; --emit-facts / --write / --dry-run / --force docs/status/<date>-<kind>.md existing report refused without --force
sync run dispatcher --dry-run, --keys, --push-only/--pull-only, --retry-base-delay via dispatcher dispatcher exit code
adapter init|check guidance / contract check optional path .work/sync-state.json adapter_path contract violations
promote <suggestion_id> classifier suggestion → 1 create 1 create event + consumed marker already consumed / not found
compact manual compaction --yes required rewrites logs (verified) refuses without --yes
wiki-add <file> register in publish ledger --key --title required .work/published.json file not found
adr new|list|check ADR lifecycle new <title> with --status --deciders --tags --supersedes N docs/adr/NNNN-slug.md + ledger entry check exits 1 with problem list
wiki-key <path> print stable wiki_key -v for canonical + aliases none path outside content model
ia-normalize backfill wiki_key + truth_state --check sidecars / live frontmatter check exits 1 if pending
ia-inventory content inventory --check docs/.index/_inventory.json check exits 1 if stale/invalid
ia-render / ia-manifest reader plane + publish manifest --check docs/.index/rendered/*, manifest, aliases check exits 1 if stale
ia-index normalize → inventory → render all of the above
ia-graph build traceability graph --seed (propose-only) _graph.json or suggestions.jsonl
ticket-body <item> rich issue body projection stdout only unknown item
ia-ticket <item> preview a generated ticket page (v0.14.0) stdout only unknown item
link-pr <item> overlay PR/commit edge --pr / --commit item sidecar under docs/.index/item/ validation error
trace-check unlinked-evidence report --strict none strict exits 1 if gaps

Versioning/back-compat: --type survives as a deprecated alias mapping through the same LEGACY_TYPE_MAP as the fold; pre-1.7 events are normalized on read and migrated physically at the next compaction.

Gap (Confirmed): spec §10.5's --scope active|all, --report, and --apply sync flags are not implemented (the shipped scope is open ∪ hash-dirty ∪ --keys). See §34.

15. Database Design

The event log is the database. Type: append-only JSONL, two files. Ownership: bin/worklog — append() is the only runtime writer (invariant 15.4); compact.py is the only rewriter (15.2). Connection strategy: O_APPEND file descriptor per write; atomicity guaranteed for lines under PIPE_BUF — hence the 2048-byte body cap. Transaction model: one event per write; there are no multi-event transactions, deliberately — no runtime command writes two files (spec §7).

Record types: create, update, close, reopen, link, conflict (append-time), snapshot, compact (compactor only). Indexing: none — the fold is a full scan; ULID sort order substitutes for a time index. Replication: git push/pull. Concurrent updates: union merge (.gitattributes) plus fold dedupe/sort. Backup and recovery: git history.

Migration strategy: schema evolution rides the fold's leniency — the taxonomy migration (docs/migrations/0001-type-split.md) normalizes on read; the IA migration (docs/migrations/0002-ia-content-model.md) is additive (sidecars + generated index), never rewrites frozen bodies. One-time canonical-hash churn was accepted only for the taxonomy split (canonical.py comment).

Retention: done.jsonl holds closed history forever; compaction prunes only stale entries for currently-open items. Sensitive data: none by design.

Ancillary stores (plain JSON/YAML, not event logs — direct load/dump sanctioned):

File Committed Purpose Writer
.work/config.yml yes all machine-readable settings humans
.work/published.json yes wiki page identity ledger (+ self-description after normalize) _register_published(), wiki-publish skill, ia.normalize
.work/sync-state.json gitignored per-clone last_pushed_hash, cursors, adapter_path Dispatcher._save_state(), cmd_adapter()
.work/suggestions.jsonl gitignored classifier + IA edge proposals (propose-only) classify skill; ia-graph --seed; cmd_promote()
docs/.index/_inventory.json yes content inventory ia.write_inventory()
docs/.index/_graph.json yes traceability graph ia_graph.write_graph()
docs/.index/<wiki_key>.yml yes frozen-doc sidecars + item PR overlays ia.normalize(), ia_graph.link_pr()
docs/.index/rendered/* yes Home, Sidebar, indexes ia_render.write_all()
docs/.index/publish-manifest.json yes publish plan for wiki-publish skill ia_render.build_manifest()
docs/.index/aliases.json yes wiki_key aliases / redirects ia_render.build_aliases()

Data lifecycle:

flowchart LR
    C["create event"] --> O["open item in todo.jsonl"]
    O -->|"updates, links, conflicts append"| O
    O -->|close event| CL["closed (still in todo.jsonl)"]
    CL -->|"nightly compaction"| D["snapshot in done.jsonl"]
    O -->|"nightly compaction"| S["single snapshot in todo.jsonl"]
    D -->|"reopen event (higher ev outsorts snapshot)"| O
    D -->|"compaction prunes stale done entries for reopened items"| X["removed"]
Loading

20. External Service Integrations

Ticket trackers via the typed adapter contract. Protocol: JSON over stdin/stdout to a subprocess, one call per verb (capabilities, push, pull, get, close). Authentication: the adapter's own tooling (gh auth for GitHub); the dispatcher never holds credentials. Retries: exit 4 → 3 retries with doubling backoff. Idempotency: marker worklog:<ulid> plus canonical-hash skip. Capability degradation: GitHub has no epic type, so epics push as story if available else task, with a drift note (outbound()).

GitHub PR merging via gh in merge-when-green.sh (§8.6).

Wiki publishing is skill-driven (github-wiki configured in .work/config.yml); the deterministic core contributes the ledger (published.json), worklog wiki-add, and (v0.13.0) publish-manifest.json + truth banners. Publish-time frontmatter strip for Gollum-style wikis (item 01KY5JB9F9). Frozen rules at the edge: plans, snapshots, and status reports publish once; live roadmap/designs republish when source_hash changes.

Azure DevOps ships no adapter; field-tested caveats are recorded in adapters/README.md.

Non-adapter systems, integration-guide skill (v0.16.0). For the six ticket/wiki systems with no shipped adapter (Jira, Confluence, GitLab, Azure DevOps, AWS CodeCatalyst, Google Cloud DevOps) and the four SDD tools this repo composes with rather than calls (Superpowers, GSD, SpecKit, OpenSpec), the integration-guide skill fetches a wiki page named Integration-<Name> at runtime and falls back to a bundled copy under docs/integrations/ if the fetch fails or resolves to the wiki's Home page instead (GitHub wikis 200 a missing slug by redirecting to Home rather than 404ing — the skill checks for a ## Recommended workflow heading before trusting the response). For Jira and Confluence specifically it defers to the existing global jira/ confluence skills (or an Atlassian MCP server) rather than raw REST calls. This is deliberately not a twelfth adapter: no code here talks to any of these six systems' APIs — the guidance is agent-researched prose, honestly labeled as such (docs/plans/2026-07-25-wiki-driven-integration-guides.md).

22. Security Design

Threat surface is small by construction: no server, no listener, no stored credentials. Confirmed controls:

Threat Component Mitigation Residual risk
Corrupt/hostile log line fold parse-tolerant skip; schema check in hook + CI one line's events lost, reported
Hand edit corrupting the log append path self-heal + hook + merge hook + CI re-run none observed
Adapter smuggling invariant logic contract banned-token scan; capabilities schema gate new invariants must extend BANNED
Malicious/buggy adapter output dispatcher JSON parse + schema validation before any push; pull lines individually parsed adapter runs with user privileges
Credential leakage adapters credentials live in platform CLIs (gh auth), never argv/log env hygiene
Merge bypass process branch protection + merge-when-green never --admin (ADR-0003) human override outside tooling
Frozen history rewrite via "metadata fix" IA normalize sidecars only for frozen docs; is_frozen() human hand-edit still possible; hooks warn

Authorization: the git repository's own access control. AI-specific: skills are propose-only where they touch state (classifier + ia-graph --seed write gitignored suggestions.jsonl, never the log), and the dispatcher, not the model, performs all remote mutations during worklog sync.

23. Error Handling and Resilience

Error taxonomy, Confirmed:

  • Validation errors (user-facing, non-retryable): sys.exit with a message naming the rule and often the spec section.
  • Data corruption (infrastructure): never fatal on read (fold leniency); fatal-by-refusal on write paths (hook, compaction verify).
  • External-dependency errors: adapter exit codes; retryable = 4 only; auth (2) aborts loudly; not-found (3) self-repairs by scheduling a re-push.
  • Contract errors: ContractError names the offending field path; the capabilities gate runs first, every run, before any push.
  • IA drift: check modes exit 1; pre-commit currently warns (Phase 5 will hard-fail). trace-check without --strict always warns.

Degradation ladder: no adapter → local-only, exit 0; adapter lacking pull → drift note; unsupported fields → drift note, never an error. Compensation: compaction's abort-and-delete of temp files on any verification failure.

24. Performance and Scalability

Confirmed characteristics: every read command folds the full log (O(n log n) in event count); acceptable because compaction bounds n. Appends are O(1) single writes. The Mermaid generator caps diagrams at MAX_NODES = 40 with a "+K more" note. IA inventory/render is a full docs walk — fine at current repo scale. Bottleneck if the system outgrows a team: the full-scan fold — Recommendation: a cached fold keyed on file mtimes would be the first lever.

25. Observability

No metrics stack — observability is artifacts, Confirmed:

Signal Where Producer
Sync outcome sync report: created=… conflicts=… deferred=… + drift Dispatcher.report()
Unresolved conflicts roadmap Needs attention; worklog list stderr render_roadmap.render()
Orphans / corrupt lines fold stderr warnings; roadmap Needs attention fold.main()
Work-in-flight age status report in_progress age_days _status_facts()
Unplanned-work ratio status "unplanned_in_window" _status_facts()
Version skew, missing hooks SessionStart doctor context hooks/session-doctor.sh
Compaction result commit message chore(worklog): compact through <ulid> compact.yml
IA drift pre-commit WARNING lines ia-normalize/inventory/render --check
Unlinked evidence worklog trace-check output ia_graph.trace_check()

Every status report records the exact through watermark, making it reproducible after the fact (spec §13.3).

26. Configuration and Secrets

Single config file, .work/config.yml, committed; policy prose lives in CLAUDE.md/AGENTS.md and carries no values scripts read (spec §4.1). Confirmed keys: project, ticketing.system/project, wiki.system/root_url, paths, status.*, sync.* (active_window_days, conflict_policy: report, push_on_capture), features.auto_merge_on_green, release.sync_docs (drives design-doc / walkthrough / user-guide / readme refresh at release), classifier.* (off by default). Readers parse it with naive block scans, no YAML library.

Environment variables: WORKLOG_TICKET_ADAPTER, WORKLOG_TICKET_SYSTEM / WORKLOG_TICKET_PROJECT, WORKLOG_FAKE_STATE, WORKLOG_AUTO_MERGE, WORKLOG_STOP_SETTLE. Secrets: none stored; platform CLIs own their auth. Deliberately not configurable: the body cap (§6).

27. Deployment Architecture

There is no deployed service. "Deployment" is three widening circles, Confirmed:

  1. Repo scaffoldbin/, hooks/ (armed via git config core.hooksPath hooks), .work/, CI workflows; committed, so it works for teammates and harnesses without the plugin (README "Repo install"). init.sh writes the relative hooks, but an absolute path to the same directory is equally armed and is what a git worktree needs, since a relative hooksPath there resolves against the wrong CWD. Both doctors accept either form as of v0.17.1 (hooks/session-doctor.sh 15–18, plugin/hooks/scripts/session-doctor.sh same, plugin/scripts/doctor.sh 63–68); before that, a correctly-wired worktree was reported broken. init.sh installs IA modules on scaffold (v0.13.0 fix items 01KY5ZY3ZX, 01KY6037BN).
  2. Claude Code pluginplugin/ packages skills, /worklog:* commands, hook wiring (plugin/hooks/hooks.json), and canonical script copies; version 0.17.1 in plugin/.claude-plugin/plugin.json locked to bin/worklog VERSION by tests/test_plugin.py. MIT LICENSE shipped. Drift (Confirmed): README.md's repo-layout table row still reads "v0.16.0" against a 0.17.1 VERSION — now two releases stale, the same recurring cosmetic gap noted at v0.14.0 through v0.17.0; tracked in the code walkthrough's Gaps section rather than fixed here (this document is generated, not hand-patched — readme is a separate release.sync_docs target).
  3. CIworklog.yml (invariants job re-runs the pre-commit script with WORKLOG_SKIP_BRANCH_GUARD=1, since the checkout runs with no commit in flight; a PR-scoped step added in v0.15.0 walks git rev-list --no-merges base..HEAD through hooks/commit-msg, requiring fetch-depth: 0; then unit + integration suites; coverage job with subprocess-aware .pth hook and --fail-under=80) and compact.yml (nightly, main-only, own commit, permissions: contents: write).

Rollback: git revert; /worklog:uninstall removes tooling but never data (README). Release flow: release.sync_docs lists the docs regenerated by background agents at every release, including this design doc and the walkthrough.

28. Testing Strategy

20 stdlib-unittest suites, no third-party test dependencies, run as for t in tests/test_*.py; do python3 "$t"; done (README). Categories, Confirmed:

  • Fold as executable spec: test_fold.py — file-order fold, close-assumes-done, ignored add/del, snapshot-merge, determinism, corrupt-line tolerance.
  • Deterministic ingest: test_ulid.py — TestTheBugThisPrevents.
  • Sync invariants without network: test_dispatch.py (idempotent double push, retry-no-duplicate, capabilities gate, degrade path, both-sides conflict, echo suppression, taxonomy ingest, dirty-close update-before-close, schema-mirror, orphan skip) against adapters/fake/adapter; test_adapter_contract.py banned-token dumbness scan.
  • PR simulation: test_integration.py — throwaway git repos with real branches, union merges, armed hooks.
  • Compaction safety: test_compact.py (fold preserved, snapshots outsort watermark, reopen restores fields, stale done pruning, closed-orphan verify).
  • IA content model (v0.13.0): test_ia.py — schema/constants equivalence and doc↔entity disjointness; frontmatter parse/dump; wiki_key derivation + ledger seed; inventory + sidecar precedence; normalize idempotence and dynamic truth_state; render pages/manifest/aliases and banner hash separation; ia-index pipeline; graph edges + link-pr overlay; trace-check warn/strict; ticket-body projection; seed-edges propose-only.
  • Item-id resolution (v0.17.1): test_resolve.py — prefix close/update/link reach the real item (each asserts len(items) == 1, which is what a phantom breaks), update's taxonomy and closed-item guards fire against real state, unknown ids leave the log byte-identical, ambiguous prefixes name both candidates, show/reopen unchanged.
  • The rest: taxonomy write rules, ingest whitelist, reopen, status facts, roadmap rendering, viz, plan capture, ADR invariants, merge-green, classifier promote, plugin mirror/version lockstep.

Coverage: CI gate ≥80% on bin/*.py (subprocess-aware via coverage.process_startup() in a .pth), target 95%. 19 → 20 suites at v0.17.1, the first new suite since v0.15.0: tests/test_resolve.py ships with the _resolve() fix, per the CLAUDE.md rule that new logic ships with tests. tests/test_dispatch.py — TestOrphanNeverPushed was also rewritten in the same release, and the reason is the fix itself: it used to manufacture a fold orphan by running worklog update against a nonexistent id, which _resolve() now refuses. The test writes the raw event line instead, with a comment naming the remaining real source of orphans — a git merge landing an update ahead of its create, not a CLI typo.

29. Local Development

Prerequisites: Python 3, git; gh only for GitHub sync/merge. Setup: clone, then git config core.hooksPath hooks (arms the invariants). Run tests as above. Try it: bin/worklog add "task" --level task --kind feature; bin/worklog roadmap-render; bin/worklog ia-index. Sync locally with no network: WORKLOG_TICKET_ADAPTER=$PWD/adapters/fake/adapter bin/worklog sync --dry-run, or validate an adapter with bin/worklog adapter check. Common failure: a commit rejected for a stale roadmap — run bin/worklog roadmap-render and re-commit. IA warnings: worklog ia-normalize && worklog ia-inventory && worklog ia-render.

30. Operations and Support

Incident Diagnosis Recovery
PR shows conflict on todo.jsonl in GitHub UI hosted platforms skip merge drivers (spec §8.1) merge base locally (union applies), roadmap-render, push, then merge
Compaction failed in CI compact: VERIFY FAILED for <id> diff printed logs untouched by design; fix the cause, rerun; never hand-edit
Sync aborts with auth failure adapter exit 2 re-auth with the tracker CLI, re-run; nothing was pushed after the failure
Duplicate wiki pages published.json entry lost url/rev restore ledger entry; _register_published() preserves url/rev on re-register
Conflict flood after upgrade one-time hash churn (taxonomy migration) expected; first sync re-pushes once, idempotent by marker
Stop hook blocks a finished session tree changed with no todo event record the item or state why none applies; settle via 2 s recheck
IA pre-commit WARNINGs metadata / inventory / render drift worklog ia-index (or normalize + inventory + render separately)
Unlinked evidence WARNING closed items without plan/ticket/PR worklog trace-check; link plan on create, ticket via sync, PR via link-pr

31. Risks, Tradeoffs, and Technical Debt

Item Description Probability Impact Mitigation
Clock-skew LWW fast clock wins ties low low actor+ts on every event; Lamport counter in v2 if it bites (spec §16)
Hosted union-merge gap PR-UI conflicts on the log medium low documented recovery; rebase before UI merge
Labels don't sync pull diffs INGEST_FIELDS only confirmed low planned work
Remote-origin tickets pull reports, never creates local items confirmed low future work, kept read-safe deliberately
Triple-copy mini-validator sync_dispatch, adr, tests low deliberate; extract on the fourth copy
Spec/CLI drift on sync scopes §10.5 flags lack CLI surface confirmed low §34
IA gates still warn-only Phase 5 not shipped confirmed medium promote to hard fail in Phase 5 (#98)
Schema constant mirrors doc/entity schemas duplicated in ia.py low TestSchemaSync pins equivalence
PR pages show no live metadata files-changed/CI/review always "not tracked" confirmed low filed follow-up #138 (worklog pr-sync), deliberately deferred
close/update don't resolve ID prefixes fixed in v0.17.1 — shared _resolve() backs close/update/link/reopen/resolve/show; ambiguous prefixes refused by name closed (#123); regression suite tests/test_resolve.py
Phantom items already in the log the fix stops new orphans, it does not retract old ones. worklog fold at v0.17.1 still returns two ids that are not 26-char ULIDs: 01KYA8MD (status done, an 8-char prefix close — the exact bug #123 describes) and 01KXSP277AE68GPTHC1QJV1NX (25 chars, cancelled, resolution: "orphan from id typo; neutralized") confirmed low log is append-only, so removal is a compact.py job; neither orphan carries an unresolved external key today
banner() mislabels frozen "current" docs reader-plane banner shows every frozen doc whose page is titled "current" as a status report regardless of doc_type confirmed low filed #137, verified on 12/14 published plan pages, not yet fixed

32. Implementation Plan (extension roadmap)

Built system; recommended next steps (Recommendation, grounded in open work items and in-code TODOs):

  1. Phase 5 IA (item #98 / 01KY5G9ZW0RABXWHEMEP1FAV2G): promote IA gates to hard fail; platform render adapters (GitLab/ADO/Confluence); /worklog:find
    • glossary.
  2. worklog pr-sync (#138): the artifact-pages follow-up — one gh pr view call per PR, written via ia.write_sidecar("pr/<num>", …), kept outside render_all() to preserve the network-free render invariant.
  3. Fix ID-prefix resolution on close/update (#123)done in v0.17.1, exactly as recommended here: the prefix match reopen already did was lifted into _resolve() and reused by all six item-naming commands. Remaining follow-on, unfiled: sweep the two pre-existing orphans out at the next compaction (§31).
  4. Fix banner() doc_type mislabeling (#137): frozen "current"-titled pages render as status reports regardless of actual doc_type.
  5. Configurable work-item field model (#108 / 01KY5NE0ZYGBWG44N0KPEBFCZ8): optional fields (estimate, risk, effort, value, confidence, owner, due_date, acceptance_criteria, blocked_by/blocks) behind work_item_fields config.
  6. Label sync via add/del on pull (marked future work in pull()).
  7. Remote-origin ticket creation on pull (drift-note today).
  8. Spec §17 open questions that gate features: multi-repo aggregation (Q5), superseded-plan unpublishing (Q7).

Critical path: of the two found-while-building bugs, #123 landed in v0.17.1 and #137 (cosmetic, banner labelling only) is now the sole remaining one — no longer release-blocking; Phase 5 hard-fail should land before large teams depend on IA banners; field model is independent of IA.

33. Requirement-to-Design Traceability

Req Workflow Module Store object CLI surface Test Signal
R1 §8.1 worklog.append event line add/update/close test_integration newline suite hook failure
R2 all reads fold.fold items list/show/fold test_fold stderr warns
R3 commit render_roadmap.render roadmap.md roadmap-render test_render_roadmap pre-commit diff
R4 §8.2 plan_capture + cmd_plan_capture plan doc + creates plan-capture test_plan_capture, integration plan PRs ExitPlanMode hook
R5 §8.3 sync_dispatch sync-state.json sync, adapter check test_dispatch sync report
R6 §8.3 pull ulid.deterministic + cmd_ingest update event w/ src ingest test_ulid, test_ingest dedupe count
R7 §9.3 cmd_conflict/cmd_resolve + fold _conflicts conflict/resolve test_fold conflict tests Needs attention
R8 §8.4 compact.compact snapshots + watermark compact --yes test_compact compact commit
R9 §8.5 _status_facts/cmd_status status doc status test_status frozen file
R10 §8.1 check_taxonomy + _normalize_taxonomy + hook level/kind/milestone add/update flags test_taxonomy hook failure
R11 publish _register_published published.json wiki-add ledger tests duplicate pages
R12 ADR flow adr.py docs/adr/*.md adr new/list/check test_adr hook + check exit 1
R13 §8.6 merge-when-green.sh /worklog:merge test_merge_green loop exit codes
R14 §8.7 ia / ia_render / ia_graph docs/.index/* ia-*, wiki-key, link-pr, trace-check, ticket-body test_ia pre-commit WARNING / trace
R15 commit hooks/pre-commit branch guard, hooks/commit-msg — (git-level gate, no store object) git commit TestBranchGuard, TestCommitMsgReference (tests/test_integration.py) hook failure ("pull-only" / missing reference)

34. Open Questions and Decisions Needed

From spec §17 (still open) plus gaps found writing this document:

  1. Does plan-next write anything? Spec'd read-only; a plan-start would be a separate skill. Owner: Rick. Low urgency.
  2. Epics on trackers without an epic type — degrade path ships (story/task + drift note); milestones-as-fallback remains unexplored. Owner: whoever builds the next adapter.
  3. Multi-repo roadmap/timecard — no answer in the spec; a consultant's week spans repos. Impact of delay: per-repo timecards stay the wrong unit.
  4. Superseded plans on the wiki — leaning "banner, don't unpublish" (IA truth_state banners land in v0.13.0; unpublishing still open).
  5. Should worklog sync grow spec §10.5's --scope/--report/--apply surface, or should the spec be amended to the shipped flags? The shipped scope rule (open ∪ hash-dirty ∪ keys) covers the need; Recommendation: amend the spec. Impact of delay: doc drift only.
  6. When to promote IA gates to hard fail? Phase 5 item #98; residual risk while warn-only is silent merge of drifted metadata if warnings are ignored.

35. Appendices

Mermaid diagram index: §4 system context; §5 logical architecture + event-log data flow; §8.2 plan-capture sequence; §8.3 sync sequence; §8.7 IA pipeline; §9.2 item state diagram; §10 ER diagram; §15 data lifecycle.

Example event lines (spec §5.1, shapes confirmed against cmd_add/cmd_ingest):

{"actor":"rick","ev":"01J8X2K4A0...","item":"01J8X0M2QQ...","op":"create","set":{"kind":"feature","level":"task","priority":"P1","status":"todo","title":"Extract auth middleware"},"ts":"2026-07-16T14:02:11Z"}
{"actor":"github","ev":"01J8X4RR10...","item":"01J8X0M2QQ...","op":"update","set":{"priority":"P0"},"src":{"key":"412","rev":"2026-07-16T15:39:58Z","system":"github"},"ts":"2026-07-16T15:39:58Z"}

Exit-code catalog (adapter contract §3.6): 0 success · 2 auth (abort) · 3 not-found (re-push next run) · 4 transient (retry ×3) · 5 remote conflict (record) · 1 other (drift, continue).

v0.17.1 release highlights (Confirmed against changelog / plan / code — git log v0.17.0..v0.17.1, four commits plus merges):

  • Fix, bin/worklog: new _resolve() helper (lines 115–130) resolves a full ULID or an unambiguous prefix and is now the single lookup for update/close/reopen/link/resolve/show. close, update, and link previously wrote events under the raw caller string, minting phantom orphan items; update additionally skipped its taxonomy and closed-item guards because its state lookup returned {} for a prefix. Ambiguous prefixes are now refused by name instead of resolving to match[0]. Full analysis in §9.2; plan docs/plans/2026-07-27-post-v017-drift-and-prefix-resolution.md, GitHub #123, PR #213.
  • Fix, doctor: hooks/session-doctor.sh (15–18), plugin/hooks/scripts/session-doctor.sh (same, mirrored), and plugin/scripts/doctor.sh (63–68) compared git config core.hooksPath against the literal hooks and failed anything else. A git worktree resolves a relative hooksPath against the wrong CWD and so needs the absolute path — which doctor then called a broken install. All three now also accept a path that resolves to $(pwd -P)/hooks, and the failure message quotes the value found.
  • Test: tests/test_resolve.py, 143 lines, 8 cases — prefix close/update/ link hit the real item, both bypassed update guards fire again, unknown ids leave the log byte-identical, ambiguous prefixes name both candidates, show/reopen keep working.
  • No adapter, schema, event-format, or config change; no new CLI subcommand.

v0.17.0 release highlights (Confirmed against changelog / plan / code):

  • docs/graph-engineering.md (plan docs/plans/2026-07-26-declare-graph-engineering.md): a new documentation-only artifact declaring, with real path — function(), lines N–M citations, that this repo already implements the four primitives the industry now calls "graph engineering" — ULID node identity, ia_graph.py's typed edges, fold.py's event-sourced persistent state, and the inventory/publish-manifest index. No new subsystem and no bin/ code; a README callout links it. Not covered further in this design document beyond this entry, since it documents the system rather than adding to it.
  • bin/worklog plan-capture invariant-15.8 guard, two-part correctness fix (PR #198 + same-day follow-up) — see the invariant box in §8.2 above for the full before/after. Net effect: the guard is now slug-scoped and anchored on the YYYY-MM-DD- date field boundary, closing both a false-negative (silent duplicate across a UTC/local date boundary) and a false-positive (suffix collision on a longer slug) found in the same review cycle.
  • Generated wiki Home gained a [[Code-Walkthrough]] link alongside [[Design-Doc]] (ia_render.render_home()) — a walkthrough published by the design-docs release flow was previously unreachable from Home.
  • Two new regression tests in tests/test_integration.py (TestPlanCapturePR); no adapter, schema, or CLI-surface change.

v0.16.1 release highlights (Confirmed against changelog / code):

  • Fix (bin/sync_dispatch.py): the closed-item sync path raised KeyError: 'key' when a closed item that had never been pushed to the tracker was forced into scope via --keys — the branch assumed a remote key already existed. It now creates the ticket first, links it, then closes, mirroring how the open-item branch already handles create-vs-update.
  • Design doc and code walkthrough regenerated and frozen for v0.16.0 (this release's own doc-sync pass, run one release late — see §6 for the drift that created).
  • docs/user_guide/plugin-guide.md and README.md updated for the integration-guide skill shipped in v0.16.0.

v0.16.0 release highlights (Confirmed against changelog / plan / code):

  • Wiki-driven integration guides (plan docs/plans/2026-07-25-wiki-driven-integration-guides.md, epic #164): a new integration-guide skill (plugin/skills/integration-guide/SKILL.md) points agents at living, wiki-hosted setup guides instead of hard-coding that knowledge into the shipped skill set.
  • Zero new bin/ code. The feature is built entirely from WebFetch (already available to any Claude Code agent) plus the pre-existing worklog wiki-add / wiki-publish pipeline — no adapter, no new CLI subcommand.
  • Eleven fallback pages under docs/integrations/fallback-*.md (Superpowers, GSD, SpecKit, OpenSpec, Jira, Confluence, GitHub, GitLab, Azure DevOps, AWS CodeCatalyst, Google Cloud DevOps) plus a docs/integrations/README.md index, each following a fixed ten-section template so the skill's lookup logic works identically across all eleven.
  • Honesty framing (deliberate design decision): the seven ticket/wiki pages state plainly that only GitHub has a real, shipped adapter — the other six describe agent-researched-at-runtime CLI/MCP/REST guidance, matching ticket-sync/SKILL.md's existing Azure DevOps caveats tone rather than implying non-existent adapter machinery.
  • Redirect-to-Home detector: the skill verifies a fetched wiki page actually contains a ## Recommended workflow heading before trusting it, because a GitHub wiki returns HTTP 200 for a missing page slug by silently redirecting to Home rather than 404ing — a naive "fetch succeeded" check would be fooled into treating the Home page as the requested integration guide.
  • Skill-reuse pointer, not reimplementation: for Jira and Confluence, the page instructs checking for the global jira/confluence skill (or an Atlassian MCP server) before any raw REST call; Confluence's page also covers converting Mermaid/PlantUML diagrams to images before upload, since Confluence storage format cannot render fenced diagrams directly.
  • No test suite changed — see §28.

v0.15.0 release highlights (Confirmed against changelog / plan / code):

  • Branch discipline (plan docs/plans/2026-07-25-branch-discipline-hooks.md), shipped in response to a real incident: local main drifted 13 commits ahead of origin/main for hours because every commit landed straight on main.
  • New block in hooks/pre-commit: refuses git commit on main/master outright ("main/master is pull-only"). Exempted via MERGE_HEAD (a reconciliation merge already on disk) or the caller-set WORKLOG_MERGE_COMMIT (set by hooks/pre-merge-commit before it execs into pre-commit, since MERGE_HEAD is empirically not yet on disk at that point in git's own merge sequence) — either signal lets git merge origin/main keep working while direct authored commits on main are blocked.
  • New hooks/commit-msg hook: every non-merge commit message must contain a 26-char Crockford ULID or a #123 ticket reference, or the commit is refused.
  • WORKLOG_SKIP_BRANCH_GUARD=1 added to the three non-commit callers of hooks/pre-commit that would otherwise false-positive on main with no commit in flight: plugin/scripts/doctor.sh's health check, the CI invariants step, and two tests/test_integration.py "CI gate passes" assertions.
  • Wired into plugin/scripts/init.sh (hook-copy loop, CI template), uninstall.sh (symmetric removal), doctor.sh (existence/exec check + guarded invocation), and tests/test_plugin.py's CANON list.
  • .github/workflows/worklog.yml gains a PR-scoped step walking git rev-list --no-merges base..HEAD through hooks/commit-msg (fetch-depth: 0 now required for the checkout).
  • plugin/skills/release/SKILL.md §3's "direct-commit repos: commit on the default branch" mode removed — dead once the branch guard ships; describes branch+PR landing only.
  • New TestBranchGuard / TestCommitMsgReference test classes in tests/test_integration.py; ~23 pre-existing commit_all() fixture calls and 3 raw git commit calls in tests/test_plugin.py updated to either no_verify=True (pure setup commits) or moved onto a branch with a reference in the message.

v0.14.0 release highlights (Confirmed against changelog / plan / code):

  • Artifact pages (plan docs/plans/2026-07-24-artifact-pages.md): one generated page per work item (Ticket-<ULID>), release (Release-<tag>), and PR (PR-<num>), reusing the existing graph edges rather than adding stored fields.
  • ia_graph.build_adjacency() / item_links(): one shared forward/backward edge projection used by every new renderer instead of each re-walking graph["edges"].
  • Release pages carry a graph-derived Change Log (milestone-tagged closed items + their linked PRs) — a separate, always-accurate, mechanical list; CHANGELOG.md itself stays untouched, human-authored prose.
  • New worklog ia-ticket <ULID> preview subcommand.
  • Published-page manifest grew from 51 to 258 entries.
  • Two bugs found while building this, filed but not yet fixed: close/ update don't resolve item-id prefixes the way reopen does (#123); banner() mislabels frozen "current"-titled docs as status reports regardless of doc_type (#137).
  • Live PR metadata (files changed, review/CI status) deliberately deferred to a filed follow-up, worklog pr-sync (#138) — not built this release.

v0.13.0 release highlights (Confirmed against changelog / plans / code):

  • IA Phases 0–4: schema, inventory, normalize, reader plane, release/plan wiring, traceability graph, rich tickets, link-pr, trace-check.
  • Schema split: documents vs entities (#111).
  • Compaction closed-orphan verify fix (#101).
  • Readable ticket bodies (spec §13.4); wiki frontmatter strip at publish.
  • PYTHONDONTWRITEBYTECODE in pre-commit (hook never dirties worktree).
  • MIT LICENSE; init.sh installs IA modules on scaffold.
  • UI epic moved/cancelled to wiki_ticket_sdd_ui.

Omitted sections

  • §12 Package-by-Package Designbin/ is a single flat directory of twelve modules; §11 already covers it at module grain, and there are no packages or import-direction rules beyond those shown in §5.
  • §16 Cache Design — nothing caches. .work/sync-state.json is per-clone sync bookkeeping (covered in §15/§20), not a cache with TTL/eviction semantics.
  • §17 MCP Server Integration — no MCP server exists in this repo; skills may use a team's MCP tooling, but no server is shipped, configured, or called by code.
  • §18 AI Endpoint Design — no code calls an AI endpoint. Model work happens in the harness via skills (prose), outside this codebase's runtime.
  • §19 Managed AI Platform Integration — no Bedrock/Vertex/Azure OpenAI or any managed AI platform is present.
  • §21 Event-Driven and Asynchronous Processing — no queue, topic, or broker exists; the event log is a persistence structure (§15), and the only asynchrony is background subagents and CI, covered in §8/§27.

Closing summary

Top architectural risks: (1) the hosted-platform union-merge gap; (2) compaction as the single rewriting operation — well-gated, but the one place state loss is possible; (3) IA gates still warn-only until Phase 5; (4) labels and remote-origin tickets still don't pull (deliberate read-safety); (5) of the two correctness bugs found while building artifact pages, ID-prefix resolution (#123) is fixed in v0.17.1 and banner mislabeling (#137) — cosmetic — remains open; the phantom items the old prefix path already wrote are still in the log (§31).

Immediate decisions required: Phase 5 hard-fail timing; amend spec §10.5 to the shipped sync flags or schedule the flags (Open Question 5); decide ADO adapter timing given the recorded caveats; decide whether the next compaction should sweep the two pre-existing orphan items.

Recommended implementation order: §32 items 1→2→3→4.

Information still needed from stakeholders: multi-repo requirements (Open Question 3).

Clone this wiki locally