Project

textus

0.0
The project is in a healthy, maintained state
A coordination space for humans, AI, and automation. Durable, multi-writer project memory where each actor writes into its own lane, proposals cross a review queue, and every change is audited.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 13.0
~> 3.13

Runtime

>= 3.0
>= 5.0
>= 3.2
~> 2.6
~> 1.13
~> 0.20
~> 2.0
~> 5.0
 Project Readme

textus

CI Gem Version Gem Downloads Ruby License

A coordination space for humans, AI, and automation. Your agent forgets between sessions; your notes and CLAUDE.md get edited by whoever ran last; nobody can reconstruct who wrote what. textus is durable, multi-writer memory that stays current and survives the model, the session, and the vendor — you keep your space, agents keep theirs, automation keeps external data fresh, and every change crosses a review queue and an audit log.

textus is Latin for "the fabric a text is woven from" — same root as context, from con-texere, "to weave together."

The idea

Three actors write to your repo today:

  • Humans — you, your team. Authoritative on identity, decisions, voice.
  • Agents — Claude, Cursor, custom assistants. Smart, fast, forgetful, and not always right.
  • Automation — cron jobs, fetchers, CI. Bring outside data in and compile published artifacts.
flowchart LR
    subgraph writers["writers — who can write"]
        direction TB
        human(["human"])
        agent(["agent"])
        automation(["automation"])
    end

    human -->|author| knowledge["knowledge<br/>(canon)"]
    agent -->|keep| scratchpad["scratchpad<br/>(workspace)"]
    agent -->|propose| proposals["proposals<br/>(queue)"]
    automation -->|drain| artifacts["artifacts<br/>(machine)"]
    human -->|ingest| raw["raw<br/>(intake)"]
    agent -->|ingest| raw
    automation -->|ingest| raw

    proposals ==>|human accept| knowledge
    knowledge -.->|projection source| artifacts

    classDef actor fill:#238636,stroke:#2ea043,color:#fff;
    classDef gate fill:#9e6a03,stroke:#bb8009,color:#fff;
    classDef anchor fill:#1f6feb,stroke:#388bfd,color:#fff;
    classDef intake fill:#6e40c9,stroke:#8b5cf6,color:#fff;
    class human,agent,automation actor;
    class proposals gate;
    class knowledge anchor;
    class raw intake;
Loading

