-
Notifications
You must be signed in to change notification settings - Fork 0
Code Walkthrough 2026 07 24_v0.14.0 release
Snapshot of release v0.14.0 (2026-07-24) — 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 (1189 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 + four Claude Code hooks. |
plugin/ |
Claude Code packaging; plugin/scripts/ mirrors bin/ + hooks/. |
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). |
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.14.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 overwrite an existing plan path
(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 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 — "A dev can
--no-verify past the local hook; not this" — then unit, integration, and a
subprocess-aware coverage gate (--fail-under=80). 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.
| # | 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 |
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.
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().
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.
Closed in prior releases and still closed at v0.14.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.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
313b7cd8d903a2aed1115df37bd2002abdd1839d (tag v0.14.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