Skip to Content
文档变更日志

Changelog

Two layers:

  • Releases (below) — version-anchored freeze records. Each ## [x.y.z] is the read-only snapshot of what shipped at that git tag: mature features frozen in, plus what was deliberately held back. Append a new section per tag; never rewrite a released one.
  • Architecture milestones (further down) — newest-first narrative of contract/architecture changes. Deeper context; not version-keyed.

Versioning rules live in AGENT.md §3. Freeze criteria in AGENT.md §5.


Releases

[0.2.0-rc.3] — 2026-07-10

Git tag v0.2.0-rc.3, branch release/v0.2.0. Engine 0.2.0rc3.

Third release candidate of the v0.2 line. Bug-fix-only delta over rc.2: fixes two related sources of tx_log / IO noise from the inbound pipeline.

Fixed since rc.2:

  • Store no-op suppression for identical writesStore.set_field, set_links, tag, untag, sync_identity, update_node, unset_field, and create_link now short-circuit when the requested write would not change the persisted state. No database mutation, no tx_log row, no updated_at bump, and no reactive event is emitted. This stops the obsidian-mirror / agent-session connectors from generating millions of redundant tx_log rows during periodic full-scan polls (observed: 87 GB data.db from 49.9 M tx_log rows in ~5 days).
  • State-machine self-transitions still emit state-transitioned — the engine’s (transition …) primitive detects when the new state equals the current one and records the transition attempt directly into tx_log via the Store’s public audit_event API. The Store layer stays field-name-agnostic (no __state__ carve-out in set_field); the engine owns the “this is a state transition” semantics and the reactive bus picks the audit event up through its existing store.on_commit subscription unchanged. SCM recheck_quarter_after_* actions rely on this — without it, the reactive close_quarter_and_draft_next cascade would never re-fire after a sprint closes or a relevant Thought is added.
  • obsidian-markdown connector skips unchanged files by mtime — the connector now persists a {path: mtime} baseline on the IngestConnection node (上次扫描mtime). Between pulls it only reads files whose mtime has changed. Combined with Store no-op suppression, this reduces the steady-state cost of a periodic vault scan from “parse every file every tick” to “stat every file every tick”.

Held back / unchanged from rc.2: 成熟功能表(CLAUDE.md §5),实验功能 隐藏(CLAUDE.md §7)。

Gates: pytest 1210 passed / 15 skipped / 1 xfailed, ruff clean, iso runex init + runex ontology load + runex doctor 三步通。

[0.2.0-rc.2] — 2026-07-10

Git tag v0.2.0-rc.2, branch release/v0.2.0. Engine 0.2.0rc2.

Second release candidate of the v0.2 line. Replaces rc.1; the rc.1 → rc.2 delta is small and bug-fix-only.

