-
Notifications
You must be signed in to change notification settings - Fork 0
Design Doc 2026 07 24_v0.14.0 release
Snapshot of release v0.14.0 (2026-07-24) — frozen record. The current version is Design-Doc.
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.14.0.
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/ (fifteen dated
plan documents — the why record, most recently
docs/plans/2026-07-24-artifact-pages.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), 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.
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/reopenappends events;worklog list/show/foldreads 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, thenia-indexrefreshes the reader plane. -
Generate the roadmap:
worklog roadmap-renderis 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 syncdrives 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 fromworklog ticket-body(traceability projection). -
Report status:
worklog status --emit-factsproduces deterministic facts; a skill writes the prose;worklog status --writefreezes the report. -
IA / content model (v0.13.0, extended v0.14.0):
worklog ia-normalizebackfillswiki_key+truth_state(sidecars for frozen docs, in-place for live docs);ia-inventory/ia-render/ia-graphmaterialize the reader plane and traceability graph;trace-checkreports 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.
Major components: the worklog CLI (the API, 1189 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, and the Claude Code plugin.
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).
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 |
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.
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
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.
Layers, all Confirmed from imports:
-
Identity:
ulid.py(no imports from siblings). -
State:
fold.py(importshashlib,jsononly). -
CLI/API:
bin/worklogimportsulid,fold; lazily importsrender_roadmap,plan_capture,compact,sync_dispatch,adr,ia,ia_render,ia_graph. -
Rendering:
render_roadmap.pyimportsulid,fold;viz_mermaid.pyimportsulid,foldand is imported lazily byrender_roadmap.render(). -
Sync:
sync_dispatch.pyimportscanonical; drives adapters and the CLI as subprocesses. -
IA plane:
ia.pyimportsfold;ia_render.py/ia_graph.pyimportia(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
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
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.
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 |
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).
| Component | Type | Responsibility | Inputs | Outputs | Depends on | Failure impact |
|---|---|---|---|---|---|---|
bin/worklog |
CLI (1189 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
|
Git hooks | Newline/schema/taxonomy/roadmap/ADR/IA gates | staged tree | pass/fail (+ IA warnings) | python3 | invariants unenforced locally |
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 |
Owner for all components: the repo (single-team). Scaling model: none needed — per-repo files.
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.
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 docs/plans/<date>-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
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).
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
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.
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.
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.
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).
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
-
Normalize (
ia.normalize()): frozen docs get additive sidecars; sanctioned-live docs (roadmap,current_*designs, guides, ADR status) get in-place identity fields only.truth_stateis 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()), deterministicpublish-manifest.jsonandaliases.json. -
Graph: typed edges (
produces,decides,implements,supersedes,verified-by,lands-in, …);link-proverlays PR/commit edges on item sidecars (event log still owns item state);trace-checklists closed items missing plan/ticket/PR links (--strictat release). -
Seed (
ia-graph --seed): propose-only edges into gitignoredsuggestions.jsonl— never auto-mutates docs. -
Artifact pages (v0.14.0):
render_all()additionally loops every fold item and everyrelease/prgraph node, callingia_graph.build_adjacency()once per pass anditem_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 aCHANGELOG.mdparser, 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" (nogh pr viewcall exists in this codebase yet — #138).build_manifest()grew a second loop keyed off thetickets/,releases/,prs/filename prefixes so a future entity type needs only a new prefix, not a new loop.
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()):
- Order is by
ev, never file position, neverts; ties break on(actor, sha256(line))so every machine folds identically (dedupe_and_sort()). - Events at or below the compact watermark are dropped, except
snapshot(apply_watermark()). -
snapshotreplaces state entirely; a duplicatecreatedegrades to an update. - Mutation order within an event:
del, thenadd, thenset(_apply_mutations()). -
closetakes status fromset; only a missing/open status defaults todone— acancelleditem must never report as shipped. -
reopenclears closed status andresolution. -
conflictappends toitem._conflictsand changes no state; any later event writing that field clears the conflict. - Events for unknown items become
_orphanpartial items — reported, never invented, never fatal. - Taxonomy is normalized leniently on create/snapshot: legacy
typemaps viaLEGACY_TYPE_MAP; a create with neither type nor kind folds tokind:triage. Hard validation lives at write time and in the hooks — the fold never crashes on a bad pair.
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 --> [*]
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).
| 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()).
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.
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).
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"
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).
| 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.14.0").
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.
The CLI is the API. Global flags: --actor (defaults to $USER),
--version (worklog 0.14.0). All log writes go through append() — a single
O_APPEND write, newline-terminated, self-healing a missing prior newline.
| 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
|
close <item> |
close |
--status done|cancelled, --resolution
|
1 close event | — |
reopen <item> |
reopen a closed item | prefix match on the id | 1 reopen event | not closed / no match |
link <item> |
record external identity |
--system --key required; --url --rev --hash
|
1 link event | — |
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; prefix match on show
|
none | — |
plan-capture |
plan doc + epic + tasks |
--slug --title required, --file, --priority
|
N create events + plan doc | existing path refused (frozen) |
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.
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"]
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.
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.
Error taxonomy, Confirmed:
-
Validation errors (user-facing, non-retryable):
sys.exitwith 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:
ContractErrornames 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-checkwithout--strictalways 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.
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.
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).
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).
There is no deployed service. "Deployment" is three widening circles, Confirmed:
-
Repo scaffold —
bin/,hooks/(armed viagit config core.hooksPath hooks),.work/, CI workflows; committed, so it works for teammates and harnesses without the plugin (README "Repo install").init.shinstalls IA modules on scaffold (v0.13.0 fix items 01KY5ZY3ZX, 01KY6037BN). -
Claude Code plugin —
plugin/packages skills,/worklog:*commands, hook wiring (plugin/hooks/hooks.json), and canonical script copies; version0.14.0inplugin/.claude-plugin/plugin.jsonlocked tobin/worklog VERSIONbytests/test_plugin.py. MIT LICENSE shipped. Drift (Confirmed):README.md's repo-layout table still labels this row "v0.13.0" — stale by one release; harmless (cosmetic), tracked in the code walkthrough's Gaps section rather than fixed here (this document is generated, not hand-patched). -
CI —
worklog.yml(invariants job re-runs the pre-commit script, then unit + integration suites; coverage job with subprocess-aware.pthhook and--fail-under=80) andcompact.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.
19 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) againstadapters/fake/adapter;test_adapter_contract.pybanned-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-indexpipeline; graph edges + link-pr overlay; trace-check warn/strict; ticket-body projection; seed-edges propose-only. - 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%.
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.
| 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
|
| 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 |
a short valid prefix silently creates an orphan instead of resolving (only reopen does prefix match) |
confirmed | medium | filed #123, found while building artifact pages, not yet fixed |
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 |
Built system; recommended next steps (Recommendation, grounded in open work items and in-code TODOs):
-
Phase 5 IA (item #98 / 01KY5G9ZW0RABXWHEMEP1FAV2G): promote IA gates to
hard fail; platform render adapters (GitLab/ADO/Confluence);
/worklog:find- glossary.
-
worklog pr-sync(#138): the artifact-pages follow-up — onegh pr viewcall per PR, written viaia.write_sidecar("pr/<num>", …), kept outsiderender_all()to preserve the network-free render invariant. -
Fix ID-prefix resolution on
close/update(#123): reuse the prefix matchreopenalready does, so a short valid prefix never silently creates an orphan. -
Fix
banner()doc_type mislabeling (#137): frozen "current"-titled pages render as status reports regardless of actualdoc_type. -
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_fieldsconfig. -
Label sync via add/del on pull (marked future work in
pull()). - Remote-origin ticket creation on pull (drift-note today).
- Spec §17 open questions that gate features: multi-repo aggregation (Q5), superseded-plan unpublishing (Q7).
Critical path: the two found-while-building bugs (#123, #137) are small and correctness-affecting — fix before the next release; Phase 5 hard-fail should land before large teams depend on IA banners; field model is independent of IA.
| 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 |
From spec §17 (still open) plus gaps found writing this document:
-
Does
plan-nextwrite anything? Spec'd read-only; aplan-startwould be a separate skill. Owner: Rick. Low urgency. - 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.
- 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.
- Superseded plans on the wiki — leaning "banner, don't unpublish" (IA truth_state banners land in v0.13.0; unpublishing still open).
-
Should
worklog syncgrow spec §10.5's--scope/--report/--applysurface, 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. - 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.
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.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-walkinggraph["edges"]. - Release pages carry a graph-derived Change Log (milestone-tagged closed
items + their linked PRs) — a separate, always-accurate, mechanical list;
CHANGELOG.mditself 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/updatedon't resolve item-id prefixes the wayreopendoes (#123);banner()mislabels frozen "current"-titled docs as status reports regardless ofdoc_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.
-
PYTHONDONTWRITEBYTECODEin pre-commit (hook never dirties worktree). - MIT LICENSE; init.sh installs IA modules on scaffold.
- UI epic moved/cancelled to
wiki_ticket_sdd_ui.
-
§12 Package-by-Package Design —
bin/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.jsonis 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.
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) two correctness bugs found while building artifact pages and not yet fixed —
ID-prefix resolution on close/update (#123) and banner mislabeling (#137).
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; prioritize #123/#137 fixes against the next release.
Recommended implementation order: §32 items 1→2→3→4.
Information still needed from stakeholders: multi-repo requirements (Open Question 3).
- Roadmap
- Design-Doc · Code-Walkthrough
- Plan: Plan-ia-content-model
- Plan: Plan-ticket-sync-and-init-detection
- ADR-0001-event-log-fold-union-merge
- ADR-0002-skill-based-edges-typed-contract
- ADR-0003-green-gates-merge
- Index-Releases
- Latest snapshot: Roadmap-2026-07-29_v0.18.0-release
- Index-Status
- Index-Decisions