Each actor writes only into its own lane; low-trust input climbs to authoritative lanes only by passing a guarded transition (an agent's proposal needs a human accept). Colour legend: green = writers · amber = the review gate (proposals) · blue = the trust anchor (knowledge) · purple = write-once intake (raw).

The point of those lanes is to build context you can trust. Place each lane on two axes — how durable it is, and how much you can rely on it without review — and the value shows up as a climb: the high-trust corner (durable and authoritative = knowledge) is the one place nothing is written directly. It's earned by crossing the accept gate.

                       LOW TRUST                     HIGH TRUST
                      (unreviewed)                (authoritative)
              ┌──────────────────────────┬───────────────────────────────┐
DURABLE       │  scratchpad                │  knowledge  ★ the goal        │
(kept)        │  agent's working truth   │  canon — a human authors      │
              │  durable, but low-trust  │  here · the context you ship  │
              ├──────────────────────────┼───────────────────────────────┤
TRANSIENT     │  artifacts  (outputs)    │  proposals  (queue)           │
(staging)     │  computed, machine-made  │  a candidate, in review       │
              │  raw        (inputs)     │  ▲ climbs via human accept    │
              │  ingested, write-once    │                               │
              └──────────────────────────┴───────────────────────────────┘
                raw material ──── propose ────► a human accept lifts it to canon

Without coordination, they overwrite each other and nothing remembers why. textus gives each actor a lane — enforced at the protocol level, not by convention — routes everything they can't write directly through a proposals queue, and writes every successful change to an append-only audit log.

knowledge/   author only            — who you are, what you decide, how you sound
scratchpad/    keep only              — agent's own durable lane (bytes climb to knowledge only via propose→accept)
proposals/   propose (agent+human) — proposals waiting on a human accept
artifacts/   converge only         — machine-maintained: computed outputs + external inputs

An agent that tries to write directly into knowledge/ gets write_forbidden. It writes to proposals/ (to change authoritative content) or its own scratchpad/ (for working memory). You accept the good proposals; textus promotes them, records the move, and audits both halves. Stable per-entry uid: means a reorganization doesn't break references. A monotonic audit cursor (textus pulse --since=N) means the next session — possibly a different agent, possibly a different model — picks up exactly where the last one left off.

That's the load-bearing claim: coordination is a protocol invariant, not a library convenience.

See it in four commands

Pattern: knowledge.loop.judgment.engineering.0007-bounded-use-cases — quickstart composes role-gated actions into a single runnable flow.

gem install textus
textus init                          # creates .textus/ with lanes + schemas

# an agent proposes a change — it targets a knowledge entry, but lands in proposals/
textus propose notes.oncall --as=agent --stdin <<'JSON'
{
  "_meta": { "name": "oncall",
             "proposal": { "target_key": "knowledge.notes.oncall", "action": "put" } },
  "body": "Patrick on call.\n"
}
JSON

# you accept it — textus promotes to knowledge/ and audits the move
textus accept proposals.notes.oncall --as=human

Try the gate the other way (textus put knowledge.notes.X --as=agent) and you get write_forbidden, with the role that would be allowed named in the error. That refusal is the whole point.

Try it

  • Worked end-to-end store — the role gate (propose -> accept), drain/publish (CLAUDE.md / AGENTS.md generated from knowledge entries), schemas, ERB templates, and workflows: .textus/
  • Wire textus into Claude Code via MCP — 4 steps, ~5 minutes: docs/how-to/agents-mcp.md

Protocol, not just a gem

This Ruby gem is the reference implementation of textus/4 — a wire format and storage convention any language can speak. The protocol owns the envelope shape, the role/lane gate, the audit log format, and the key grammar. The gem version (semver, see badge) and the protocol version (textus/4) move independently; envelopes carry the protocol field so consumers can pin to the contract, not the implementation.

A second implementation in another language would share the same .textus/ directory and the same audit log. That's deliberate.

Install

gem install textus

Or from this repo:

bundle install
bundle exec exe/textus --help

What textus init gives you

You get .textus/ with all five lane directories under data/, baseline schemas, a starter manifest, and a gitignored .state/ for disposable runtime state (the audit log, per-role cursors, produce locks). Roles declare capabilities; each lane declares a kind:, and write authority is derived from the role's capabilities crossed with the lane's kind:

roles:
  - { name: human,      can: [author, propose] }
  - { name: agent,      can: [propose, keep] }
  - { name: automation, can: [converge] }

lanes:
  - { name: knowledge, kind: canon }      # author   — canonical truth
  - { name: scratchpad,  kind: workspace }  # keep     — agent's own durable lane
  - { name: proposals, kind: queue }      # propose  — proposals awaiting accept
  - { name: artifacts, kind: machine }    # converge — computed outputs + external inputs
.textus/
  manifest.yaml          # role capabilities + lane kinds + key-to-path mapping
  schemas/               # YAML field shapes per entry family
  templates/             # ERB templates for produced entries
  workflows/             # Ruby workflow files (Textus.workflow DSL) for data acquisition
  .gitignore             # generated — ignores .state/ and any tracked:false entries
  data/                  # one dir per lane; kinds + capabilities are in the manifest above
    knowledge/           # e.g. identity (knowledge.identity.*), voice, decisions, notes
    scratchpad/
    proposals/
    artifacts/           # machine lane: computed outputs + external inputs
  .state/                  # disposable runtime state — gitignored, safe to delete (ADR 0038)
    audit/audit.log      # append-only NDJSON event ledger, every write (rotates at ~10 MB)
    cursors/<role>       # per-role pulse cursor — where `pulse --since` resumes
    locks/               # per-key produce locks + the produce mutex
    sentinels/           # publish bookkeeping (target sha) — regenerated on drain (ADR 0070)
    indexes/raw.yaml     # raw lane content-hash/URL index — regenerable cache

Manifest path: fields are relative to .textus/data/. So knowledge.notes.org.jane lives at .textus/data/knowledge/loop/notes/org/jane.md.

Read and write:

textus get knowledge.notes.org.jane
textus list --lane=knowledge
printf '%s' '{"_meta":{"name":"bob","org":"acme"},"body":"hi\n"}' \
  | textus put knowledge.notes.bob --as=human --stdin
textus drain --as=automation     # re-pull stale inputs + recompute derived outputs
textus rule list                  # show every rule block
textus audit --limit=20           # query the audit log

(All verbs return JSON envelopes; --output=json is the default and the only format.)

For a worked store — knowledge entries, a staged proposal, schemas, ERB templates, and a drain that publishes CLAUDE.md / AGENTS.md — see .textus/.

What's shipped

  • Per-entry formats & publish. format: markdown|json|yaml|text per entry; a typed publish: block (to: for file fan-out, tree: for a whole-subtree mirror) byte-copies derived files to their consumer paths. (the wire protocol spec §5.2–5.3)
  • Stable identity. Auto-minted uid: survives writes and textus key mv; reorganising never breaks references.
  • Capability × lane-kind gate. Writes carry --as=<role>; a role may write a lane iff it holds the capability the lane's kind: requires (canon->author, workspace->keep, machine->converge, queue->propose). The wrong role gets write_forbidden naming the capability needed and the roles that hold it. (the wire protocol spec §5)
  • Agent loop. textus boot orients a fresh session; textus pulse --since=N is the per-turn heartbeat (changed entries, pending proposals, index etag for catalog drift detection). (docs/how-to/agents-mcp.md)
  • MCP surface. The official mcp Ruby SDK drives the stdio JSON-RPC server; protocol version auto-negotiated up to 2025-11-25. Wire textus into Claude Code, Cursor, or any MCP host in one config block.
  • textus doctor. Health checks across schemas, workflow registrations, keys, sentinels, and the audit log.
  • raw lane and ingest verb. Write-once intake lane for external URL bookmarks, files, and binary assets. Three source kinds (url/file/asset); daily key derivation; scratchpad stub per ingest. See "Intake and ingest" section below.

CLI and lanes

Every command operates on one store, located in this order: --root <path> flag -> TEXTUS_ROOT env -> walk up from the working directory for a .textus/ (the wire protocol spec §3.1). Write verbs require --as=<role>, resolved as: --as flag -> TEXTUS_ROLE env -> .textus/role file -> default human (the wire protocol spec §5.1). Default roles: human, agent, automation (rename or add your own in the manifest's roles: block). All verbs accept --output=json and return the envelope defined in the wire protocol spec §8.

  • Full verb table — read, write, health, scaffolding — is in the wire protocol spec §9.
  • Lane semantics and the capability × lane-kind mapping live in the wire protocol spec §5, with the reference in docs/reference/lanes.md.

textus boot prints the same information for the current store: lanes, entry families with schemas, registered workflows, write flows, and the verb catalog. Run it inside a store and you get the live picture; reach for the SPEC when you want the contract.

Produce and publish

Produced entries (kind: produced) declare how they're acquired in one source: block; drain materialises them. Two built-in modes, plus workflows for custom data acquisition:

  • source: { from: external, command: "...", sources: [...] }externally managed: an out-of-band command or workflow writes the file; textus tracks staleness via declared sources.
  • source: { from: external, command: "true", sources: [] } + a workflowworkflow-driven: a Textus.workflow block (in .textus/workflows/) acquires and shapes the data on drain.

Publishing is one typed publish: block (ADR 0052/0094). Each target is either { to: path, template?: name } for a single file (optionally rendered through an ERB template) or { tree: "dir" } to mirror a whole stored subtree. Sentinels for every published file live under .textus/track/sentinels/ (git-ignored, regenerated on drain). See SPEC §5.2, §5.3, §5.12.

Templates live in .textus/templates/ as ERB files (.erb). The template receives the entry's content hash as local variables via ERB#result_with_hash. If inject_boot: true, a boot variable is also available with the live orientation context.

Workflows

textus extends through workflows — a Textus.workflow block placed in .textus/workflows/**/*.rb. Each workflow declares a trigger pattern with on, runs ordered steps, and optionally saves results to a store key or publishes to a file:

# .textus/workflows/docs/my_report.rb
Textus.workflow "my_report" do
  on "artifacts.my-report"

  step :build do |_, ctx|
    # read from knowledge, fetch external data, compute anything
    rows = ctx.container.manifest.resolver
              .enumerate(prefix: "knowledge.notes")
              .map { |r| { "key" => r[:key], "title" => r[:entry].schema } }
    { "content" => { "entries" => rows } }
  end

  save to: "artifacts.my-report"    # persist data and trigger publish targets
end

drain discovers all workflow files, triggers each by its on pattern, runs the steps, and executes save/publish targets. save to: writes results back to a store key (which cascades to the entry's file publish targets); publish to: renders a template to a file directly.

Intake and ingest

The raw lane is the inbound counterpart to artifacts: where drain materialises outbound computed outputs, ingest receives inbound external source material.

The ingest principle: prefer a reference over a copy. Store body or asset only when the content itself is the value — human-authored notes, brainstorm outputs, context you want to annotate. For everything else, the URL is enough. If the source is private or access-restricted, set access: private in source: so downstream workflows can handle it appropriately.

Three source kinds:

Kind Stores Use when
url URL reference only (body: null) Bookmarking a page, skill, or doc for later annotation
file File body text Valuable human-authored content (brainstorm notes, meeting summaries)
asset Binary at assets/raw/ Screenshots, PDFs — only when the asset itself is the artefact

Write-once — the same slug on the same day cannot be overwritten. Delete and re-ingest to replace.

# bookmark a skill reference — URL only, body stays null
textus ingest url agentskills-io-brainstorming \
  --url=https://agentskills.io/skills/brainstorming \
  --label="brainstorming skill" \
  --as=agent

# see what landed in the raw lane
textus list --lane=raw

# a scratchpad stub was created alongside — annotate it
textus get scratchpad.notes.raw

Stale produced entries are re-materialised by drain, not by reads — get is a pure read (ADR 0089).

textus drain --as=automation                  # re-materialise every stale produced entry
textus drain artifacts.feeds.skills --as=automation # scope to one prefix
textus get artifacts.feeds.skills                  # a pure read; carries a freshness verdict

Schemas (.textus/schemas/<name>.yaml) declare field shapes, per-field maintained_by: ownership, and an evolution: block (added_in, deprecated_at, migrate_from). Full contract in SPEC §5.8.

See docs/how-to/agents-mcp.md for the agent boot -> pulse loop.

Examples

.textus/ — textus as a project's own context store. Human-authored knowledge/ (project facts, runbooks, ADRs), a staged proposal showing the agent-propose / human-accept loop, schemas validating each family, ERB templates and workflows, and a drain that publishes the orientation artifact to CLAUDE.md and AGENTS.md. Includes a copy-paste adoption recipe for your own repo.

Tests

bundle exec rspec

Includes conformance fixtures A-I from SPEC §12.

Code quality

bundle exec rubocop      # lint
bundle exec rubocop -A   # lint + autocorrect

Lefthook hooks (brew bundle install then lefthook install) run rubocop on pre-commit and rspec + rubocop on pre-push. Bypass with LEFTHOOK=0 git commit ... when needed. CI runs rspec (Ruby 3.3 / 3.4) and rubocop via GitHub Actions.

License

MIT