-
Notifications
You must be signed in to change notification settings - Fork 0
Code Walkthrough 2026 07 28_v0.17.1 release
Snapshot of release v0.17.1 (2026-07-28) — 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.
Every command that names an existing item now goes through one lookup.
Before v0.17.1, only reopen, resolve, and show folded the log to find the
item; close, update, and link called _require_item() (a non-empty-string
check, nothing more) and then wrote the event under whatever string the caller
passed. Hand any of them the 8-character prefix that worklog show and
worklog list themselves print, and the event landed under that short id — which
folds into a brand-new phantom item, leaving the real one untouched:
def _resolve(item):
"""Resolve a full ULID or an unambiguous prefix to the real item
(worklog 01KYA99TVC). ..."""
_require_item(item)
r = fold([LOG, ".work/done.jsonl"])
match = [i for k, i in r.items.items() if k.startswith(item)]
if not match:
sys.exit(f"worklog: no item matching {item}")
if len(match) > 1:
ids = ", ".join(sorted(i["id"] for i in match))
sys.exit(f"worklog: {item} is ambiguous — matches {ids}")
return match[0]— bin/worklog — _resolve(), lines 115–130
What it receives: a full ULID or any prefix of one. What it returns: the folded
item dict (so callers get item["id"], item["status"], item["_conflicts"]
for free). What can fail: empty string, no match, or ambiguous — and that
last case is new behavior, not just refactoring. reopen/resolve/show
previously took match[0] when two ULIDs shared a prefix, an arbitrary pick;
_resolve() exits naming both candidates instead.
Callers, all of which now write under item["id"] rather than the raw argument:
cmd_update() (line 133), cmd_close() (156), cmd_reopen() (165),
cmd_link() (176), cmd_resolve() (242), cmd_show() (298). Two of those were
worse than a phantom item. cmd_update() used to do its current-state lookup as
fold(...).items.get(a.item, {}) — for a prefix that returns {}, so
check_taxonomy(cur.get("level"), ...) ran against level=None (an epic could
be reclassified kind:bug, which taxonomy §2.2 forbids) and the
cur.get("status") in CLOSED_STATUSES guard never fired (so update --status
silently bypassed the "use reopen" refusal). cmd_link()'s failure mode was
the loudest downstream: a link event under a prefix minted an orphan item
carrying a real external key, which bin/sync_dispatch.py would then push to
the tracker.
This is the shape the lazy fix takes: reopen already had the prefix match, so
the fix was to lift those six lines into a shared helper and delete three
copies, not to add a guard to each caller. git diff --numstat v0.17.0..v0.17.1 -- bin/worklog is +30/−27 lines, one add and one delete of which is the
VERSION bump.
The regression suite is tests/test_resolve.py (new in v0.17.1, 143 lines,
sandbox-subprocess style copied from test_taxonomy.py): each of
close/update/link by prefix asserts len(items) == 1 with the message "prefix
close minted a phantom item"; the two bypassed update guards get a test each;
test_unknown_id_fails_loudly_and_writes_nothing() reads the log before and
after and asserts byte equality; test_ambiguous_prefix_names_the_candidates()
derives the shared prefix with os.path.commonprefix() on two real ULIDs.
The same release widened one unrelated check in three copies:
hooks/session-doctor.sh (lines 15–18), plugin/hooks/scripts/session-doctor.sh
(same lines, mirrored), and plugin/scripts/doctor.sh (63–68) compared
git config core.hooksPath against the literal string hooks and failed
anything else. worklog:init writes that relative form, but a git worktree
resolves a relative hooksPath against the wrong CWD, so a worktree checkout
needs the absolute path — and doctor called a correctly-wired repo broken. All
three now accept either form, comparing
cd "$hookspath" && pwd -P against $(pwd -P)/hooks, and the failure message
quotes the offending value instead of asserting a single expected string.
| # | 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_resolve.py — ResolveTest (v0.17.1). Rule proved: an id prefix
names the existing item, never a new one. Regression caught: close/update/
link writing events under the raw caller string, so the short id the CLI itself
prints minted a phantom orphan — and, for update, made the taxonomy and
closed-item guards run against an empty dict. Each prefix test asserts
len(items) == 1, which is what a phantom breaks;
test_ambiguous_prefix_names_the_candidates() pins the new refusal where the old
code silently took match[0] (worklog 01KYA99TVC).
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.1 (were open at v0.17.0):
-
ID-prefix resolution on
close/update/link(#123, open since v0.14.0): the shared_resolve()helper now backs all six commands that name an existing item, so a prefix resolves to the real item instead of minting a phantom orphan, andupdate's taxonomy / closed-item guards run against real state. Ambiguous prefixes are refused by name rather than resolved tomatch[0]. Newtests/test_resolve.py. See §2.13. -
core.hooksPathcheck accepts an absolute path to this repo'shooks/inhooks/session-doctor.sh,plugin/hooks/scripts/session-doctor.sh, andplugin/scripts/doctor.sh— the form a git worktree needs, previously reported as a broken install. See §2.13.
New in v0.17.1 (found while fixing prefix resolution, not yet fixed — filed): none found and filed this release.
Still open from v0.14.0: banner() mislabels frozen "current"-titled docs
as status reports (#137) — untouched by this release.
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.1" in bin/worklog — now two releases stale, the same cosmetic gap
noted at v0.14.0, v0.15.0, v0.16.0, and v0.17.0. readme is in
release.sync_docs, but that target is regenerated by a separate agent and this
document is generated, not hand-patched, so the row is reported here as observed
at the tagged commit.
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
b71e5e423a60f5c95b43f380165af94b2cd07010 (tag v0.17.1 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