-
Notifications
You must be signed in to change notification settings - Fork 0
Design Doc 2026 07 28_v0.17.1 release
Snapshot of release v0.17.1 (2026-07-28) — 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.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.
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. -
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:WebFetchreads the liveIntegration-<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.mdindex, registered through the existingworklog 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).
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.
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 |
| 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).
| 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.
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 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
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:
- The original guard checked only
os.path.exists(f"docs/plans/{date}-{slug}.md")withdatefromtime.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 globbingdocs/plans/*-{slug}.mdacross all dates instead of checking one fixed path (PR #198). - That first fix's bare
*-{slug}.mdglob matched by raw suffix, not field boundary: a search for slugmigrationalso matched an existing2026-07-01-database-migration.md(real slugdatabase-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 fixedYYYY-MM-DD-date shape withre.compile(r"^\d{4}-\d{2}-\d{2}-%s\.md$" % re.escape(slug))againstglob.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).
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).
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.
| 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.16.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.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.
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.
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).
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.shwrites the relativehooks, but an absolute path to the same directory is equally armed and is what a git worktree needs, since a relativehooksPaththere resolves against the wrong CWD. Both doctors accept either form as of v0.17.1 (hooks/session-doctor.sh15–18,plugin/hooks/scripts/session-doctor.shsame,plugin/scripts/doctor.sh63–68); before that, a correctly-wired worktree was reported broken.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.17.1inplugin/.claude-plugin/plugin.jsonlocked tobin/worklog VERSIONbytests/test_plugin.py. MIT LICENSE shipped. Drift (Confirmed):README.md's repo-layout table row still reads "v0.16.0" against a0.17.1VERSION — 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 —readmeis a separaterelease.sync_docstarget). -
CI —
worklog.yml(invariants job re-runs the pre-commit script withWORKLOG_SKIP_BRANCH_GUARD=1, since the checkout runs with no commit in flight; a PR-scoped step added in v0.15.0 walksgit rev-list --no-merges base..HEADthroughhooks/commit-msg, requiringfetch-depth: 0; 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.
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) 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. -
Item-id resolution (v0.17.1):
test_resolve.py— prefix close/update/link reach the real item (each assertslen(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/reopenunchanged. - 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.
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 |
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 |
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— done in v0.17.1, exactly as recommended here: the prefix matchclose/update(#123)reopenalready 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). -
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: 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.
| 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) |
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.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 forupdate/close/reopen/link/resolve/show.close,update, andlinkpreviously wrote events under the raw caller string, minting phantom orphan items;updateadditionally 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 tomatch[0]. Full analysis in §9.2; plandocs/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), andplugin/scripts/doctor.sh(63–68) comparedgit config core.hooksPathagainst the literalhooksand failed anything else. A git worktree resolves a relativehooksPathagainst 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 bypassedupdateguards fire again, unknown ids leave the log byte-identical, ambiguous prefixes name both candidates,show/reopenkeep 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(plandocs/plans/2026-07-26-declare-graph-engineering.md): a new documentation-only artifact declaring, with realpath — function(), lines N–Mcitations, 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 nobin/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-captureinvariant-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 theYYYY-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 raisedKeyError: '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.mdandREADME.mdupdated for theintegration-guideskill 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 newintegration-guideskill (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 fromWebFetch(already available to any Claude Code agent) plus the pre-existingworklog 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 adocs/integrations/README.mdindex, 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 workflowheading 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/confluenceskill (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: localmaindrifted 13 commits ahead oforigin/mainfor hours because every commit landed straight onmain. - New block in
hooks/pre-commit: refusesgit commitonmain/masteroutright ("main/master is pull-only"). Exempted viaMERGE_HEAD(a reconciliation merge already on disk) or the caller-setWORKLOG_MERGE_COMMIT(set byhooks/pre-merge-commitbefore itexecs intopre-commit, sinceMERGE_HEADis empirically not yet on disk at that point in git's own merge sequence) — either signal letsgit merge origin/mainkeep working while direct authored commits onmainare blocked. - New
hooks/commit-msghook: every non-merge commit message must contain a 26-char Crockford ULID or a#123ticket reference, or the commit is refused. -
WORKLOG_SKIP_BRANCH_GUARD=1added to the three non-commit callers ofhooks/pre-committhat would otherwise false-positive onmainwith no commit in flight:plugin/scripts/doctor.sh's health check, the CI invariants step, and twotests/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), andtests/test_plugin.py'sCANONlist. -
.github/workflows/worklog.ymlgains a PR-scoped step walkinggit rev-list --no-merges base..HEADthroughhooks/commit-msg(fetch-depth: 0now 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/TestCommitMsgReferencetest classes intests/test_integration.py; ~23 pre-existingcommit_all()fixture calls and 3 rawgit commitcalls intests/test_plugin.pyupdated to eitherno_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-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) 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).
- 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