Fixed since rc.1:

  • install.sh: while IFS= read 丢最后一行extensions/CORE_SKILLS 原文件没有 trailing newline,bash 的 while read 会默默丢掉最后一行 结果 RUNEX_SKILLS 永远是空。修法:
    1. CORE_SKILLS 加 trailing newline
    2. install.sh 改用 while read ... || [ -n "$line" ] 防御性写法
  • ontology: validate supertag refs on SCM loadload_ontology_file 现在校验每个 machine 的 :supertag 是否已注册;未注册抛 LoaderError 而非等到 dispatch 时才崩。dispatch 也把 ValueError 归类为 unregistered_supertag(PR #d18ac24,Windows 端报的 50+ 测试错误的 根因)
  • build 脚本单源 versionscripts/runex/{build.sh,build.ps1} 现在从 src/runex/__init__.py__version__,按 Nuitka 4-part 格式转换(0.2.0rc20.2.0.2)。runex --versionCFBundleShortVersionString 同步,不会再因为 hardcoded 错位
  • help URL 更新runex.0ma.ai0ma.ai(文档站改路径)
  • __init__.py 简化 — 删 importlib.metadata.version fallback (wheel metadata 是 stale 的,曾导致 0.3.0.dev1 残留覆盖 source)

Held back / unchanged from rc.1: 成熟功能表(CLAUDE.md §5),实验功能 隐藏(CLAUDE.md §7)。

Gates: pytest 1201 passed / 15 skipped / 1 xfailed, ruff clean, iso runex init + runex ontology load + runex doctor 三步通。

[0.2.0-rc.1] — 2026-07-09

Git tag v0.2.0-rc.1, branch release/v0.2.0. Engine 0.2.0rc1.

First release candidate of the v0.2 line. Superseded by rc.2 — has the trailing-newline bug fixed in rc.2. Not the source of truth; historical record.

[0.2.0-canary.1] — 2026-07-09

Git tag v0.2.0-canary.1, branch dev. Engine 0.2.0.dev1.

First canary of the v0.2 line. Aggregated everything from v0.2.0-alpha forward: Windows build, obsidian-mirror Recipe, agent-session / obsidian-assistant / autoagency packs, runex skill 重设计, ADR-0018 引擎最小底座。Used for internal testing only; bugs expected.

[0.2.0-alpha] — 2026-06-26

First frozen tag of the v0.2 milestone (git tag v0.2.0-alpha, branched from the Phase-6-hardened commit). The contents below accurately describe this tag.

Not released as a user-facing version. Stable for CI and architecture review; no installer published. Next: Phase 9 (agent-mesh, ADR-0012) + real Agent Provider.

[0.2.0] — 2026-06-25

Proactive Agent Runtime. Ontology state can now durably dispatch slow Agent/tool work, survive restart, request human approval, and evolve its own rules — all without weakening the synchronous deterministic ReactiveBus. Slow, durable work lives permanently outside the bus (ADR-0010); every new durable concept is an ordinary node, no new L1 table.

Shipped (Phase 1–6, all frozen mature):

  • Obsidian ↔ Ontology human mirror (Phase 1). Runnable Connector / Recipe / IngestJob seam; the obsidian-markdown Connector + no-I/O ontology-frontmatter Recipe; safe Frontmatter/body patch with checksum baseline + Conflict node instead of silent overwrite; cursor-based Store observation that schedules projection (ADR-0008/0011).
  • Durable work protocol (Phase 2). WorkItem as a Supertag + Machine + Actions — idempotent signal key, attempts, availability, claim owner, lease, retry/backoff, and expired-claim recovery. A killed worker restarts and finishes exactly once at the business-effect level.
  • Worker host + Agent contract (Phase 3). runex ontology listen composes ingest + durable work + projection in one supervised loop with graceful shutdown and a persisted cursor. The Agent contract: bounded envelope, fresh reference-context load, manifest-validated CommandBatch output, provenance, and a deterministic FakeAgent for CI (no model in tests).
  • Proposal & human approval (Phase 4). Generic Proposal lifecycle (pending_review → approved/rejected/revision_requested/expired/cancelled). Approval is a later graph write (a Frontmatter/CLI field), never a waiting thread; approving idempotently upserts the real object by natural key, rejecting creates none.
  • First proactive reflex arc (Phase 5). Obsidian Thought → WorkItem → Agent extraction → Task Proposal → Obsidian approval → real Task → Obsidian projection, proven from a clean RUNEX_HOME with a crash injected during work (recovers exactly once), no echo loop, complete provenance. Markdown sink now renders type: so machine-born nodes round-trip back in.
  • Safe natural-language rule evolution (Phase 6). BusinessRule draft → compiled → verified → approved → deployed (→ rolled_back). A Compiler Agent only produces a .scm draft; the verifier statically checks it against the live manifest + a machine-readable DSL primitive catalog (runex ontology primitives), writing structured diagnostics; an approved rule is the only thing that loads into production. No Agent has direct deploy authority.

New CLI: ontology listen, ontology proposals / proposal-approve / proposal-reject, ontology rules / rule-approve, ontology primitives.

Held back / not in this release:

  • A real-Agent dogfood is an operator-run manual gate; CI uses FakeAgent / FakeCompiler throughout.
  • Phase 7–8 (Assertion / Conflict / Current-Truth; Artifact & external collaboration) remain post-v0.2 candidates.
  • Legacy non-Recipe preset adapters (obsidian-thought, wechat-session, media-snapshot, nocodb) are no longer on the evolution path; the stable ingest path is the obsidian-markdown Connector + ontology-frontmatter Recipe. packs/media.scm / packs/session.scm / intel-research.scm stay dev-only fixtures.

Gates: full pytest green (682 pass, 2 skip), ontology check --strict exit 0, ReactiveBus unchanged (no async/threading).

[0.1.1] — 2026-06-24

清理 + 文档发布(PATCH,无新功能、无 schema 变更)。

  • 撤出误进 wheel 的实验 packintel-research.scmsrc/runex/skill/ ontology/packs/ 移到 tests/fixtures/,不再随包分发(0.1.0 曾误打进 wheel)。media.scm / session.scm 同处 fixtures,均开发中。
  • runex ingest 标记为实验功能:docstring 注明 adapter / 参数 / 命令行 入口未来可能变更或移除,勿在稳定流程依赖。
  • 安装文档改进:README + references/installation.md 首推”交给 agent 代装”,手动路径主推 uv tool install(自动 bootstrap Python)。
  • 修正 skill-collection-roadmap.md 与代码现实矛盾(拆分已落地)。
  • changelog 引入版本号锚定的冻结层;AGENT.md §7 固化该流程。

Held back 不变:见 [0.1.0]。

[0.1.0] — 2026-06-24

First distributable release. Frozen mature surface:

模块内容
core-memory.scm17 通用类型 · 10 机器 · 36 action
packs/knowledge.scmAtom / Topic / HowTo / MOC
packs/time.scmThread(跨周期工作线,closure-aware scheduling)
packs/wiki-links.scmbody [[..]] 自建 mentions / 提炼自 边
skillscapture · agenda · sprint-planning · distill · topic · moc
ref facade所有 ref 字段 CLI 可写(名称 → typed edge,ADR-0009)
runex init / skill install一键建库 + 加载 ontology + 装 skill
host cronclose_sprint / close_goal(scheduling 闭环)
kernelderived-node 删除传播 · guard 自省 · dry-run preview · blocking-kernel lint

Held back (experimental, dev-only — not in this freeze): runex ingest 入口及其 adapter(obsidian-thought / wechat-session / media-snapshot); packs/media.scmpacks/session.scmintel-research.scm(均在 tests/fixtures/,开发中)。下个版本发布前再确认去留。


Architecture milestones

Newest first. Each entry covers what changed at the contract and architecture level; implementation detail lives in the commit history.


2026-08-05 — personal-assistant pack:闭环行动 + 会话导入合并 + Pi 支持

New pack. extensions/packs/personal-assistant/ 把「无需 Obsidian 的闭环行动 体验」(know-me / capture-now / plan-today / advance-work / review-week + onboard + unified-ontology-compile)与「历史会话导入」合为一个包;原 agent-session pack 并入其中(连接器仍在引擎层)。

What changed.

  • 新增 PiSession supertag(extends LLMSession)与 pi-session Connector
    • pi-session-mirror Recipe(读取 ~/.pi/agent/sessions/<编码cwd>/*.jsonl, 目录编码与 Claude Code 一致);session-onboard 从 3 平台扩到 4 平台 (Claude Code / Codex CLI / OpenCode / Pi)。
  • 会话类型在图中可被 know-me / advance-work 感知:「认识下我」先读最近会话, 「推进X」先跨会话检索未完成事项(跨 Agent 续接)。
  • 安装器在 runex home 注入 AGENTS.mdCLAUDE.md 软链)+ 部署 8 个项目级 skill + 按序加载 memory → knowledge → session → personal-core。
  • 移除 extensions/packs/agent-session/(其连接器 claude/codex/opencode _session_connector 与三个 mirror Recipe 保留在引擎层,由 personal-assistant 的 session.scm 提供服务)。

边界。 新版 GPT 应用(含 Codex 功能)的会话在云端、无本地文件,无法导入; Codex 仅指 CLI 版(~/.codex/sessions)。

Contract change. Until now runex was published as a pip install runex / uv tool install runex Python wheel. The engine binary that downstream products shell out to was resolved through that Python-package install. As of this entry, runex ships exclusively as a Nuitka-compiled onefile Mach-O inside runex-macos-arm64.zipsrc/runex/ is build-time source-of-truth, no longer a Python-installable artifact.

What changed.

  • scripts/runex/build.sh and scripts/runex/build-release.sh produce dist/runex-macos-arm64/{runex, install.sh, README.txt} and pack them into dist/runex-macos-arm64.zip. The Mach-O is self-contained (Python runtime + system SCMs + SQLite DDL baked in via --include-data-dir=).
  • scripts/runex/install.sh symlinks the binary into ~/.local/bin/runex; --uninstall removes the symlink only.
  • The [tool.hatch.build.targets.wheel] block in pyproject.toml is annotated as a dev-only convenience: it still resolves a Python module for in-tree tests, but production consumers take the binary from the release zip.
  • autoagency (runexfab/build-release.sh) now accepts RUNEX_BINARY_ZIP to pull the engine Mach-O from a release zip instead of the legacy src/runex/ mirror.

Why now. Concrete pain: integrating two agents across pip install

  • uv tool install was a constant footgun (the wrong runex PATH entry got picked up; the source-tree src/runex/ import shadowed the binary; CI tests couldn’t tell which one they were exercising). The onefile binary has a single concrete location, no Python on the host, and trivial install symmetry — every consumer sees the same ~/.local/bin/runex.

Platforms. Only macos-arm64 is built today. Cross-platform packaging is a future addition; the build script has the platform check at the top — adding linux-x86_64 is a matter of an analogous build-linux.sh that pipes through the same Nuitka invocation.

Migration path. Operators with an existing uv tool install runex can uv tool uninstall runex then ./install.sh from the unzipped runex-macos-arm64/. The CLI surface (runex commands) is unchanged — same command names, same --help output, same options. Anything sub-command set changed is recorded in the v0.2.0-alpha → next tag release notes.

Held back / not in this entry.

  • src/runex/skill/references/ content stays as-is for now; the move to bundled/ continues under separate doc-side rewrites (see the 2026-07-05 bundled/ refactor entry).
  • autoagency-side runex mirror (src/runex/) is git-ignored but kept on disk — see .gitignore entry for the next-commit removal. The binary is now authoritative; the source mirror is only for archaeology and diff inspection.
  • RunEX skill bundling for the engine binary itself (runex skill install finding the SKILLs from inside the zip) is a follow-up — current runex skill install still walks the source tree.

2026-06-30 — Thread-local Store isolation for multi-threaded access

The existing single-writer Store model was thread-unsafe: a multi-threaded context (e.g., a worker host running ingest and work concurrently) could interleave writes, corrupt the transaction log, or observe each other’s uncommitted state.

What changed. Store gains an isolated() context manager that clones the underlying SQLite connection per thread. Each thread gets its own ThreadLocalStore — a private connection with independent transaction state and cursor. Event producers emit through the original connection’s on_commit hook, so the reactive bus still sees a single serialised event stream.

Invariant preserved. The ReactiveBus remains single-threaded and deterministic within each effect scope. The multi-threaded boundary is ingest-side only: parallel Connector reads, not parallel reactive dispatch. ADR-0014’s effect_scope() contract (one bus, one thread per cascade) is unchanged; the thread-local layer applies only to the Store→L2 boundary, not to L3 dispatch.

Gates: pytest -q green; no change to effect_scope() or reactive ordering.


2026-06-29 — ADR-0014: Manual-effect scope, cascade deferral, and ADR-0015 dual-entry implicit scoping

A bug surfaced where a manual action’s effect block could trigger mid-effect reactive cascades before the effect finished, causing guards to see inconsistent intermediate state. This violated the principle that a reactive rule sees a consistent final snapshot.

What changed (ADR-0014, Accepted). Reactive cascade is now deferred until the manual effect block completes. The bus flushes accumulated events in one batch at the end of effect_scope(), guaranteeing that any guard sees the complete final __state__* and field values of the triggering mutation. Three mechanisms deliver this:

  • M1 — Deferred cascade. bus.effect_scope() collects store events during the with: block and dispatches matched actions only on exit.
  • M2 — Forced LWW dedup. Multiple writes to the same literal field within one scope are reduced to the last value before reactive dispatch, eliminating self-churn from sequential set-field calls mid-effect.
  • M3 — Net-zero short-circuit. If the final field value equals the pre-scope value, no field-set event is emitted at all — preventing no-op cascades from restarting.

ADR-0015 (Implicit Effect Scope — Dual-Entry Design) extends ADR-0014 with the effect_scope() context-manager API and two entry paths:

  • Scheme A (primary): Engine.dispatch opens effect_scope() at the call boundary — every manual dispatch is implicitly scoped without explicit with: in the DSL.
  • Scheme B (secondary): The DSL evaluator opens effect_scope() on any bare (begin …) block — covers code executed outside dispatch (e.g., kernel callbacks, test harnesses).

M5 enforces single-effect transactional atomicity: if any step in the effect block raises, the entire scope rolls back via store SAVEPOINT/ROLLBACK. M6 nests correctly — events from enclosing scopes survive even if an inner scope rolls back.

ADR-0015 is referenced but the ADR file is pending; the implemented behaviour is documented in ADR-0014’s “M5/M6 + Dual-Entry Design” sections.

Gates: pytest -q green; guard introspection traces unchanged; M3 net-zero verified by CascadeSpy assertions; M5 rollback verified with store.atomic(); effect_scope() is the only public entry to the reactive flush machinery.


2026-06-29 — ADR-0013: Vault schema distribution from manifest (Proposed)

ADR-0013 (Proposed) addresses a gap left by ADR-0011: the human-sovereign Obsidian mirror has no machine-readable contract for frontmatter shapes. When an agent creates a note, it has no structural guarantee about required fields, types, or valid ref targets for a given supertag.

The proposed direction: manifest() as the single schema authority — agents and skills read frontmatter shapes from it; runex vault schema generates Obsidian templates and .base (Bases) files from the live ontology; no second source of schema truth exists.

Not yet implemented; the ADR is in Proposed state. A companion PRD (tasks/prd-vault-registry-distribution.md) details the opinionated default vault path (~/.runex/runex-vault/) and the template/Base generation surface.


2026-06 — Documentation information architecture: one source of truth per concern

The planning docs had drifted into overlap: docs/roadmap.md duplicated the phase breakdown that lives in tasks/, mirrored changelog.md (it carried large amounts of done material), and absorbed realized-architecture essays that belong with the invariants. “Who is SoT?” had no clear answer.

The assignment now is explicit:

  • tasks/ is the planning SoT — prd-*.md (requirements + acceptance, with requirement IDs) and tasks-*.md (the execution checklist dev is driven from). Promoted to a first-class entry in the docs/ map; a new tasks/README.md states the convention and distinguishes the active milestone pair from the historical v0.1-era coverage PRDs.
  • changelog.md is the history SoT (this file).
  • architecture.md is the current-state SoT. It absorbed roadmap’s realized “channels as extensions” thesis: the capability/channel seam’s executable criterion (the test_capability_seam.py grep gate), a new “Hot-discovery and the trust boundary” subsection, strengthened Invariant 2 (“no channel names in core”), and a new Invariant 12 (the extension-directory trust principal).
  • roadmap.md is now future-only (~150 lines, down from ~560): the shipped v0.2.0 summary, the post-v0.2 Phase 7/8/9 direction, and open architectural positions. It no longer restates phase detail, done-tracking, or invariants; it points at the SoT for each.

No code behavior changed; stale docs/roadmap.md pointers in source/tests were repointed to architecture.md, and one obsolete “guard introspection is not yet diagnosable” note was corrected (it shipped).


2026-06 — Derived-node lifecycle: source deletions now propagate to the Store

FileWatcher previously handled two file events: create (new node) and modify (re-ingest → update). The third event — deletion — was silently ignored, leaving stale ghost nodes in the Store whenever a source file was removed.

What changed. FileWatcher now runs a deletion sweep on every poll tick. After scanning the current file set, it compares against the previously-ingested snapshot. For each file that has disappeared, it looks up Store nodes whose 来源路径 field matches the path and deletes them. The lookup is implemented via store.find_nodes_by_field_value(field_name, value), a new cross-supertag field query that does not require knowing the supertag name.

The invariant this establishes. Derived nodes — those ingested from an external source file — now have a complete and symmetric lifecycle: they are created when their source appears, updated when their source changes, and deleted when their source disappears. The Store reflects the external source’s existence state without any manual reconciliation step.

Native nodes are structurally untouched. Nodes created inside runex (via create-node DSL effect, direct CLI, or reactive synthesis) carry no 来源路径 field and are therefore unreachable by the deletion sweep. The two-mode contract — derived nodes are a disposable cache, native nodes live in the Store permanently — is now enforced by lifecycle behaviour, not just convention.

Opt-out. FileWatcher(…, source_path_field=None) disables deletion propagation for adapters that do not write a path field (or where ghost retention is explicitly desired). The default remains "来源路径".


2026-05 — Dogfooding round: guard introspection, dry-run preview, blocking-kernel lint

Three usability gaps surfaced during real SCM scenario dogfood and closed.

Guard expression introspection. When dispatch() or the reactive bus rejects an action because its guard returned false, the result now carries a structured guard_trace list. Each entry names the failing sub-expression, the value it produced at runtime, and a note (e.g. "always-false literal" or the evaluated value of (field "client")). evaluate_guard_trace re-walks the guard AST after the fact — safe because guards are pure — and pinpoints the sub-expression responsible. Before: guard_rejected gave the action name and nothing else; an agent had to know engine internals to root-cause a silent rejection. After: the trace is machine-readable and surfaces directly in dispatch() Result.data and in the reactive_guard_rejected event payload.

Dry-run preview — ontology preview. runex ontology preview <action> <node-id> evaluates the full guard+effect+reactive cascade and returns the event list, but commits nothing. Implementation: Store._txn was changed from BEGIN/COMMIT to SAVEPOINT _sp / RELEASE (outer behavior identical, now nestable); facade.preview() wraps dispatch in a SAVEPOINT _preview / ROLLBACK TO SAVEPOINT _preview, capturing cascade events before the rollback. Result carries data.dry_run=true, data.ran, the full events list, and guard_trace on rejection. The failure mode that motivated this: agents ran ontology trace (which commits) as a preview step, consuming state transitions; when the user later said “execute”, the engine returned IllegalTransitionError because the states were already consumed.

Blocking-kernel lint — register_kernel(..., blocking=True) + ontology check. The architectural decision: do not make the reactive bus async. Async dispatch introduces thread-local hazards (_depth counter is mutable instance state) and does not fix the real problem — non-idempotent writes (create-node) inside an action effect would race on concurrent bus wakeups. The root cause is action design, not bus threading. What shipped instead:

  • register_kernel(name, fn, *, blocking=False) — opt-in flag marks kernels that perform heavy I/O (subprocess, network, ASR).
  • engine.blocking_kernels: set[str] — maintained alongside engine.kernels.
  • list_kernels() and manifest() return [{"name": k, "blocking": bool}] dicts (previously plain name strings — agents can now read the flag before authoring an action).
  • analyze_blocking_kernels() — static AST walk over every action’s effect_steps, flagging (call-kernel "NAME" ...) calls where NAME is in blocking_kernels. Returns structured {warnings, clean}.
  • ontology check now runs both conflict analysis and blocking-kernel lint, printing WARNING lines with the Signal-Then-Work pointer. --strict exits 1 on any blocking-kernel warning (in addition to the existing error-on-conflict gate).
  • extension-authoring.md documents the Signal-Then-Work pattern: action effect writes a status field only (fast, atomic); external daemon detects pending status and runs the blocking kernel; a second reactive action reacts to the result field.

The thought_auto_transcribe action in runex-product is the canonical example of what the lint now catches. Its redesign into three-phase shape (detect → daemon runs ASR → analyze) is tracked as a product-side task.


2026-05 — ADR directory added for durable architecture decisions

The docs now include a dedicated ADR directory: adr/README.md.

The first accepted records capture decisions that had become important enough to stop re-arguing informally:

These ADRs complement, rather than replace:

2026-05 — White paper added for the technical vision

The repo now carries a long-form white paper: whitepaper.md.

It frames runex not as “just a protocol” or “just an app”, but as an agent-native ontology runtime: a shared operating substrate for agents, human-facing shells, and external systems. The paper is problem-led rather than product-led: why current screen-centric apps, tool-calling agents, and workflow graphs leave a missing semantic runtime layer; why ontology, eventfulness, and runtime-malleable rules matter; and why channels, typed boundaries, and machine-first contracts are central to the category.

2026-05 — Repository-local Obsidian + NocoDB e2e workflow landed

The end-to-end validation loop is now repository-owned rather than an oral procedure.

What changed. The repo now carries a sanitized but structurally real Obsidian vault fixture, a disposable local NocoDB environment, a bootstrap that creates the 自媒体作品快照 / 作品快照 schema and seed rows, and one formal suite proving both source-read and writeback paths. A single script — scripts/run-local-e2e.sh — starts the service, bootstraps it, loads the generated env, and runs the suite.

Why this matters. The capability/channel architecture is no longer validated only by unit tests, stub-at-seam tests, or one-off production dogfood. The repository now contains a repeatable, safer-than-production business-loop check for:

  • Obsidian Thought -> ingest
  • reactive bilibili detection
  • Bookmark creation
  • NocoDB writeback through the real sink/source adapters

What is proven. OrbStack headless was verified as a working local Docker-compatible runtime for this environment, and the formal suite now passes against the local NocoDB service.

2026-05 — Extension authoring contract documented

The capability/channel architecture now has its Python-side authoring guide. extension-authoring.md documents the runtime contract for:

  • DataSourceSpec source extensions
  • SinkSpec sink extensions
  • KERNELS extension modules

It makes the hot-discovery shape operational for extension authors: directory layout, discovery semantics, capability injection, spec-vs- configured-sink distinction, and the expected testing layers from unit tests through e2e ontology wiring.

2026-05 — Channel extension hot-discovery completed

The last open P1 from the capability/channel architecture landed: the registry is now hot-pluggable in both directions, not just for kernels.

What changed. At startup, runex now scans ~/.runex/extensions/sources/*.py and .../sinks/*.py in addition to .../kernels/*.py. A module-level SPEC: DataSourceSpec or SPEC: SinkSpec is merged into the lazy registry automatically, so a new channel can appear in manifest() and CLI registry lookups without editing the core module list.

Why the implementation moved. Discovery helpers were extracted to a top-level runex.extensions module. The registry can depend on this without importing the ontology package and re-entering facade startup, so the extension path stays cycle-safe.

What is proven. tests/test_extension_discovery.py now covers all three extension buckets: missing dirs are a silent no-op; kernel discovery still works; source/sink extensions appear in manifest().data.datasources / manifest().data.sinks once dropped into the extension dir.

2026-05 — Convergence determinism (4 gaps closed)

Four correctness gaps in the reactive bus were identified and closed behind regression tests. All are observable through the typed event log.

Gap 1 — Ordering enforced at dispatch, not at load. priority ASC, name ASC is now applied inside _on_commit at the point a matched group is formed, not only when actions are loaded. Reloading actions in a different order no longer changes dispatch outcome.

Gap 2 — error-on-conflict fails closed on opaque writes. An error-on-conflict action whose derived field target is not a literal (kernel result, variable — anything static analysis cannot resolve) is now blocked (opaque_blocked) rather than passed through. The guarantee it asserted cannot be proven, so refusal is correct.

Gap 3 — Cross-event same-field conflicts surfaced statically. Two actions on different triggers that write the same field never share a dispatch plan. analyze_latent_conflicts() / manifest().latent_conflicts / runex ontology check now run a whole-ontology static pass and classify findings as blocking (an error-on-conflict field is co-written elsewhere) or advisory. Catch it at design time; the append-only tx_log cannot un-write a prior cascade hop.

Gap 4 — Lint gate is real. ruff check now runs in the test suite (tests/test_lint_gate.py). Pre-existing violations were fixed. The datasource capability registry (adapters/registry.py) landed as part of this milestone: adapters self-describe via SPEC: DataSourceSpec, manifest().data.datasources exposes the registry as structured data.


2026-05 — Contract leap (Phases 0–5)

Rebuilt the L4 interface as a machine-first contract. Previously the facade returned heterogeneous shapes; CLI output was Rich prose; failure was sometimes a raised exception and sometimes a returned dict depending on which operation was called.

Result envelope. Every operation — load, dispatch, manifest, events, analyze_conflicts — now returns {ok, data, error, events, cursor}. ok:false carries a stable error.code (branch on this; never regex message). The CLI --json flag emits this envelope verbatim; exit code mirrors ok.

Closed Event taxonomy + actor provenance invariant. event.kind is drawn from a closed set, readable from manifest().data.event_kinds. Actor field carries one invariant: reactive:<action> ⟺ that action’s effect committed; system:reactive ⟺ a decision event (guard rejected, blocked conflict, illegal transition). Nothing is swallowed.

manifest() as single introspection surface. One call returns the entire introspectable surface: supertags, machines, actions, kernels, datasources, event_kinds, and known latent_conflicts. An agent reads what is possible from this one structured call before acting — never from documentation or source.

Event stream with cursor. events(since, limit, kinds) is cross-process-safe and resumable. cursor is the scanned-window tail — it advances even when a kind filter matches nothing, so a follower on a rare kind never wedges. runex ontology events --follow is a safe cross-process tail.

Datasource registry. Adapters self-describe via SPEC: DataSourceSpec. manifest().data.datasources exposes the registry; DataSourceSpec.build(ctx) validates required params before construction with a structured error naming what is missing.

analyze_conflicts() pre-flight. Returns {field_conflicts, opaque_guarded_actions, blocking_count, clean}. runex ontology check --strict exits 1 on blocking_count > 0 — drop it in CI as a design gate.

Policy-A migration gate. upsert_supertag now enforces: additive schema changes always accepted at runtime; destructive changes refused when nodes exist; byte-identical re-apply is a true no-op (no event emitted, no churn on repeated load).


2026-05 — Data-driven ontology layer

The foundational architectural shift: state machines and actions became declarative data, not Python code.

Before. State machines were Python @register Action subclasses. Adding a new lifecycle required a code edit, restart, and deploy. Agents could not synthesize behavior at runtime. Cascade behavior was imperative call-site coupling.

After. A machine is a node tagged MachineDefinition; an action is a node tagged ActionDefinition. Both are written in a small Scheme-like DSL (s-expressions) stored as longtext fields. A reactive bus subscribes to store mutations and auto-dispatches matching actions.

Shipped in 8 phases: DSL kernel (parser + evaluator) → internal supertags + store_io → Engine + Kernel registry → Bookmark machine as data (first dogfood) → Thought machine + cross-node writes → retire old Python framework → ReactiveBus + Store.on_commit → wiki-link extraction as a reactive action → Ontology facade + CLI + agent guide → pipeline + reactive ingest e2e.

Outcome. A single upsert_node_from_item() call produces a fully-processed graph — tagged, fielded, wiki-linked, triaged, zettelized — with zero orchestration code in the pipeline. Behavior changes by editing .scm files; no Python touched, no restart required.

Migration path. from runex.ontology import register, run_by_name is gone; use Engine.dispatch or Ontology.dispatch. Machines and wiki extraction are now .scm in config/ontology/. The old Python framework classes are deleted. store.resolve_wiki_links() and store.sync_all_wiki_links() are gone; wiki extraction is reactive and automatic once wiki_links.scm is loaded.

Last updated on