-
Notifications
You must be signed in to change notification settings - Fork 0
Code Walkthrough 2026 07 27_v0.17.0 release
Snapshot of release v0.17.0 (2026-07-27) — frozen record. The current version is Code-Walkthrough.
The guided tour a new team member reads on day one. Every claim is anchored in
the code at the commit above. The companion design document is
docs/designs/current_design_doc.md; where this walkthrough and that document
disagree with the code, the code wins (drift is reported in §6).
Three sentences: Worklog tracks work as an append-only JSONL event log inside
the git repo; item state is a fold over the events, and git's union merge makes
concurrent writes compose instead of conflict. Everything human-readable — the
roadmap, status reports, Mermaid diagrams, and (since v0.13.0) the IA reader
plane under docs/.index/ — is generated from the log and committed docs, and
everything remote — tickets, wiki pages — is a mirror driven through a typed
dispatcher or a skill. The rules that matter are enforced by hooks and CI, not by
memory.
Directory map:
| Path | What lives there and why |
|---|---|
bin/worklog |
The CLI and the only writer of .work/*.jsonl (1206 lines). |
bin/fold.py |
The only code allowed to decide what the log means. |
bin/ulid.py |
IDs. Includes the deterministic form that makes ingest idempotent across clones. |
bin/canonical.py |
THE canonical hash (sync change detection). 34 lines, high blast radius. |
bin/sync_dispatch.py |
Every ticket-sync invariant, in one place. |
bin/compact.py |
The only file rewriter. CI-only, verification-gated. |
bin/render_roadmap.py, bin/viz_mermaid.py
|
Byte-deterministic roadmap + diagrams. |
bin/plan_capture.py, bin/adr.py
|
Pure helpers: plan parsing, ADR tooling. |
bin/ia.py |
wiki_key, truth_state, inventory, normalize, sidecars (IA foundation). |
bin/ia_render.py |
Reader plane: Home, Sidebar, indexes, publish-manifest, aliases, and (v0.14.0) ticket/release/PR artifact pages (670 lines). |
bin/ia_graph.py |
Traceability graph, link-pr, ticket-body, trace-check, and (v0.14.0) build_adjacency()/item_links() (302 lines). |
.work/ |
The log (todo.jsonl, done.jsonl), config.yml, ledgers. |
docs/.index/ |
Generated IA plane (committed, regenerate-and-diff). |
adapters/ |
github (worked example), fake (CI double), authoring rules. |
hooks/ |
git pre-commit/pre-merge-commit/commit-msg (v0.15.0) + four Claude Code hooks. |
plugin/ |
Claude Code packaging; plugin/scripts/ mirrors bin/ + hooks/; plugin/skills/integration-guide/ new in v0.16.0. |
schema/ |
capabilities, adapter-io, adr, doc, entity JSON schemas. |
tests/ |
19 stdlib-unittest suites; the executable spec (includes test_ia.py). |
docs/ |
Generated roadmap, frozen plans/status/ADRs/designs, the spec (v1.9). |
docs/integrations/ |
Eleven per-system setup guides + index (v0.16.0), prose only, no code. |
The one diagram (derived from actual imports and subprocess calls):
flowchart TD
U["dev / agent / skill"] --> WL["bin/worklog"]
WL -->|"append() — sole writer"| LOG[(".work/todo.jsonl<br/>.work/done.jsonl")]
LOG --> FOLD["fold.py"]
FOLD --> WL
FOLD --> RR["render_roadmap.py"] --> VIZ["viz_mermaid.py"]
RR --> MD["docs/roadmap.md"]
WL -.->|"lazy import"| SD["sync_dispatch.py"]
SD -->|subprocess| AD["adapters/*/adapter"] --> TR["tracker (gh CLI)"]
SD -->|"subprocess: link / ingest / conflict"| WL
WL -.->|"lazy import"| IA["ia.py"]
IA --> IDX["docs/.index/_inventory.json<br/>sidecars"]
WL -.-> IAR["ia_render.py"] --> REND["docs/.index/rendered/*"]
WL -.-> IAG["ia_graph.py"] --> G["docs/.index/_graph.json"]
CP["compact.py (CI nightly)"] -->|"verified rewrite"| LOG
HK["hooks/pre-commit + CI"] -->|"gate, never write"| LOG
HK -->|"WARN ia-normalize / inventory / render / trace"| IDX
Entry: argparse dispatches to cmd_add (bin/worklog, build_parser tail +
cmd_add body at lines 81–105). Validation happens before any write —
--unplanned requires --discovered-during, and taxonomy rules are hard here
even though the fold is lenient:
def check_taxonomy(level, kind, milestone):
"""Write-time rules, taxonomy spec §2. The fold is lenient; this is not."""
if level == "epic":
if kind in ("bug", "triage"):
sys.exit(f"worklog: an epic cannot be kind:{kind} — epics are "
"feature or ops (taxonomy §2.2)")
if milestone is not None:
sys.exit("worklog: milestone lives on leaves; epic milestones are "
"derived (taxonomy §2.5)")— bin/worklog — check_taxonomy(), lines 70–79
Note the cmd_add comment: kind is only written when given — an omitted kind
folds to triage (§2.3), never silently to feature. Unclassified must look
unclassified.
Every event then funnels through the single writer:
def append(event):
"""The only writer. Single O_APPEND write, always newline-terminated. ..."""
if len(event.get("set", {}).get("body", "")) > MAX_BODY:
sys.exit(f"worklog: body exceeds {MAX_BODY}B; put prose in the plan doc")
line = json.dumps(event, separators=(",", ":"), sort_keys=True) + "\n"
fd = os.open(LOG, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o644)
try:
if os.fstat(fd).st_size:
rfd = os.open(LOG, os.O_RDONLY)
try:
os.lseek(rfd, -1, os.SEEK_END)
if os.read(rfd, 1) != b"\n":
line = "\n" + line
finally:
os.close(rfd)
os.write(fd, line.encode()) # atomic under PIPE_BUF
finally:
os.close(fd)
return event— bin/worklog — append(), lines 35–58
What it receives: a finished event dict. What it returns: the event. What can
fail: an oversized body exits before the write; everything else is one atomic
write(). Why it is written this way: the self-heal (lseek -1; read 1) repairs
a hand-edited file missing its trailing newline — without it, O_APPEND would
fuse two events into one unparseable line and lose both (spec §8.2). MAX_BODY = 2048 (line 31) is "derived from PIPE_BUF … Not a setting." VERSION = "0.15.0"
(line 32) is lockstepped with the plugin by tests/test_plugin.py.
Every read command (list, show, fold, roadmap, status, sync scope, IA plan
lifecycle) calls fold([todo, done]). Four stages, each load-bearing:
Parse tolerantly — read_lines() (bin/fold.py): a bad line is reported
into result.errors and skipped. The docstring explains why this is not
politeness: union merge plus a missing newline "can fuse two valid lines into
one invalid one, and that must cost two events, not the entire history."
Dedupe and sort deterministically:
return sorted(
seen.values(),
key=lambda e: (
e["ev"],
e.get("actor", ""),
hashlib.sha256(e["_line"].encode()).hexdigest(),
),
)— bin/fold.py — dedupe_and_sort()
ULIDs sort lexicographically by time, so ordering is a string sort; the
(actor, line-hash) tiebreak makes two machines fold the same bag of lines
identically — the property union merge depends on.
Apply the watermark — apply_watermark() drops everything the compactor
already folded, except snapshot events, which carry the state those events
produced.
Replay — fold(). The subtleties that bite naive implementations, each with
a guarding test (§4): snapshot replaces state entirely (never merges); a
duplicate create degrades to an update; close takes its status from set
(only defaulting to done when nothing closed was set); conflict records
without changing state; and a later write to a conflicted field clears the
conflict while an earlier one does not, because events apply in ev order
(_apply_mutations()).
Orphans: an event for an item with no create/snapshot creates
{"id": iid, "_orphan": True} — "Report it; never crash, never silently invent
an item." Legitimate mid-rebase. Compaction counts orphans as open so it never
drops them (bin/compact.py partition comment).
cmd_plan_capture() (bin/worklog, lines 339+). It parses the draft with
plan_capture.parse_tasks() — checkboxes under a ## Tasks heading only, with
the regex TASK_RE (bin/plan_capture.py) where a leading indent means
"subtask of the task above". It refuses to recapture a slug that already has a
plan (invariant 15.8), appends the epic create, then each task/subtask create,
and finally writes the plan doc with front matter linking every item ID.
Captured items get explicit kind:feature.
The guard is slug-scoped, not filename-scoped (v0.17.0, PR #198 + a
same-day follow-up; bin/worklog — cmd_plan_capture(), lines ~344–367).
The original code checked only os.path.exists(f"docs/plans/{date}-{slug}.md")
with date from time.gmtime() (UTC) — so it enforced "plans are never
rewritten" only when the UTC date happened to match the plan's actual filename.
A plan captured in the evening from a timezone behind UTC looks for
tomorrow's UTC-dated path, finds nothing, and silently writes a duplicate:
observed in a downstream repo on 2026-07-26 (a byte-identical duplicate plan
plus 23 duplicate work items, caught by hand and rolled back — nothing had
warned). The fix globbed docs/plans/*-{slug}.md across every date instead of
one fixed path. That fix's own bare glob then matched by raw string suffix, not
field boundary — a search for slug migration also matched an existing
2026-07-01-database-migration.md (real slug database-migration), a
false-positive refusal of a legitimately new, unrelated slug, caught in review
before PR #198 merged. The shipped guard anchors on the fixed date shape:
slug_re = re.compile(r"^\d{4}-\d{2}-\d{2}-%s\.md$" % re.escape(a.slug))
existing = sorted(p for p in glob.glob("docs/plans/*.md")
if slug_re.match(os.path.basename(p)))Two regression tests pin both failure modes
(test_plan_capture_refuses_a_slug_already_captured_on_another_date — a plan
dated 2020-01-01 must still be caught; test_plan_capture_does_not_false_positive_on_a_suffix_match
— a longer slug sharing a suffix must not block a new, shorter one),
tests/test_integration.py.
The whole flow is forced by hooks/exit-plan-capture.sh, which fires on
PostToolUse: ExitPlanMode and injects a non-optional instruction that now also
requires worklog ia-index after capture so the reader plane stays current.
render_roadmap.render() folds the log and emits markdown. Two non-obvious
choices: generated-at comes from the newest event's ULID timestamp, not the
wall clock — "wall clock here would fail every commit" — because
hooks/pre-commit regenerates and diffs. The default --viz deps,hierarchy in
cmd_roadmap_render must match render()'s default because the hook runs
the bare script and diffs against the file the CLI wrote. viz_mermaid.py caps
nodes at MAX_NODES = 40 and strips Mermaid-breaking characters.
hooks/pre-merge-commit is a one-line exec of the same script, because "git
runs THIS hook (not pre-commit) when a merge auto-commits."
worklog sync runs sync_dispatch.main() in-process (cmd_sync()). Order
inside Dispatcher.sync(): capabilities gate, push, pull, save state, report.
The gate runs first, every run: adapter capabilities output is parsed,
validated against the embedded schema mirror (CAPABILITIES_SCHEMA) by a 28-line
mini JSON Schema validator, plus one check the schema subset cannot express —
"{ulid}" must appear in caps["marker"]["template"]
(sync_dispatch.py — capabilities()).
Push scope (push_items()): open ∪ hash-dirty ∪ --keys. The canonical hash
is computed over the outbound shape — after type degradation — so "the degraded
echo coming back on pull still suppresses." A closing item whose hash is dirty
pushes an update with the final item shape before the close verb
(v0.12.1; TestCloseSyncsFields). On a successful create, external identity
enters the log the only way it can — worklog link as a subprocess (invariant
15.4).
Pull: NDJSON lines; echo suppression by comparing canonical_hash(line) to
last_pushed_hash; remote-only becomes worklog ingest with deterministic
ev = ulid.deterministic(system, key, rev, rev_ts); both-sides-changed records
a conflict per field and never overwrites. Field-diff runs over
INGEST_FIELDS (includes level/kind/milestone since v0.12.0). Labels on
pull remain future work.
Failure handling is an exit-code table (handle_exit()): 2 aborts; 3 pops
last_pushed_hash; 4 was already retried; 5 files per-field conflicts; anything
else is drift. No adapter at all is a mode: LOCAL_ONLY, exit 0.
Adapters are dumb on purpose. adapters/github/adapter maps verbs to gh
calls and embeds the marker; a test bans invariant tokens from every adapter
source.
Rich bodies (v0.13.0): worklog ticket-body <ulid> prints a projection with
summary, epic/plan/milestone context, and graph edges
(ia_graph.ticket_body(), lines 166+) for the issue-description skill to push.
compact.compact() (bin/compact.py), nightly on main via
.github/workflows/compact.yml. Sequence: refuse on uncommitted log changes;
watermark = max raw ev; short-circuit if todo is already all snapshots;
partition open vs closed where orphans count as open — "never drop data"; write
temp files; then gate on fold(new) == fold(old) plus trailing newline plus
every line parses. Only after that do two os.replace calls swap the files.
v0.13.0: snapshots write folded state verbatim so a closed orphan no longer fails verify by diverging from fold (item 01KY5HW7KS / #101).
_status_facts() is the deterministic half: a fold plus a raw-event pass; an
event is in-window when its ev ULID timestamp is. Daily windows open at the
last daily report's date; weekly is a fixed 7 days; timecards bucket per UTC day
and attach best-effort git commit subjects. The prose is the skill's job.
cmd_status --write stamps front matter with the window and the through
watermark and refuses to overwrite without --force (invariant 15.9).
Claude Code hooks (wired by plugin/hooks/hooks.json):
prompt-reminder.sh injects a one-line policy on every prompt;
stop-worklog-check.sh blocks ending a session where the tree changed but
.work/todo.jsonl did not, with a settle-and-recheck sleep;
session-doctor.sh reports missing policy blocks, unarmed hooks, or plugin
version skew, read-only.
CI (worklog.yml) re-runs the pre-commit script verbatim (as
WORKLOG_SKIP_BRANCH_GUARD=1 hooks/pre-commit since v0.15.0 — the checkout
runs with no commit in flight, and the branch guard only makes sense for an
actual git commit) — "A dev can --no-verify past the local hook; not
this" — then unit, integration, and a subprocess-aware coverage gate
(--fail-under=80). A new PR-scoped step (v0.15.0) walks
git rev-list --no-merges base..HEAD through hooks/commit-msg for every
non-merge commit on the PR, requiring fetch-depth: 0 on the checkout since
commit-msg's own MERGE_HEAD check has nothing to read post-hoc in a CI
clone. merge-when-green.sh polls gh pr checks and merges only on
all-green; empty check output counts as pending, and 24 failed polls exit 4
(ADR-0003).
Pre-commit also runs IA gates at WARN level (plan ia-content-model, migration 0002) — soon hard fail:
if [ -f bin/ia.py ] && [ -x bin/worklog ]; then
python3 bin/worklog ia-normalize --check >/dev/null 2>&1 || \
echo "worklog: WARNING (soon a hard gate) — doc metadata drift; run: worklog ia-normalize" >&2
python3 bin/worklog ia-inventory --check >/dev/null 2>&1 || \
echo "worklog: WARNING (soon a hard gate) — inventory stale/invalid; run: worklog ia-inventory" >&2
[ ! -f bin/ia_render.py ] || python3 bin/worklog ia-render --check >/dev/null 2>&1 || \
echo "worklog: WARNING (soon a hard gate) — rendered pages/manifest stale; run: worklog ia-render" >&2
# trace-check stays warn-level here forever; --strict runs at release time
[ ! -f bin/ia_graph.py ] || python3 bin/worklog trace-check >/dev/null 2>&1 || \
echo "worklog: WARNING — unlinked evidence; run: worklog trace-check" >&2
fi— hooks/pre-commit (IA block)
PYTHONDONTWRITEBYTECODE is set in the hook so it never dirties the worktree
with __pycache__ (item 01KY5P9V0C).
Identity. Every doc gets a stable wiki_key. Legacy keys are seeded
verbatim from .work/published.json so no URL or page name changes;
new docs derive keys by rule (ia.derive_canonical_key(),
ia.resolve_key()). CLI: worklog wiki-key <path>.
Normalize (ia.normalize(), cmd_ia_normalize lines 510–520):
def cmd_ia_normalize(a):
import ia
changes = ia.normalize(check=a.check)
for c in changes:
print(("needs: " if a.check else "wrote: ") + c)
if a.check and changes:
sys.exit(1)
...— bin/worklog — cmd_ia_normalize(), lines 510–520
Frozen docs get additive sidecars under docs/.index/<wiki_key>.yml;
sanctioned-live docs get in-place identity fields only. truth_state is
recomputed every run (DYNAMIC_FIELDS), never pinned from a stale sidecar.
Inventory (ia.build_inventory() / write_inventory()): pure function of
committed files → docs/.index/_inventory.json (one record per doc).
Render (ia_render.write_all()): Home, Sidebar, decisions/releases/status
indexes, truth banners, publish-manifest.json, aliases.json. Deterministic —
no wall clock — so --check can regenerate-and-diff.
Convenience wrapper:
def cmd_ia_index(a):
import ia, ia_render
for c in ia.normalize():
print("normalize: " + c)
ia.write_inventory()
print("inventory: " + ia.INVENTORY)
for path in ia_render.write_all():
print("wrote: " + path)— bin/worklog — cmd_ia_index(), lines 534–541
Graph (ia_graph.build_graph() / write_graph()): typed edges from
frontmatter, plan items, ADR references, and item sidecars. link-pr is an
overlay only — it does not append to the event log:
def link_pr(ulid_, pr=None, commit=None):
...— bin/ia_graph.py — link_pr(), lines 118+
trace_check(strict=False) lists closed items missing plan/ticket/PR links;
--strict exits 1 at release. ia-graph --seed proposes decides/implements
edges into gitignored .work/suggestions.jsonl (propose-only, never auto-edits
docs).
Schema split. Document types live in schema/doc.schema.json; graph/execution
entities (item today) live in schema/entity.schema.json. Both are mirrored in
ia.DOC_TYPES / ENTITY_TYPES / REQUIRED_* constants; TestSchemaSync pins
equivalence and asserts the two enums are disjoint so items never pretend to be
documents (#111).
docs/plans/2026-07-24-artifact-pages.md. Two graph nodes existed since the
Phase-4 traceability graph shipped — pr/<num> and release/<tag> stubs with
no page of their own. This release gives every work item, PR, and release a
generated wiki page, reusing graph edges instead of adding stored fields.
One shared traversal. ia_graph.build_adjacency(graph) builds the
forward/backward edge maps once per render pass; item_links(iid, fwd, back)
projects parent/children/PRs/release for one item from those maps:
def item_links(iid, fwd, back):
key = item_key(iid)
parent = next((to for typ, to in fwd.get(key, []) if typ == "belongs-to"),
None)
children = sorted(to for typ, to in back.get(key, []) if typ == "contains")
prs = sorted(to for typ, to in fwd.get(key, []) if typ == "lands-in")
release = next((to for typ, to in fwd.get(key, [])
if typ == "targets" and to.startswith("release/")), None)
return {"parent": parent, "children": children, "prs": prs,
"release": release}— bin/ia_graph.py — item_links(), build_adjacency()
Every one of the three new renderers calls this — "not four near-duplicates"
was a design decision in the plan (render_item_page() branches by level
instead of shipping separate story/epic/task renderers).
Ticket pages (ia_render.render_item_page()): title, level/kind/status
badge, a one-line summary derived at render time from the body's first
sentence (one_line_summary() — never cached, keeping the module's
byte-determinism), an upward ## Hierarchy walk to the root
(_upward_chain(), cycle-safe via a seen set), a downward ## Subtasks /
## Children list with a done/total progress rollup, ## Linked PRs, and
## Release. worklog ia-ticket <ULID> (cmd_ia_ticket, bin/worklog)
previews one page without a full render pass — builds the graph, calls
build_adjacency(), and writes render_item_page()'s output to stdout.
Release pages (render_release_page()): the Change Log is
milestone-tagged closed items plus their linked PRs, walked straight off the
graph — not a CHANGELOG.md parser. CHANGELOG.md stays human-authored
prose; the page's Change Log is a separate, mechanical, always-accurate list
(the plan is explicit that building a changelog-parsing engine would be a
fragile addition this project avoids). Also renders a ## Release Tree
(_release_tree() — a lighter nested list than viz_mermaid.hierarchy(),
which only covers open items, a different "what's left" use case), Related
PRs, Related Tickets, and Dependencies & Risks.
PR pages (render_pr_page()): linked tickets via reverse lands-in
edges, related releases/epics via item_links() on each linked ticket.
Changed-files and CI/review status render literally as "not tracked" — no
code in this repository calls gh pr view today, and the plan defers that
integration to a separate follow-up item (worklog pr-sync, filed as #138,
not built this release) rather than silently shipping a page that implies
data exists.
Manifest growth. build_manifest() gained a second loop keyed off the
tickets/, releases/, prs/ filename prefix in the rendered-pages dict —
a new entity type needs one new prefix branch, not a new loop. The
published-page manifest grew from 51 entries to 258.
docs/plans/2026-07-25-branch-discipline-hooks.md. Shipped after a real
incident: local main drifted 13 commits ahead of origin/main for hours
because every commit landed straight on main, while GitHub's nightly
compaction bot pushed its own commit directly to origin/main in parallel —
the divergence surfaced as a failed PR merge. Two new checks follow the
existing hook philosophy ("hooks enforce invariants, not hope").
Branch guard — a new block in hooks/pre-commit, inserted right after
fail() is defined, before the .work/*.jsonl checks:
if [ -z "${WORKLOG_SKIP_BRANCH_GUARD:-}" ] && \
[ -z "${WORKLOG_MERGE_COMMIT:-}" ] && \
[ ! -f "$(git rev-parse --git-path MERGE_HEAD)" ]; then
branch=$(git symbolic-ref --quiet --short HEAD || true)
case "$branch" in
main|master)
fail "commits go on a branch, not '$branch' (main/master is pull-only). Run: git checkout -b <branch-name>, then commit there."
;;
esac
fi— hooks/pre-commit (branch-guard block)
Detached HEAD is allowed (branch=""). Two independent exemptions cover
"this is a merge, not authored work": WORKLOG_MERGE_COMMIT — set by
hooks/pre-merge-commit before it execs into this script, because
MERGE_HEAD is empirically not yet on disk at the point git invokes
pre-merge-commit (it only appears between that hook running and
commit-msg firing) — and MERGE_HEAD itself, present by the time a merge
a hook rejected is resumed via a later plain git commit. A third variable,
WORKLOG_SKIP_BRANCH_GUARD, is set only by non-commit callers running the
script standalone with no commit in flight: plugin/scripts/doctor.sh's
health check, the CI invariants step, and two
tests/test_integration.py "CI gate passes" assertions — all of which would
otherwise false-positive on main. hooks/pre-merge-commit itself needed no
changes — it's a one-line exec of pre-commit, and MERGE_HEAD/
WORKLOG_MERGE_COMMIT already cover it.
hooks/commit-msg — new hook, requires every non-merge commit message to
reference a worklog item or ticket:
[ -f "$(git rev-parse --git-path MERGE_HEAD)" ] && exit 0
python3 - "$1" <<'PY' || fail "commit message must reference a worklog item (26-char ULID) or a ticket (#123) -- see: worklog show <id>. Merge commits are exempt."
import re, sys
msg = open(sys.argv[1], encoding="utf-8").read()
sys.exit(0 if (re.search(r'\b[0-9A-HJKMNP-TV-Z]{26}\b', msg) or
re.search(r'#\d+', msg)) else 1)
PY— hooks/commit-msg
The ULID pattern matches bin/ulid.py's Crockford base32 alphabet exactly.
No exemption beyond merge commits (via MERGE_HEAD, same signal as the
branch guard). Orthogonal check — applies on any branch, including feature
branches.
Wiring. hooks/commit-msg is mirrored byte-identical to
plugin/scripts/commit-msg; plugin/scripts/init.sh's hook-copy loop and CI
workflow template both gained it; uninstall.sh removes it symmetrically;
doctor.sh checks its existence/exec bit and now runs its own pre-commit
invocation with WORKLOG_SKIP_BRANCH_GUARD=1; tests/test_plugin.py's
CANON list includes it so TestCanonSync still guards the mirror. Both
hooks hard-fail immediately — no warn-only rollout period, unlike the IA
gates above.
Release skill. plugin/skills/release/SKILL.md §3's "direct-commit
repos: commit on the default branch" mode is removed — dead once the branch
guard ships everywhere; the skill now describes branch+PR landing only.
Test fixture fallout. ~23 commit_all() calls in
tests/test_integration.py and 3 raw git commit calls in
tests/test_plugin.py mostly committed on main with no-reference
messages — pure fixture plumbing. Pre-existing baseline commits got
no_verify=True; commits meant to represent real authored work moved onto a
branch (Sandbox.branch()) and picked up an item-ULID in the message,
following the file's existing precedent for "not what this test is about"
commits.
docs/plans/2026-07-25-wiki-driven-integration-guides.md. Eleven systems this
repo talks about but mostly doesn't ship real adapter code for — four SDD
tools it composes with (Superpowers, GSD, SpecKit, OpenSpec) and seven
ticket/wiki systems where only adapters/github/adapter is real (Jira,
Confluence, GitHub, GitLab, Azure DevOps, AWS CodeCatalyst, Google Cloud
DevOps) — each get a dedicated setup guide. The design choice worth noting:
this ships zero new Python. WebFetch (already available to any Claude
Code agent) covers "fetch the live wiki page at runtime"; worklog wiki-add
(pre-existing) covers "get an arbitrary file into the wiki-publish pipeline."
The entire feature is one new skill plus markdown content.
plugin/skills/integration-guide/SKILL.md (mirrored to
.claude/skills/integration-guide/SKILL.md) is pure prose, six numbered
steps:
-
Match the name to a canonical key via a fixed alias table in the skill
body — no network call needed to resolve "ADO" or "Azure Boards" to
azuredevops. -
Try the live wiki page first. Build the URL from
wiki.root_urlin.work/config.ymlplus/Integration-<Name>;WebFetchit. -
Verify before trusting. A GitHub wiki does not 404 a missing page
slug — it silently redirects to Home with a normal 200. The skill checks
the response for a
## Recommended workflowheading before treating it as a hit; anything else (network error, 404, wrong-page redirect) is handled identically to a fetch failure. -
Fall back to the local copy,
docs/integrations/fallback-<key>.md, saying so explicitly ("using the bundled local copy, which may lag the published page"). -
Soft version-staleness check — only for systems with a real,
probeable CLI (
gh --version,glab --version,az --version,aws --version); the four SDD tools are Claude Code skills with nothing to introspect, and the skill says so rather than fabricating a check. -
Compose, don't reinvent — for Jira/Confluence, check for the global
jira/confluenceskill or an Atlassian MCP server before any raw REST/CLI call; that skill already owns auth, pagination, and markup conversion.
Content: docs/integrations/README.md (index) plus eleven
fallback-<key>.md files, one fixed ten-section template each (When to
use, One-command setup, Adapter configuration, Recommended workflow, Mapping
events, Pulling changes, Rendering support, Example links, Gotchas &
troubleshooting, Last updated) so the skill's lookup logic is uniform
across all eleven. Two placements are load-bearing and appear in exactly one
page each: the Jira/Confluence skill-reuse paragraph (§Recommended workflow,
those two files only), and the Confluence diagram-to-image conversion note
(§Rendering support, fallback-confluence.md only — Confluence storage
format doesn't render Mermaid/PlantUML fences directly).
Publishing: bin/worklog wiki-add docs/integrations/fallback-<key>.md --key integrations/<key> --title "Integration-<Name>" registers the file in
.work/published.json; the ordinary wiki-publish skill's existing
hash-compare skip logic carries it from there — no new publish path was
built or needed (wiki-publish/SKILL.md §4). Confirmed: git diff --stat v0.15.1..v0.16.0 touches no file under bin/, hooks/, or tests/ for
this feature — only docs/integrations/*, the two SKILL.md mirrors, and
the version/changelog/roadmap bookkeeping every release touches.
| # | Invariant | Enforced at | Broken means |
|---|---|---|---|
| 1 | Every .jsonl write ends in \n
|
append() self-heal; hooks/pre-commit; CI |
next append fuses two events into one corrupt line; both lost |
| 2 | Only worklog writes the log; only compact.py rewrites it |
policy + CLAUDE.md; sync_dispatch shells into worklog
|
hand edits corrupt merges; invariants unauditable |
| 3 | Fold order is ev, never file position or ts
|
dedupe_and_sort() |
union-merged logs fold differently per machine |
| 4 | Ingested events carry deterministic ev and the remote's ts
|
ulid.deterministic(); cmd_ingest()
|
duplicate ingests silently revert local edits |
| 5 | Push idempotency: marker worklog:<ulid> + canonical-hash skip |
push_items(); marker template gate |
retried pushes file duplicate tickets |
| 6 | Canonical hash = exactly HASH_FIELDS, one implementation |
canonical.py ("nothing else may reimplement it") |
echo suppression breaks for every existing clone |
| 7 | Compaction only lands if fold(new) == fold(old)
|
_verify(); temp files + os.replace
|
state loss — "the worst failure mode in this system" |
| 8 |
close reads status from set
|
fold() |
cancelled work reports as shipped |
| 9 | Generated roadmap always matches the log | pre-commit + pre-merge-commit diff; deterministic timestamps | roadmap silently lies; hand edits stick |
| 10 | Frozen artifacts are never rewritten | plan-capture/roadmap-snapshot/status existence refusals; ADR mark_superseded(); IA sidecars for frozen docs |
history that people acted on gets rewritten |
| 11 | Adapters contain no invariant logic |
test_adapter_contract.py banned-token scan |
invariants fork per platform and drift |
| 12 | Epics are feature/ops only; milestone lives on leaves |
check_taxonomy(); pre-commit taxonomy scan; fold stays lenient |
taxonomy queries give wrong answers |
| 13 | Merges happen only on all-green gates | merge-when-green.sh |
broken main, agent-speed |
| 14 | IA index artifacts are pure functions of committed files | no wall clock in inventory/render/graph writers; freshness --check
|
regenerate-and-diff gates become flaky |
| 15 | Doc types and entity types are disjoint | TestSchemaSync.test_doc_and_entity_types_are_disjoint |
inventory/graph validation confuses items with pages |
| 16 | Artifact-page hierarchy/PR/release links are derived at render time, never stored on the item or a sidecar |
ia_graph.item_links() reads graph["edges"] only |
a cached copy would drift from the graph, the exact second-source-of-truth problem sidecars were built to avoid |
| 17 | Every commit on main/master is a merge (MERGE_HEAD/WORKLOG_MERGE_COMMIT), never authored directly (v0.15.0) |
hooks/pre-commit branch guard |
local main and origin/main diverge silently until a failed merge surfaces it — the actual incident this hook exists for |
| 18 | Every non-merge commit message references a worklog item ULID or a ticket number (v0.15.0) | hooks/commit-msg |
work is untraceable to a plan or ticket after the fact |
tests/test_fold.py — test_cancelled_stays_cancelled(). Rule proved:
close takes status from set. Regression caught: a fold that hardcodes done
— abandoned work reporting as shipped.
tests/test_ulid.py — TestTheBugThisPrevents. Two devs poll the same remote
change; with deterministic ev, dedupe collapses them. The companion test
passes while documenting the failure mode with random evs — Rick's edit is
gone, nothing errors. Exists "because this design keeps getting proposed."
tests/test_dispatch.py — test_push_twice_same_ulid_is_one_ticket(). Rule
proved: canonical-hash skip + marker idempotency. Sibling
test_retry_after_transient_does_not_duplicate injects exit-4 with _fail_next.
tests/test_adapter_contract.py — test_adapters_contain_no_invariant_logic().
Scans every adapters/*/adapter for banned tokens. Automatically covers new
adapters the day they appear.
tests/test_integration.py — test_a_fused_line_costs_exactly_its_own_events().
Corruption is contained and detected at the merge boundary.
tests/test_compact.py — test_reopen_after_compact_restores_pre_close_fields().
Folding todo + done by ev makes reopen work across the physical file split.
tests/test_dispatch.py — test_pull_ingests_remote_taxonomy_change() (v0.12.0).
Remote taxonomy edits pull instead of silently dropping.
tests/test_dispatch.py — TestCloseSyncsFields (v0.12.1). Reclassify then
close; local kind survives the round-trip; pull is an echo, not a remote edit
(worklog 01KY129S, GitHub #76).
tests/test_ingest.py — TestReopen (v0.12.0). reopen clears resolution;
update --status on closed is refused; reopen of open is refused.
tests/test_ia.py — TestSchemaSync (v0.13.0).
def test_doc_schema_json_matches_ia_constants(self):
...
self.assertEqual(schema["required"], list(ia.REQUIRED_ALL))
self.assertEqual(props["doc_type"]["enum"], list(ia.DOC_TYPES))
...
def test_doc_and_entity_types_are_disjoint(self):
self.assertEqual(set(ia.DOC_TYPES) & set(ia.ENTITY_TYPES), set())Rule proved: embedded IA constants cannot silently diverge from
schema/doc.schema.json / entity.schema.json before Phase 5 hard-fail.
tests/test_ia.py — TestNormalize.test_normalize_backfills_then_noop. First
run writes sidecars/frontmatter; second run is a no-op. Rule proved: normalize is
idempotent and additive.
tests/test_ia.py — TestGraph.test_link_pr_is_overlay_only. link-pr
mutates the item sidecar, not the event log. Rule proved: PR edges do not violate
invariant 15.4.
tests/test_ia.py — TestGraph.test_trace_check_warn_and_strict. Default is
non-zero gaps without process failure; --strict exits 1.
tests/test_ia.py — TestGraph.test_seed_edges_propose_only_and_deduped. Seed
writes suggestions only; never edits docs; dedupes re-proposals.
tests/test_ia.py — TestArtifactPages.test_ticket_page_hierarchy_and_progress
(v0.14.0). Builds a real epic→task→subtask chain, closes the subtask, and
asserts the epic page's ## Children section, the task page's ## Hierarchy
and ## Subtasks sections, and the Progress: 1/1 done rollup — the
strongest proof that item_links() + _upward_chain() compose correctly
across three levels.
tests/test_ia.py — TestArtifactPages.test_release_page_change_log_is_graph_derived.
Closes an item tagged with a milestone, renders, and asserts the release
page's Change Log contains the item's title — proves the Change Log is
graph-derived, not a CHANGELOG.md parse.
tests/test_ia.py — TestArtifactPages.test_manifest_grows_with_items_releases_prs.
Closes an item, links a PR, renders, and asserts item/, release/, and
pr/ wiki_keys all appear in the manifest with the right cardinality — proves
build_manifest()'s new prefix-keyed loop actually fires for every entity
type, not just documents.
tests/test_integration.py — TestBranchGuard (v0.15.0). Three cases:
committing on main with no branch is rejected with a "pull-only" message;
committing on a feature branch succeeds; merging a feature branch onto
main — the exact incident scenario — is allowed even though it lands a
commit directly on main. Rule proved: the guard blocks authored commits on
main, not merges.
tests/test_integration.py — TestCommitMsgReference (v0.15.0). A commit
message with neither a ULID nor a #123 reference is rejected; one with
either passes; a merge commit's message is exempt regardless of content.
tests/test_integration.py — TestPlanCapturePR.test_plan_capture_refuses_a_slug_already_captured_on_another_date
(v0.17.0). Seeds docs/plans/2020-01-01-demo.md, then captures a fresh draft
with slug demo; asserts the refusal names the 2020 file and cites invariant
15.8, and that nothing gets written under today's date either. Rule proved:
invariant 15.8 is slug-scoped, not filename-scoped — the exact bug that
produced a real duplicate-plan incident in a downstream repo (§2.3, §6).
tests/test_integration.py — TestPlanCapturePR.test_plan_capture_does_not_false_positive_on_a_suffix_match
(v0.17.0). Seeds docs/plans/2026-07-01-database-migration.md, then captures
a new draft with the unrelated, shorter slug migration; asserts the capture
succeeds. Rule proved: the guard's regex is anchored on the YYYY-MM-DD-
date field, so a bare suffix match (-migration.md) on a longer, different
slug does not false-positive-refuse a legitimately new one — the correctness
bug the first fix's naive glob introduced, caught before PR #198 merged.
Five things to internalize:
- State is derived, never stored. If
worklog listlooks wrong, the question is "what events exist?" (worklog fold, or read the JSONL), never "where is the state file?" -
evorder is the only order. File position andtsare noise. - There is exactly one writer (
append()), one meaning-maker (fold()), one rewriter (compact.py), one hash (canonical.py). Adding a second of any of these is the design failure the tests hunt. - Generated vs frozen:
docs/roadmap.mdanddocs/.index/*are regenerated and diffed; plans, snapshots, status reports, and ADR bodies are written once (IA metadata for frozen docs lives in sidecars, not in the body). - The dispatcher enforces; adapters translate; skills orchestrate; the IA plane navigates.
Where to start debugging: python3 bin/fold.py prints derived state with
warnings for corrupt lines and orphans. worklog sync --dry-run prints
decisions without side effects. worklog adapter check validates a contract.
bash hooks/pre-commit runs every local gate manually. worklog ia-index and
worklog trace-check diagnose reader-plane / evidence gaps.
Where common changes go: new CLI behavior → bin/worklog (subcommand + a
test suite); roadmap presentation → render_roadmap.py/viz_mermaid.py (keep
byte-determinism — no wall clocks); a new tracker → copy
adapters/github/adapter, keep it dumb, then worklog adapter check; policy →
CLAUDE.md prose backed by a hook if it must always hold; doc identity /
navigation → ia.py / ia_render.py / ia_graph.py + test_ia.py; a new
artifact-page entity type (v0.14.0 pattern) → add a render_<x>_page() in
ia_render.py that consumes ia_graph.item_links(), wire it into
render_all()'s loop, and add one prefix branch to build_manifest(); a
twelfth integration system (v0.16.0 pattern) → one row in
integration-guide/SKILL.md's alias table, one
docs/integrations/fallback-<key>.md, one worklog wiki-add call — no
bin/ code.
Risky files: bin/canonical.py (any change churns every clone's hashes —
the file says "Don't."); bin/fold.py (every command's notion of truth);
bin/compact.py (the only code that can lose state); append() in
bin/worklog (the atomicity/newline dance); sync_dispatch.CAPABILITIES_SCHEMA
and ia.REQUIRED_* / DOC_TYPES (must stay identical to schema/* — tests
diff them).
Never break: invariants table in §3 — especially trailing newline,
ev-ordering, deterministic ingest, marker idempotency, fold-equality in
compaction, and frozen-doc immutability (use sidecars).
Confirmed facts unless labeled otherwise.
Shipped in v0.17.0 (were open at v0.16.1):
-
bin/worklog plan-captureinvariant-15.8 guard fixed twice in the same release cycle (PR #198 + same-day follow-up): first from filename-scoped (missed a UTC/local date-boundary duplicate) to slug-scoped via a bare*-{slug}.mdglob, then from that glob (a suffix match, not a field-boundary match — false-refused an unrelated longer slug) to aYYYY-MM-DD--anchored regex. See §2.3. - Generated wiki Home gained
[[Code-Walkthrough]]next to[[Design-Doc]](ia_render.render_home()) — a walkthrough page was previously unreachable from Home once published. -
docs/graph-engineering.mdadded: documentation only, no code path, no gap surface of its own.
New in v0.17.0 (found while fixing the plan-capture guard, not yet fixed — filed): none found this release — both defects found were caught and fixed within the same PR cycle before merge (see §2.3), not left open.
Shipped in v0.16.1 (were open at v0.16.0):
-
bin/sync_dispatch.pyclosed-item sync path fixed: forcing a never-pushed closed item into scope via--keysraisedKeyError: 'key'; it now creates-then-links-then-closes, mirroring the open-item branch.
New in v0.16.1 (found while shipping the sync_dispatch fix, not yet fixed — filed): none found and filed.
Shipped in v0.16.0 (were open at v0.15.1):
- Wiki-driven integration guides:
integration-guideskill + 11docs/integrations/fallback-*.mdpages + index. Zero newbin/code — see §2.12.
New in v0.16.0 (found while shipping integration guides, not yet fixed — filed): none found this release — the feature is content plus one prose-only skill with no code path to regress.
README repo-layout drift persists (Confirmed, recurring):
README.md's plugin/ row still reads "v0.16.0" against VERSION = "0.17.0" in bin/worklog — the same one-release-stale cosmetic gap noted at
v0.14.0, v0.15.0, and v0.16.0, not part of release.sync_docs and not fixed by
this regeneration (this document is generated, not hand-patched).
Closed in prior releases and still closed at v0.15.0: dispatcher
INGEST_FIELDS carries taxonomy; worklog reopen exists; conflict_policy is
report only; dirty-close pushes final shape before close; TestResolve
exercises the resolve CLI.
Shipped in v0.15.0 (were gaps or plans at v0.14.0):
- Branch discipline:
hooks/pre-commitbranch guard (refuses authored commits onmain/master) and newhooks/commit-msg(requires a ULID or ticket reference), both hard-fail immediately with no warn period. Wired intoinit.sh/uninstall.sh/doctor.sh/tests/test_plugin.pyCANON and a new CI PR-scoped commit-message check. -
plugin/skills/release/SKILL.md's dead "direct-commit repos" mode removed.
New in v0.15.0 (found while shipping branch discipline, not yet fixed — filed):
None found and filed this release — the plan's own verification checklist
(full suite green, TestCanonSync, manual incident-scenario replay, worklog doctor still healthy on main) was walked without surfacing new drift.
Shipped in v0.14.0 (were gaps or plans at v0.13.0):
- Artifact pages: ticket/release/PR pages generated from existing graph edges
(
render_item_page(),render_release_page(),render_pr_page()),ia_graph.build_adjacency()/item_links(),worklog ia-ticketpreview. -
build_manifest()grows a second, prefix-keyed loop for items/releases/PRs; published-page manifest 51 → 258 entries.
New in v0.14.0 (found while building artifact pages, not yet fixed — filed):
-
close/updatedon't resolve item-id prefixes (#123).reopendoes prefix matching on the item id;closeandupdatedo not — a short but valid prefix silently creates a new orphan item rather than resolving to the intended one. Confirmed as filed drift, not fixed in this release. -
banner()mislabels frozen "current"-titled docs as status reports (#137), regardless of actualdoc_type. Verified live on 12 of 14 published plan pages. Confirmed as filed drift, not fixed in this release. -
README.md repo-layout table says "v0.13.0" on the
plugin/row — one release stale (bin/worklog VERSIONis"0.14.0", locked toplugin/.claude-plugin/plugin.jsonbytests/test_plugin.py, but README prose isn't part of that lockstep test). Cosmetic; not filed as a ticket, noted here as doc drift. -
docs/user_guide/cli-reference.md's new "Information architecture (IA) commands" section documentswiki-key,ia-normalize,ia-inventory,ia-render/ia-manifest,ia-index,ia-graph,link-pr,ticket-body,trace-check— but not the newia-ticketsubcommand. Confirmed by grep; the CLI reference has not caught up withbin/worklogfor this one command. -
worklog sync --pullcannot bootstrap a cursor-less pull (item 01KYAGZ8, filed but not yet worked): the adapter'spullverb requires--since, which a first-ever pull has none of. Not exercised by this walkthrough's citations; flagged from the roadmap snapshot for release readers.
Still open / drift, carried from v0.13.0 and earlier:
-
Spec §10.5 sync surface ≠ shipped CLI. Spec documents
--scope active|all,--report,--apply; CLI ships--dry-run,--keys,--push-only,--pull-only. Doc drift, not a bug. -
.work/config.ymlcomments still say "no adapter binary" under ticketing/wiki blocks whileadapters/and the dispatcher ship. Harmless (skill path still works) but a 1.4-era story for config-only readers. - Spec §11's three-phase orchestration (changeset.json, results/) is not in code. Shipped dispatcher is single-process push/pull. Assumption: still aspirational for parallel-subagent sync.
-
estimateand related optional fields (spec §5.4 / #108) have no CLI surface yet — configurable field model is open work. -
Labels don't pull — marked future work in
pull(). - Remote-origin tickets are reported, never created locally — deliberate read-safety.
-
Duplicated mini-validator (dispatcher,
adr.py, contract tests) and duplicated IA schema constants (ia.pyvsschema/*.json) — deliberate "bin-only install"; pinned by tests; fourth/diverge copies should extract or fail CI. -
IA gates warn-only until Phase 5 (#98);
trace-checkstays warn at commit forever (strict at release). Residual risk: ignored warnings allow metadata drift to merge. -
Phase 5 / platform render adapters /
/worklog:find+ glossary not shipped. -
UI work was moved to
wiki_ticket_sdd_uiand cancelled here — do not look for UI code in this repo. -
Live PR metadata (files changed, review/CI status) deliberately
deferred to a filed follow-up,
worklog pr-sync(#138) — nogh pr viewcall exists anywhere in this repo; PR pages render "not tracked" by design, not by oversight.
Final check against the code: every flow above was walked at commit
a15a7bb15cadc1d8d41648d29bc546b56c258623 (tag v0.17.0 on main); all
citations are to that tree. Dated freeze pairs for this release pin
git_hash to this same commit.
- 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