Ursa:把 AI 变成点对点,Ursa Minor 训练模型、Ursa Major 导入微调
alexandrapaiz/Ursa
Alexandra Systems 旗下 Ursa 提出"点对点 AI"双产品架构:Ursa Minor 用分散知识训练模型并获取全部收入,Ursa Major 把用户的微调导入任意模型且永久不收费,两者互为前提。
Turn AI peer-to-peer.
Ursa is one parent with two products. Ursa Minor trains models on the world's dispersed knowledge and earns all the revenue. Ursa Major imports your tuning into every model and earns none, permanently, by design. Neither works without the other.
The full mission text and the principles that bind the project are in
docs/vision.md.
Ursa is a subcompany of Alexandra Systems Company, which generalizes
operations across the portfolio; its standards, seats and chair reach
into this repository by design (see CLAUDE.md, "The holding company").
The principles
Four theses, taken from the Ursa Minor site, guide every decision here.
- Distributed intelligence. The information a decision needs is dispersed across many individuals. No single observer holds it whole.
- Discovery of intelligence. Generation discovers facts that would otherwise stay unknown. If the result could be specified beforehand, there would be nothing to discover.
- Tacit intelligence. Much of what people know cannot be stated as rules. It shows up only in action, applied to a particular case.
- Pretence of intelligence. Claims of information sufficient for central assessment always overreach. Systems built on them conform to the measure, not the world.
A central grader breaks the first. A benchmark that demands the spec up front breaks the second. A stated-preference survey breaks the third. A metric the system starts conforming to breaks the fourth.
What the product does
Every session where a person steers an AI through a piece of work is a fine-tuning session. The person corrects, the agent adapts, and the person often does not know what they want until they see it. Ursa Major reads that session after the fact and reverse-engineers the whys. The finished work and the person's own satisfaction supply the grade, never the instructions, because instructions mid-session are guesses.
The core data artifact is the outcome record: a finished piece of work joined backward to every model generation that fed it, with each span of the final text classified by what happened to it.
| Class | Meaning |
|---|---|
survived_verbatim |
generated and kept unchanged |
survived_mutated |
kept but edited. The edit is the correction. |
generated_deleted |
produced and thrown away |
no_generation_provenance |
in the finished work but traceable to no generation. The model was never in the running. |
Every record also names what kind of finished thing it is about,
because finished work is not only chat. A record's artifact.kind is
chat when the conversation is the product, repo when the product is
versioned source, hosted when it is served at a URL, and visual when
the person accepted or corrected it by eye. When a rendered state exists,
artifact.renderRef points at it, a deploy URL or a screenshot path, so
a lab reading the record can go and look at the thing that was actually
judged. ursa run fills repo on every run and upgrades it to hosted
when the episode's own commit names a domain.
A class is a verdict taken at one instant: the first commit in which
the person edited the agent's output. When the project's git history
continues past that commit, every surviving span also carries how
long it lasted, because text kept at the time and removed by the work
three commits later is not the same as text that was kept.
| Fate | Meaning |
|---|---|
durable |
still there at the newest commit |
eroded |
partly removed by later work |
decayed |
gone. The span's own class is a false positive. |
untested |
nothing came after it, so nothing is claimed |
The record's decayRate is the share of its own "the user kept it"
verdict that later work overturned, scored against the person's own
prose in the same files so a volatile repo is not read as a bad model.
How it is measured is in
docs/design/span-lifespan.md.
Two capture paths feed it. Session logs carry the trace, which is
where the fine-tuning churns. Git commit pairs carry the label, a
generated commit followed by the person's edit of it. A full record
joins both. The reasoning is in
docs/beyond-preference-pairs.md.
Architecture
Two layers, drawn together because neither explains the company alone. The product layer is what a user runs on their own machine. The org layer is the set of agent seats that build the product, which run in GitHub Actions and write only through pull requests the owner merges. Every node below is a real file or a real store in this repository, and every edge carries the actual type or file that crosses it.
The one edge worth reading twice is the dotted one. It is the only path out of the user's machine, it carries derived signal and never a raw record, and it is the constraint the whole business rests on.
flowchart TB
subgraph ORG["Org layer — the agent seats that build it"]
direction TB
WF["<code>.github/workflows/agent-*.yml</code><br/>cron plus workflow_dispatch"]
CH["<code>prompts/*-agent.md</code><br/>one charter per seat"]
PR["a branch and one pull request"]
OWN{{"the owner's merge<br/>the only authority"}}
MEM[("<code>docs/agents/</code><br/>incidents.md, learning-log.md,<br/>org-chart.md, pending-workflow-changes.md")]
WF -->|"prompt plus claude_args"| CH
CH -->|"commits"| PR
PR --> OWN
OWN -->|"applies the change"| MEM
MEM -->|"read at the start of every run,<br/>the memory a fresh session lacks"| CH
end
subgraph PRODUCT["Product layer — the two products"]
direction TB
GIT["the user's own machine:<br/>a project's git history"]
PF["<code>src/pairfinder.ts</code>"]
EP["<code>src/episodes.ts</code>"]
RS["<code>src/resolve.ts</code>"]
SG["<code>src/signals.ts</code>"]
ST["<code>src/store.ts</code>"]
REC[("<code><project>/.ursa/records/*.json</code><br/>never leaves the machine")]
DI["<code>src/tuning/distill.ts</code>"]
TUN[("<code><project>/.ursa/tuning.json</code>")]
EX["<code>src/tuning/export.ts</code>"]
MD["<code>tuning.md</code><br/>pasted into any model"]
MINOR["Ursa Minor aggregation<br/>not built yet"]
DEP["<code>src/deploy.ts</code>"]
GIT -->|"<code>CNAME</code>, <code>package.json</code>, <code>vercel.json</code><br/>at the episode's final commit"| DEP
DEP -->|"DeployDetection, which becomes<br/>OutcomeRecord.artifact"| RS
GIT -->|"commits and Co-Authored-By trailers"| PF
PF -->|"CommitPair[]"| EP
EP -->|"Episode[]"| RS
RS -->|"OutcomeRecord"| SG
LP["<code>src/loops.ts</code>"]
SG -->|"OutcomeRecord, when a chat trace is present"| LP
LP -->|"CorrectionLoop[], RegressionEvent[], OneShotCorrection[]"| SG
SG -->|"OutcomeRecord plus LabSignals"| ST
ST --> REC
REC -->|"OutcomeRecord"| DI
DI -->|"TuningRecord"| TUN
TUN -->|"TuningRecord"| EX
EX --> MD
SG -.->|"derived signal only, on consent,<br/>after redaction. Never a raw record."| MINOR
end
OWN ==>|"merged edits to <code>ursa-major/</code>"| GIT
Loading
What is built
ursa-major/ is a TypeScript package. It has no daemon, no watcher,
and no timer. You select a finished project and launch a run.
cd ursa-major npm install npm test # 505 tests, 4 skipped # Read a project's git history for generated-then-edited commit pairs, # resolve each pair into an outcome record under <project>/.ursa/, # and print a summary that leads with what survived. npx tsx src/bin/ursa.ts run <projectPath> --declare unsatisfied # Distill one record's corrections into evidence-backed tuning units. # Runs locally through the claude CLI on your own subscription. npx tsx src/tuning/cli.ts distill --record <project>/.ursa/records/<id>.json \ --tuning <project>/.ursa/tuning.json --model sonnet # Render the tuning store as a block any model can read. npx tsx src/tuning/cli.ts export --tuning <project>/.ursa/tuning.json --out tuning.md # Read the HQ before working: the rules that apply to these files, the # nearest prior corrections, and the guardrails already learned. npx tsx src/hq/cli.ts brief <projectPath> --files src/app/page.tsx # Or serve both of those to a model over MCP, so nothing is pasted. npx tsx src/mcp/cli.ts <projectPath> # Which packages the stdio MCP surface actually loads, as opposed to # which ones are installed. Prints one JSON object. node src/mcp/loaded-packages.mjs
--declare records your own verdict on the project's current state.
Without it the record says undeclared. Retention is never treated as
acceptance; only the owner's declaration is.
The older session-log path is src/cli.ts, which takes --final and
--sessions flags and produces the same record plus a self-contained
HTML viewer. It defaults to artifact.kind: 'chat', and takes
--artifact-kind and --render-ref when the finished thing was
something you looked at rather than something you read:
npx tsx src/cli.ts --id ursa-minor-site \
--final ~/Desktop/ursa-minor-site --conversations ./conversations \
--artifact-kind visual --render-ref screenshots/hero-accepted.png \
--out ./outThe second launch mode is the GitHub Action. Copy one workflow file into a repository where an AI agent commits, and every merged pull request from then on resolves itself: outcome records on that repository's own runner, and one comment on the pull request carrying the same five fields in the same order every time, zeros included.
# the whole consumer side mkdir -p .github/workflows curl -fsSL https://raw.githubusercontent.com/alexandrapaiz/Ursa/main/ursa-major/examples/resolve-on-merge.yml \ -o .github/workflows/ursa-resolve.yml # what the Action runs on the runner: one committed file, no install node ursa-major/dist/ursa.cjs ci "$GITHUB_WORKSPACE" \ --repo "$GITHUB_REPOSITORY" --event "$GITHUB_EVENT_PATH"
It installs nothing, because ursa-major/dist/ursa.cjs is one committed
CommonJS file the runner's own node executes. With no model credential
on the runner the distiller runs in CI mode: the records are still
resolved, no interpretation pass runs, and the tuning delta is reported
as a zero with the reason attached, so a zero from an absent model never
reads as a model finding nothing. A 👍 on the run comment is read on the
next run and recorded as a declared acceptance. No reaction records
nothing, because retention is never acceptance. The design is
docs/design/resolver-action.md, and the test that
holds the two launches to the same record is
docs/design/launch-parity.md.
| Module | Job |
|---|---|
src/pairfinder.ts |
walks git history, identifies agent commits by their Co-Authored-By trailer or author pattern, pairs each with the next human edit, and reads a file's contents at any commit in three states: present, absent from that tree, or unreadable from this clone. Refuses the pair rather than guessing when the edit is too far away in commits or in hours, when another generation rewrote the same file first, when the edit sits on a branch the generation never reached, or when the "generation" is a merge commit, and reports every refusal it made (docs/design/pairing-window.md) |
src/episodes.ts |
one episode per commit pair; boundaries are explicit, never inferred from idle time |
src/resolve.ts |
joins final text to generations and classifies every span |
src/signals.ts |
derives correction signals from a record; carries the owner's declaration. Two stages: a git commit pair yields one-shot corrections from its edited spans, a chat trace yields loops and regressions through src/loops.ts |
src/loops.ts |
auto-detects correction loops, regressions and one-shot corrections from the user's own messages in a chat trace, with no hand annotation (docs/design/trace-stage-loops.md) |
src/lifespan.ts |
walks the commits after an episode closed and records how long each span actually lasted |
src/deletion.ts |
decides what destroyed a span that is gone from the final text: the person, who read it and did not keep it, a merge commit, which overwrote it mechanically and carries no correction signal, or nothing nameable, when the clone cannot read the boundary that would settle it (docs/design/unknown-deletion-cause.md) |
src/corroborate.ts |
asks git whether a span's text already existed outside the generation's line of descent, so a high similarity score is not mistaken for proof that the person edited this generation. A span found verbatim in a commit that is not a descendant of the generation loses the survived_mutated label and the word-level diff that goes with it, because text that already existed elsewhere was not composed here (docs/design/corroborated-descent.md) |
src/vendored.ts |
refuses a whole file before any span of it is read: when the finished file is byte-for-byte a copy of content that exists outside the generation's line of descent, it arrived from elsewhere (a vendored standard, a rebase, a cherry-pick, a copied template) and no character of it was composed in this work, so it is left out of the record rather than reported as text the person kept. On a clone of this repository it removed 21,656 characters of one rebased document from the survival figure, and the record says so: the refusal is written into the record as an exclusions entry naming the path, the commit the content came from and the 30,287 characters that left the figures, so a buyer can audit what the run declined to claim (docs/design/vendored-paths.md, docs/design/record-exclusions.md) |
src/adapters/github-pr.ts |
reads one pull request as a source of corrections, for repositories where the work is reviewed in pull requests rather than edited on the default branch, and reports the merges that sat between a generation and its closure so src/deletion.ts can judge them (docs/design/pr-path-merge-attribution.md) |
src/store.ts |
writes records and the episode index to <project>/.ursa/ |
src/invariants.ts |
checks a record against itself: fifteen bounds a record must satisfy, twelve of them arithmetic or structural and reported with both sides of the number they broke, one that re-reads every excerpt a signal quotes against the raw text it names, one that checks the record did not keep a mutation label it had already found the evidence against, and one that checks it did not classify a file it had already declared an import. The count is BOUND_COUNT, derived from the bounds themselves, because this line said thirteen for four days after the fifteenth was added. both launches exit non-zero when one is violated, after writing the records, because an impossible record is still the evidence, and the CI launch's run comment says in its detail block how far to trust the five fields above it (docs/design/generated-denominator.md, docs/design/signal-grounding.md, docs/design/corroborated-descent.md, docs/design/record-exclusions.md) |
src/launch.ts |
the one definition of the three decisions each launch makes around the resolve step, shared by src/bin/ursa.ts and src/ci/run.ts so the two cannot answer them differently: whether a resolved record clears the --min-chars floor (a refusal-only record has no generation side to measure, so it is exempt), which episodes the user erased (both launches rebuild episodes from git history, so a launch that skips the consent record undoes ursa forget), and whether every record just written satisfies all fifteen bounds. All three had drifted apart between the two launches, found by resolving one fixture repository both ways and diffing the records (docs/design/launch-parity.md) |
src/intervals.ts |
the one definition of how many distinct characters a set of extents covers, shared by src/stats.ts and src/invariants.ts so a generated sentence reused in two places is counted once where it should be and twice where it should be |
src/text.ts |
the one definition of a quoted, truncated excerpt, shared by src/signals.ts and src/loops.ts, plus the predicate src/invariants.ts uses to decide whether a quote really appears in the text it claims to come from |
src/tuning/ |
distillation into rules and cases, deterministic merge with revocation tombstones, export |
src/hq/ |
the same store read forwards instead of backwards. Given a domain tag and the files an agent is about to touch, get_briefing returns the rules that apply, the nearest prior correction loops with the person's own words, and the guardrails they had to state after an agent went further than asked. Every unit carries where it came from, and the payload says in writing that it is evidence rather than orders (docs/design/hq-briefing.md, docs/design/semantic-retrieval.md) |
src/mcp/ |
serves the two reads above over the Model Context Protocol on stdio, so a model fetches them instead of the person pasting them. The resource ursa://tuning/current returns the exported tuning block and the tool get_briefing returns the briefing. Reads only, and a revoked rule reaches neither. Every briefing reports its own size in characters, so an agent can set its ceilings against the context it has left rather than guessing (docs/design/hq-mcp.md) |
src/mcp/loaded-packages.mjs |
which packages a module graph actually compiles, measured by recording V8's own script-compilation events rather than reasoned about. It exists because the MCP SDK's remote transports bring a web framework, a rate limiter and a JSON Web Token library into the production dependency tree that the stdio surface never executes, and the dependency gate audits all of them (docs/design/hq-mcp.md §10.2) |
src/deploy.ts |
reads a commit's own CNAME, package.json homepage or vercel.json alias to find the URL the finished work is served from, which is what makes a record hosted rather than repo |
src/ci/ |
the CI launch: the merge window from the event payload, the five-field run comment, the thumbs-up read, CI mode for the distiller |
action.yml, dist/ursa.cjs |
the reusable GitHub Action and the one-file bundle it runs |
.ursa/ belongs in the target project's .gitignore. Raw records and
the tuning store never leave the machine they were made on.
Trials
| Trial | Subject | Result |
|---|---|---|
| n=1 | Ursa Major run over the building of the Ursa Minor site, from Claude Code session logs | 193 generations, 79.2% of the site survived verbatim, 82.1% of generated characters deleted, four correction loops and three regressions hand-annotated |
| n=2 | the full alexandria repo, 374 commits, from git commit pairs | 19 records from one command, 11 carrying the owner's corrections; the digest is the most-corrected artifact. Owner-declared unsatisfied, so survived text is not endorsed text. |
Raw trial records contain verbatim prompts and are kept in a private repository. Redacted records return here only after the security seat's pass and the owner's sign-off.
Plan
docs/design/product-plan.md is the
plan of record: architecture with real signatures, exact commands,
on-disk layout, tooling with versions and reasons, milestones with
acceptance tests, the scale architecture, the cross-tool matrix, and
the encouragement mechanics. Where a presentation and the plan
disagree, the plan wins.
Other governance files:
docs/okrs/2026-q4.md, the quarter's objectives and key resultsdocs/decisions.md, architecture decision recordsdocs/ideas.md, the ledger of proposed workdocs/agents/incidents.md, blameless postmortemsdocs/agents/learning-log.md, what each ExO run observed and what the next one must check firstdocs/agents/org-chart.md, the seats, their cadences, and the governance-cycle trackerdocs/agents/pending-workflow-changes.md, workflow edits specified for the owner to apply, because the runner's token cannot write themdocs/allhands/, meeting minutesdocs/presentations/, slide sources
Operations
Ursa is a portfolio product of
Alexandra Systems Company
and runs on its agent-seat model. Charters live in prompts/,
workflows in .github/workflows/, and the standards in
docs/standards/. The OKR, PM, and ExO seats are active on a schedule.
The builder seats are dormant until one full governance cycle has been
merged. The owner's merge is the only authority. See
docs/agents/org-chart.md.
Constraints that do not move
- Ursa Major never monetizes the user directly.
- The user can always see, edit, revoke, and delete what has been inferred about them.
- Raw processing happens on the user's machine. Raw data never touches Ursa-operated compute.
- The software is not the moat. The consenting constellation of users is.
来源:GitHub · github.com