# swarph timeline — the OKF temporal on-ramp — Implementation Plan
> For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: Ship swarph timeline range/around/since — a deterministic, $0 temporal-lookup verb over the raw git-backed ~/swarph-timeline/TIMELINE.md, plus a reusable OKF link-grammar parser and an MCP tool — the temporal on-ramp (sub-project A) of the OKF traversal brain.
Architecture: A new timeline.py command reads and parses TIMELINE.md (one entry per line), filters entries by their embedded ISO timestamp, and prints them (human or OKF---json). Edges come from a new shared okf_links.py parser (reused later by swarph memory and the swarph brain walker). No network, no server — pure file read; complements the gbrain-backed swarph memory. Follows the #41 swarph memory command/MCP/dispatch patterns.
Tech Stack: Python 3, stdlib-only (argparse, re, json, datetime, os) — public PyPI constraint. Tests via venv/bin/python -m pytest.
Global Constraints
- Stdlib-only. No new third-party dependency (swarph-cli is public on PyPI).
- Deterministic + $0. Pure file parse + date filter; NO LLM, NO network, NO server. Filter by each entry's embedded
@(the swarph-highlight line format), NOT the git commit date. - Fail-safe, read-only. A missing/unreadable
TIMELINE.md→ a stderr note + non-zero exit at the CLI (never a traceback); the MCP tool NEVER raises (returns[]). Mirrorswarph memory's fail-safe shape. - OKF edge model (from the spec):
--jsonemits, per matched entry,{"node": {"id", "hemisphere":"time", "ts"}, "edges":[{"type":"link","to","to_hemisphere":"knowledge","direction":"out"}...], "cell", "text"}. The node's canonical id is its ISO timestamp. - Link grammar (droplet-pinned — the reusable parser MUST honor exactly):
[[slug]]→slug;[[slug|alias]]→slug;[[slug#heading]]→slug;![[embed]]→embed(transclusion = an edge);[text](path.md)→path.md. Order-preserving dedupe. TIMELINE.mdline format (verbatim):-where· · | [ · → [[mem]]] is%Y-%m-%dT%H:%MZ(e.g.2026-07-15T08:51Z), separator is·(U+00B7 with spaces). The· → [[mem]]memory pointer is optional.- Inert-safe. Additive verb only; NO hook or existing-command behavior changes. Publish is commander-gated — this plan ends at green + PR (NOT published). New public verb → PR-not-merged, left for review (merge-on-green is standing auth, but not for a fresh public surface).
- Version: bump
swarph_cli.__version__0.30.0 → 0.31.0 inpyproject.toml+src/swarph_cli/__init__.py+ BOTH pin tests (tests/test_brain_ask_command.py,tests/test_watchdog.py). - Staging discipline: stage only the files each task names; never
git add -A; do not stage.codegraph/or stray docs. - Create
src/swarph_cli/commands/okf_links.py— the shared OKF link-grammar parser (reusable "leave a tool"). - Create
tests/test_okf_links.py— grammar unit tests. - Create
src/swarph_cli/commands/timeline.py— theswarph timelineverb. - Create
tests/test_timeline_command.py— timeline unit tests. - Modify
src/swarph_cli/main.py— register"timeline"in_VERB_HANDLERS. - Modify
src/swarph_cli/commands/mcp_server.py— addswarph_timeline_navigate+_timeline_navigate. - Modify
README.md—### swarph timelinesection. - Modify
pyproject.toml,src/swarph_cli/__init__.py,tests/test_brain_ask_command.py,tests/test_watchdog.py— version bump 0.31.0. - Create:
src/swarph_cli/commands/okf_links.py - Test:
tests/test_okf_links.py - Produces:
parse_okf_links(text: str) -> list[str]— target slugs/paths in document order, deduped. - [ ] Step 1: Write the failing test
- [ ] Step 2: Run test to verify it fails
- [ ] Step 3: Write minimal implementation
- [ ] Step 4: Run test to verify it passes
- [ ] Step 5: Commit
- Create:
src/swarph_cli/commands/timeline.py - Test:
tests/test_timeline_command.py - Consumes:
parse_okf_links(Task 1). - Produces:
Entry(namedtuplets: datetime, cell: str, text: str, links: list[str]);load_entries(path: str) -> list[Entry];_parse_arg_date(s: str) -> datetime;run_timeline(argv: list) -> int. - [ ] Step 1: Write the failing test
- [ ] Step 2: Run test to verify it fails
- [ ] Step 3: Write minimal implementation
- [ ] Step 4: Run test to verify it passes
- [ ] Step 5: Commit
- Modify:
src/swarph_cli/commands/timeline.py(already contains_as_okf+--jsonfrom Task 2 — this task adds its dedicated test) - Test:
tests/test_timeline_command.py - Consumes:
run_timeline,_as_okf(Task 2). - [ ] Step 1: Write the failing test
- [ ] Step 2: Run test to verify it fails
- [ ] Step 3: (only if the test failed) align
_as_okfto the asserted schema - [ ] Step 4: Run test to verify it passes
- [ ] Step 5: Commit
- Modify:
src/swarph_cli/main.py - Test:
tests/test_timeline_command.py - Consumes:
run_timeline(Task 2). - [ ] Step 1: Write the failing test
- [ ] Step 2: Run test to verify it fails
- [ ] Step 3: Add the dispatch entry
- [ ] Step 4: Run test + smoke
- [ ] Step 5: Commit
- Modify:
src/swarph_cli/commands/mcp_server.py - Test:
tests/test_timeline_command.py - Consumes:
timeline.load_entries,timeline._timeline_path,timeline._as_okf,timeline._bounds(Task 2). - Produces:
_timeline_navigate(op, start=None, end=None, date=None, window="3d") -> list; registeredswarph_timeline_navigateMCP tool. - [ ] Step 1: Write the failing test
- [ ] Step 2: Run test to verify it fails
- [ ] Step 3: Add the wrapper + register the tool
- [ ] Step 4: Run test to verify it passes
- [ ] Step 5: Commit
- Modify:
README.md,pyproject.toml,src/swarph_cli/__init__.py,tests/test_brain_ask_command.py,tests/test_watchdog.py - [ ] Step 1: Bump the pin tests first (write the failing assertion)
- [ ] Step 2: Run to verify they fail
- [ ] Step 3: Bump the version constants
- [ ] Step 4: Run to verify they pass
- [ ] Step 5: Add the README section
- [ ] Step 6: Run the FULL suite (no regressions)
- [ ] Step 7: Commit
- Shared OKF link-grammar parser, pinned grammar → Task 1 ✓ (all five forms + dedupe).
swarph timeline range/around/sinceover raw git TIMELINE.md, embedded-ts filter, fail-safe → Task 2 ✓.- OKF
--jsonnode/edge schema (node=ts/hemisphere=time; edges=[[links]] out→knowledge; cell) → Task 3 ✓. swarph_timeline_navigateMCP tool, fail-safe → Task 5 ✓.- CLI dispatch registration → Task 4 ✓.
- README + version bump → Task 6 ✓.
- Out of scope (correctly absent): #42 gbrain graph, the
swarph brainwalker,--rootfile-native walk, the graph-summary onboarding output (those belong to sub-projects B/C).
---
File Structure
---
Task 1: The shared OKF link-grammar parser
Files:
Interfaces:
`python
# tests/test_okf_links.py
from swarph_cli.commands.okf_links import parse_okf_links
def test_grammar_table(): assert parse_okf_links("[[a]]") == ["a"] assert parse_okf_links("[[a|Alias A]]") == ["a"] # alias dropped assert parse_okf_links("[[a#Heading]]") == ["a"] # heading dropped assert parse_okf_links("![[embed]]") == ["embed"] # transclusion is an edge assert parse_okf_links("see [txt](notes/b.md)") == ["notes/b.md"] # md link # combined, order-preserving dedupe, markdown non-.md links ignored assert parse_okf_links("[[a]] x [[a|z]] y [[c#h]] z [q](http://x)") == ["a", "c"]
def test_empty_and_none():
assert parse_okf_links("") == []
assert parse_okf_links(None) == []
`
Run: venv/bin/python -m pytest tests/test_okf_links.py -v
Expected: FAIL — ModuleNotFoundError: swarph_cli.commands.okf_links.
`python
# src/swarph_cli/commands/okf_links.py
"""Shared OKF link-grammar parser — the one true [[link]]/markdown-link extractor.
Reused across the OKF traversal brain: swarph timeline (this sub-project),
and later swarph memory + the swarph brain walker. The grammar is PINNED
(design review, droplet 2026-07-15) so the same edge is read identically
everywhere — a traversal brain is only as trustworthy as its least-deterministic
edge, so link parsing must be exact, not naive.
Grammar (target in brackets): [[slug]] -> slug [[slug|alias]] -> slug (alias is display text, not the target) [[slug#heading]] -> slug (heading is an intra-page anchor) ![[embed]] -> embed (transclusion is a reference = an edge) [text](path.md) -> path.md (standard-markdown link to a .md file) Order-preserving de-dupe; non-.md markdown links (http, images) are ignored. """ from __future__ import annotations
import re
# ![[x]] or [[x]] — capture the target up to the first '#', '|', or ']'. _WIKI = re.compile(r"!?\[\[\s([^\]|#]+?)\s(?:[#|][^\]]*)?\]\]") # [text](path.md) — capture a .md path target (ignore http/images/other extensions). _MDLINK = re.compile(r"\[[^\]]\]\(\s([^)\s]+\.md)\s*\)")
def parse_okf_links(text: str) -> list:
"""Extract OKF edge targets from text, in document order, de-duped."""
if not text:
return []
hits = []
for m in re.finditer(r"!?\[\[\s[^\]|#]+?\s(?:[#|][^\]])?\]\]|\[[^\]]\]\(\s[^)\s]+\.md\s\)", text):
frag = m.group(0)
w = _WIKI.match(frag)
if w:
hits.append(w.group(1).strip())
continue
d = _MDLINK.match(frag)
if d:
hits.append(d.group(1).strip())
return list(dict.fromkeys(hits))
`
Run: venv/bin/python -m pytest tests/test_okf_links.py -v
Expected: PASS (2 tests).
`bash
git add src/swarph_cli/commands/okf_links.py tests/test_okf_links.py
git commit -m "feat(okf): shared OKF link-grammar parser (pinned grammar, reusable)"
`
---
Task 2: swarph timeline — parse, date-filter, verbs, human output
Files:
Interfaces:
`python
# tests/test_timeline_command.py
import datetime as dt
from swarph_cli.commands import timeline
SAMPLE = ( "# swarph timeline\n" "- 2026-07-10T21:02Z · lab-ovh · built tunnel-watch · → [[feedback_x]]\n" "- 2026-07-13T04:24Z · lab-ovh · credential isolation note [[reference_swairm_repo]]\n" "- 2026-07-15T08:51Z · gridiron · reaper operational · → [[feedback_y]]\n" )
def _write(tmp_path): p = tmp_path / "TIMELINE.md" p.write_text(SAMPLE, encoding="utf-8") return str(p)
def test_load_entries_parses_ts_cell_links(tmp_path): entries = timeline.load_entries(_write(tmp_path)) assert len(entries) == 3 e = entries[0] assert e.ts == dt.datetime(2026, 7, 10, 21, 2, tzinfo=dt.timezone.utc) assert e.cell == "lab-ovh" assert e.links == ["feedback_x"] # inline [[link]] (not just the → pointer) is captured assert entries[1].links == ["reference_swairm_repo"]
def test_range_since_around(tmp_path, monkeypatch, capsys): monkeypatch.setenv("SWARPH_TIMELINE", _write(tmp_path)) assert timeline.run_timeline(["range", "2026-07-12", "2026-07-14"]) == 0 out = capsys.readouterr().out assert "2026-07-13T04:24Z" in out and "2026-07-10" not in out and "2026-07-15" not in out assert timeline.run_timeline(["since", "2026-07-14"]) == 0 assert "2026-07-15T08:51Z" in capsys.readouterr().out assert timeline.run_timeline(["around", "2026-07-13", "--window", "1d"]) == 0 around = capsys.readouterr().out assert "2026-07-13T04:24Z" in around and "2026-07-15" not in around
def test_missing_file_is_fail_safe(tmp_path, monkeypatch, capsys):
monkeypatch.setenv("SWARPH_TIMELINE", str(tmp_path / "nope.md"))
rc = timeline.run_timeline(["since", "2026-07-01"])
assert rc == 1 # non-zero, not a traceback
assert "timeline" in capsys.readouterr().err.lower()
`
Run: venv/bin/python -m pytest tests/test_timeline_command.py -v
Expected: FAIL — ModuleNotFoundError: ...timeline / AttributeError.
`python
# src/swarph_cli/commands/timeline.py
"""swarph timeline — DETERMINISTIC temporal lookup over the git-backed timeline.
The TEMPORAL on-ramp of the OKF traversal brain (sub-project A). Reads the raw
~/swarph-timeline/TIMELINE.md (the append-only, git-merged shared log) and
answers date-scoped questions — range/around/since — with NO model,
NO network, NO server ($0, deterministic). Each entry is an OKF temporal node:
its canonical id is its ISO timestamp; its edges are the [[links]] it names
(into the knowledge hemisphere). Complements the semantic brain-ask and the
structural codegraph/memory.
Filters by each entry's EMBEDDED @ (the swarph-highlight line
format), never the git commit date — the entry's own date is canonical. Read-only;
stdlib-only; fail-safe (a missing/unreadable file → stderr note + non-zero, never
a traceback).
"""
from __future__ import annotations
import argparse import datetime as dt import json import os import re import sys from collections import namedtuple
from swarph_cli.commands.okf_links import parse_okf_links
Entry = namedtuple("Entry", "ts cell text links")
_DEFAULT_TIMELINE = os.path.expanduser("~/swarph-timeline/TIMELINE.md")
# -
def _timeline_path() -> str: return os.environ.get("SWARPH_TIMELINE", _DEFAULT_TIMELINE)
def _parse_entry_ts(s: str) -> dt.datetime | None:
"""Parse the entry timestamp 2026-07-15T08:51Z (minute precision, UTC)."""
try:
return dt.datetime.strptime(s, "%Y-%m-%dT%H:%MZ").replace(tzinfo=dt.timezone.utc)
except ValueError:
return None
def load_entries(path: str) -> list: """Parse TIMELINE.md into Entry tuples. Raises OSError if unreadable (caller is fail-safe). Malformed lines (no match / bad ts) are skipped, not fatal.""" entries = [] with open(path, encoding="utf-8") as f: for line in f: m = _LINE.match(line.rstrip("\n")) if not m: continue ts = _parse_entry_ts(m.group("ts")) if ts is None: continue rest = m.group("rest") entries.append(Entry(ts=ts, cell=m.group("cell").strip(), text=rest, links=parse_okf_links(rest))) return entries
def _parse_arg_date(s: str) -> dt.datetime:
"""Parse a CLI date arg. Accepts YYYY-MM-DD or a full ISO timestamp."""
for fmt in ("%Y-%m-%dT%H:%MZ", "%Y-%m-%d"):
try:
return dt.datetime.strptime(s, fmt).replace(tzinfo=dt.timezone.utc)
except ValueError:
continue
raise ValueError(f"unparseable date {s!r} (use YYYY-MM-DD)")
def _fmt_human(e: Entry) -> str: ts = e.ts.strftime("%Y-%m-%dT%H:%MZ") links = (" " + " ".join(f"[[{l}]]" for l in e.links)) if e.links else "" return f"{ts} · {e.cell} · {e.text}{links}"
def run_timeline(argv: list) -> int: p = argparse.ArgumentParser( prog="swarph timeline", description="Deterministic temporal lookup over the git-backed swarph timeline " "(range/around/since). $0, no model, no network.") sub = p.add_subparsers(dest="subcommand") pr = sub.add_parser("range", help="entries between two dates (inclusive)") pr.add_argument("start"); pr.add_argument("end") pa = sub.add_parser("around", help="entries within a window of a date") pa.add_argument("date"); pa.add_argument("--window", default="3d", help="e.g. 3d, 12h") ps = sub.add_parser("since", help="entries on/after a date") ps.add_argument("date") for sp in (pr, pa, ps): sp.add_argument("--json", action="store_true", help="OKF node/edge JSON") args = p.parse_args(argv) if not args.subcommand: p.print_help(); return 0
try: entries = load_entries(_timeline_path()) except OSError as e: print(f"swarph timeline: cannot read {_timeline_path()} ({e})", file=sys.stderr) return 1
try: lo, hi = _bounds(args) except ValueError as e: print(f"swarph timeline: {e}", file=sys.stderr) return 1 hits = [e for e in entries if (lo is None or e.ts >= lo) and (hi is None or e.ts <= hi)]
if getattr(args, "json", False): print(json.dumps([_as_okf(e) for e in hits], indent=2)) else: for e in hits: print(_fmt_human(e)) return 0
def _parse_window(w: str) -> dt.timedelta: m = re.fullmatch(r"(\d+)([dh])", w.strip()) if not m: raise ValueError(f"bad --window {w!r} (use e.g. 3d or 12h)") n = int(m.group(1)) return dt.timedelta(days=n) if m.group(2) == "d" else dt.timedelta(hours=n)
def _bounds(args): """(lo, hi) datetime bounds for the chosen subcommand; end-of-day for bare dates.""" eod = dt.timedelta(hours=23, minutes=59) if args.subcommand == "range": return _parse_arg_date(args.start), _parse_arg_date(args.end) + eod if args.subcommand == "since": return _parse_arg_date(args.date), None if args.subcommand == "around": c = _parse_arg_date(args.date); w = _parse_window(args.window) return c - w, c + w + eod return None, None
def _as_okf(e: Entry) -> dict:
ts = e.ts.strftime("%Y-%m-%dT%H:%MZ")
return {
"node": {"id": ts, "hemisphere": "time", "ts": ts},
"edges": [{"type": "link", "to": l, "to_hemisphere": "knowledge",
"direction": "out"} for l in e.links],
"cell": e.cell, "text": e.text,
}
`
Run: venv/bin/python -m pytest tests/test_timeline_command.py -v
Expected: PASS (4 tests). (The _as_okf/--json path is exercised in Task 3's test; it's included here so the module is complete.)
`bash
git add src/swarph_cli/commands/timeline.py tests/test_timeline_command.py
git commit -m "feat(timeline): swarph timeline range/around/since over the git timeline"
`
---
Task 3: --json OKF node/edge output
Files:
Interfaces:
`python
def test_json_emits_okf_node_edges(tmp_path, monkeypatch, capsys):
import json
monkeypatch.setenv("SWARPH_TIMELINE", _write(tmp_path))
assert timeline.run_timeline(["since", "2026-07-14", "--json"]) == 0
payload = json.loads(capsys.readouterr().out)
assert len(payload) == 1
rec = payload[0]
assert rec["node"] == {"id": "2026-07-15T08:51Z", "hemisphere": "time",
"ts": "2026-07-15T08:51Z"}
assert rec["edges"] == [{"type": "link", "to": "feedback_y",
"to_hemisphere": "knowledge", "direction": "out"}]
assert rec["cell"] == "gridiron"
`
Run: venv/bin/python -m pytest tests/test_timeline_command.py::test_json_emits_okf_node_edges -v
Expected: PASS if Task 2's _as_okf is correct. If it FAILS, fix _as_okf/--json in timeline.py until the OKF schema matches exactly. (This task's value is locking the schema with an assertion; write it first and confirm it drives the shape.)
Ensure _as_okf returns exactly {"node": {"id","hemisphere":"time","ts"}, "edges":[{"type":"link","to","to_hemisphere":"knowledge","direction":"out"}], "cell", "text"}.
Run: venv/bin/python -m pytest tests/test_timeline_command.py -v
Expected: PASS (all 5).
`bash
git add src/swarph_cli/commands/timeline.py tests/test_timeline_command.py
git commit -m "test(timeline): lock the OKF node/edge --json schema"
`
---
Task 4: Register timeline in the CLI dispatch
Files:
Interfaces:
`python
def test_timeline_registered_in_dispatch():
from swarph_cli import main as m
assert m._VERB_HANDLERS["timeline"] == "swarph_cli.commands.timeline.run_timeline"
`
Run: venv/bin/python -m pytest tests/test_timeline_command.py::test_timeline_registered_in_dispatch -v
Expected: FAIL — KeyError: 'timeline'.
In src/swarph_cli/main.py, add to the _VERB_HANDLERS dict (next to "memory"):
`python
"timeline": "swarph_cli.commands.timeline.run_timeline",
`
Run: venv/bin/python -m pytest tests/test_timeline_command.py::test_timeline_registered_in_dispatch -v → PASS.
Run: venv/bin/python -m swarph_cli timeline --help → prints range/around/since help, exit 0.
`bash
git add src/swarph_cli/main.py tests/test_timeline_command.py
git commit -m "feat(timeline): register swarph timeline verb in CLI dispatch"
`
---
Task 5: swarph_timeline_navigate MCP tool
Files:
Interfaces:
`python
def test_timeline_navigate_failsafe(tmp_path, monkeypatch):
from swarph_cli.commands import mcp_server
monkeypatch.setenv("SWARPH_TIMELINE", _write(tmp_path))
got = mcp_server._timeline_navigate("since", date="2026-07-14")
assert got and got[0]["node"]["id"] == "2026-07-15T08:51Z"
# fail-safe: unknown op / bad input NEVER raises
assert mcp_server._timeline_navigate("bogus") == []
monkeypatch.setenv("SWARPH_TIMELINE", str(tmp_path / "nope.md"))
assert mcp_server._timeline_navigate("since", date="2026-07-01") == []
`
Run: venv/bin/python -m pytest tests/test_timeline_command.py::test_timeline_navigate_failsafe -v
Expected: FAIL — AttributeError: module 'mcp_server' has no attribute '_timeline_navigate'.
In src/swarph_cli/commands/mcp_server.py, extend the existing import line to add timeline:
`python
from swarph_cli.commands import brain_ask, memory, timeline
`
Add the wrapper (near _memory_navigate):
`python
def _timeline_navigate(op: str, start: str = "", end: str = "", date: str = "",
window: str = "3d"):
"""Deterministic temporal lookup over the git timeline. op: 'range'|'around'|'since'.
Returns OKF node/edge records (list). Fail-safe: any bad input/op/read → [] (never raises)."""
import argparse as _a
try:
ns = _a.Namespace(subcommand=op, start=start, end=end, date=date, window=window)
lo, hi = timeline._bounds(ns)
entries = timeline.load_entries(timeline._timeline_path())
hits = [e for e in entries if (lo is None or e.ts >= lo) and (hi is None or e.ts <= hi)]
return [timeline._as_okf(e) for e in hits]
except Exception:
return []
`
Register the tool inside the existing FastMCP builder (beside swarph_memory_navigate):
`python
@mcp.tool()
def swarph_timeline_navigate(op: str, start: str = "", end: str = "",
date: str = "", window: str = "3d"):
"""Deterministic temporal lookup over the swarph timeline: op='range'|'around'|'since'.
The temporal hemisphere of the OKF traversal brain — entries are dated OKF nodes with
[[link]] edges into knowledge. Complements semantic recall; $0, no model."""
return _timeline_navigate(op, start=start, end=end, date=date, window=window)
`
Run: venv/bin/python -m pytest tests/test_timeline_command.py::test_timeline_navigate_failsafe -v
Expected: PASS.
`bash
git add src/swarph_cli/commands/mcp_server.py tests/test_timeline_command.py
git commit -m "feat(timeline): swarph_timeline_navigate MCP tool (deterministic temporal nav)"
`
---
Task 6: README §timeline + version bump to 0.31.0
Files:
Interfaces: none (docs + version constants).
Grep the current pins: grep -rn "0.30.0" tests/. In tests/test_brain_ask_command.py and tests/test_watchdog.py, change the asserted version "0.30.0" → "0.31.0" AND rename the test functions test_version_is_0_30_0 → test_version_is_0_31_0.
Run: venv/bin/python -m pytest tests/test_brain_ask_command.py tests/test_watchdog.py -k version -v
Expected: FAIL (code still reports 0.30.0).
pyproject.toml: version = "0.31.0". src/swarph_cli/__init__.py: __version__ = "0.31.0".
Run: venv/bin/python -m pytest tests/test_brain_ask_command.py tests/test_watchdog.py -k version -v
Expected: PASS.
In README.md, after the ### swarph memory section, add:
markdown
swarph timeline (v0.31.0)
Deterministic, $0 temporal lookup over the git-backed swarph timeline — the temporal hemisphere of the OKF traversal brain (counterpart to swarph codegraph for code and swarph memory for knowledge). No model, no network — it parses the timeline file.
`
swarph timeline range `
Dates are YYYY-MM-DD (or a full ISO timestamp). Source is ~/swarph-timeline/TIMELINE.md (override with SWARPH_TIMELINE). Add --json for the OKF node/edge payload (each entry is a dated node with [[link]] edges into knowledge). Also exposed to any MCP host as swarph_timeline_navigate.
Run: venv/bin/python -m pytest -q
Expected: all pass (new okf_links + timeline tests + bumped pins; no regressions).
`bash
git add README.md pyproject.toml src/swarph_cli/__init__.py tests/test_brain_ask_command.py tests/test_watchdog.py
git commit -m "docs(timeline): README §timeline; bump 0.31.0"
`
---
Self-Review
1. Spec coverage (sub-project A of the traversal-brain spec):
2. Placeholder scan: no TBD/TODO; every code step is complete. Task 3 is a schema-locking test over Task 2's _as_okf — the one place an implementer might find the assertion already green; the step says so explicitly and to fix _as_okf if not.
3. Type consistency: parse_okf_links → list[str] (Task 1) consumed by load_entries (Task 2). Entry(ts: datetime, cell, text, links) used in Tasks 2/3/5. _bounds(args)/_as_okf(e)/_timeline_path()/load_entries(path) signatures identical across Task 2 (def), Task 3 (test), Task 5 (MCP wrapper reuse). The MCP wrapper builds an argparse.Namespace with exactly the fields _bounds reads (subcommand, start, end, date, window). _VERB_HANDLERS["timeline"] (Task 4) matches the module path swarph_cli.commands.timeline.run_timeline.