Todos los paquetes

pm-vcs

A general version control system written from scratch on the pm SDK for arbitrary files and structured records, with stable file and change identities, native PM attribution, its own object store, refs, merge, operation log and bundles, and no Git dependency.

Instalar

Este paquete aún no se ha publicado, así que todavía no se puede instalar.

README

A version control system written from scratch, in TypeScript, on the pm SDK — for projects whose most important content is structured, not textual.

Not a git wrapper. Not a helper around git merge. pm-vcs has its own content-addressed object store, its own refs, its own index, its own diff, its own three-way merge, its own operation log and its own distribution format. No code path in the engine shells out to git, and nothing it writes is git-compatible.

The scope is the whole system — the storage and history model of git, the rewriting and operation-log model of jujutsu, and the conviction Fossil, Forgejo and lore share that a project's own metadata and its changes under review belong inside the repository rather than in a service beside it. All of it written from scratch on the pm SDK, around one primitive git does not have: the record.

ARCHITECTURE.md is the design document — every decision, why it was made, and an honest per-capability statement of what is shipped and what is still ahead. Read it before the command table if you want to know what this actually is. LORE.md is the first-party-source research mapping Epic Games' general, binary-first Lore VCS into pm-vcs. It is not about the kernel's email archive of the same name.

npm install --save-dev pm-vcs     # or: bun add -d pm-vcs
pm package install pm-vcs
pm vcs init

Why write another version control system

Git is superb at what it was built for. It was built for source files, and its merge is a merge of lines.

Almost everything a team's context actually lives in is not lines. A tracker item is a record: a status, a priority, a set of tags, an assignee, an append-only history. Two agents working the same project on two branches routinely touch two different fields of one item. That is not a conflict in any meaningful sense — but whether git can merge it depends on how many lines apart the fields happen to be serialized, which is an accident of the file format rather than a property of the change.

The ecosystem's existing answer is to bolt merge drivers onto git so that .toon items and .jsonl history get field-aware handling. That works, and pm-cli ships it. But it is a patch over a mismatch: the storage layer still thinks in lines, and every tool downstream has to be told not to.

pm-vcs removes the mismatch instead of patching it. A record is a first-class object kind alongside blobs, trees and commits. Merging two revisions of a record compares fields, not lines. Two agents editing two different fields converge, always, with no line-merge hazard anywhere in the pipeline — because there is no line merge in the pipeline.

This is the philosophy the rest of the ecosystem is built on — project management = context management — pushed down into the storage layer. If context is the thing worth preserving, the system that versions it should understand its shape.

Where it sits among the systems it learns from

versions merge unit pm-vcs takes
git file trees lines content addressing, the commit DAG, the object model
Subversion file trees lines nothing structural; a cautionary tale about central state
Jujutsu file trees lines the operation log, and undo as a first-class verb
Fossil / Forgejo files + project metadata lines / a database the conviction that project metadata belongs inside the VCS
Epic Games Lore arbitrary binary files and repository-scale fragments storage/view dependent binary-first storage, stable identity, sparse instances and resumable publication
pm-vcs file trees and records fields, then lines —

Jujutsu's operation log is the best idea in modern version control for agents: every command is recorded, so "put it back" is one verb rather than a reasoning problem about which id was the old tip. pm-vcs has it. Fossil is right that issues and project state belong in the repository rather than beside it — pm-vcs goes further and makes them a native object kind rather than a table in an attached database.


Designed for many agents on one project

Every decision below exists because the expected user is several autonomous agents working concurrently, not one person at a keyboard.

Ref updates are compare-and-swap under an exclusive lock. Two agents that read the same branch tip and both commit on top of it cannot both win. The second write fails, loudly, and says to re-read and retry. A last-write-wins ref update is precisely how one agent's commit disappears with nothing anywhere reporting it.

Identical concurrent edits are agreement, not conflict. Two agents reaching the same conclusion independently is the most common way an automated merge wastes attention. pm-vcs merges it cleanly.

A conflict is scoped to the thing that conflicted. A genuine scalar disagreement on one field conflicts on that field, and every other field of that record still merges. A document does not become unreadable because one value disagreed. File-identity disagreements are reported the same way: independently added paths and divergent renames produce an identity conflict with a deterministic, structurally valid merged tree instead of aborting.

Merge bases are computed, not assumed. Two branches that have already merged each other once have several minimal common ancestors. pm-vcs finds all of them and builds a virtual base. Picking one arbitrarily is how a criss-cross merge silently reintroduces a change that was already reverted.

Nothing is destroyed before it is known to succeed. A switch that would overwrite an uncommitted edit refuses before writing anything. A half-applied switch leaves an agent with a tree matching no commit and no way to describe what it has.

Objects are never removed, so undo is always possible. Rewinding a ref makes a commit unreachable, not absent.


Commands

The repository

pm vcs init --record-path '.agents/pm/**/*.toon' --set-field 'tags:set,notes:sequence,updated_at:timestamp'
pm vcs add
pm vcs commit --message "Close the deployment item"
pm vcs log --limit 10
pm vcs diff main feature
command what it does
pm vcs init Create a repository. --record-path declares which paths hold structured records; --set-field declares how their fields merge.
pm vcs status The three-way difference between HEAD, the index and the working tree. Stable indexed paths are checked from stat metadata without re-reading content; racy timestamp-window entries are always hashed.
pm vcs add [paths…] Stage paths, or everything. A path that no longer exists stages as a deletion.
pm vcs commit --message Record the index. --item id[,id...] stores validated PM work associations. Refuses an empty commit unless --allow-empty.
pm vcs log [rev] First-parent history, newest first.
pm vcs diff [from] [to] Unified diff between two revisions' trees.
pm vcs branch [name] List, create (--at) or delete (--delete) branches. --remotes adds each remote-tracking branch with its ahead/behind counts against HEAD.
pm vcs switch <rev> Move HEAD and update the working tree. Refuses rather than overwrite uncommitted work.
pm vcs merge <rev> Three-way merge. --fail-on-conflict to gate CI.
pm vcs tag [name] List or create tags.
pm vcs undo Reverse a recorded operation, refs and working tree together.
pm vcs oplog Every operation, with the refs it moved and where from.
pm vcs export <file> Write refs and their history to a bundle.
pm vcs import <file> Import a bundle, verifying every object against its own id.
pm vcs remote [name] [url] List remotes, add one, or remove one (--remove).
pm vcs clone <url> [dir] Create a repository from another one, adopting its record configuration.
pm vcs fetch [remote] Bring a remote's branches onto refs/remotes/<remote>/. Touches no local branch.
pm vcs push [remote] Send branches (--branch). Refuses a non-fast-forward unless --force.
pm vcs verify Re-read every reachable object and check it against its id.
pm vcs trace <path-or-file-id> Trace one logical file across edits, moves, copies and deletion.
pm vcs files <item-id> Resolve a PM item's linked arbitrary files to stable identities and changes.
pm vcs changes <item-id> Report explicit and file-derived stable ChangeIds for a PM item.
pm vcs items [from..to] Report PM items explicitly associated with, or linked to files changed by, native revisions.

Git interoperability

pm items today mostly live in git repositories, and pm-vcs can reason about that without being git. These three are the only commands that touch git:

command what it does
pm vcs git preflight Can this git checkout merge tracker data field-aware?
pm vcs git preview <ref> What would merging <ref> do to tracker data, per item and per field?
pm vcs git items <range> Which pm items did this commit range create, modify and close?

The part that matters: per-field merge

Declare which paths hold records and how their fields reconcile:

pm vcs init --record-path 'items/*.json' --set-field 'tags:set,history:sequence,updated_at:timestamp'

Two agents, on two branches, edit the same item:

// base
{ "id": "pm-1", "title": "Ship the thing", "status": "open",
  "priority": 3, "tags": ["area:vcs"], "history": ["created"] }

// agent A closes it
{ …, "status": "closed", "history": ["created", "closed by A"] }

// agent B retitles and reprioritises it
{ …, "title": "Ship the thing (revised)", "priority": 1,
      "tags": ["area:vcs", "urgent"], "history": ["created", "retitled by B"] }
$ pm vcs merge agent-b
merge:
  kind: "merged"
  clean: true
  conflicts: []
{ "history": ["created", "closed by A", "retitled by B"],
  "id": "pm-1", "priority": 1, "status": "closed",
  "tags": ["area:vcs", "urgent"], "title": "Ship the thing (revised)" }

Every change survived. No conflict markers exist anywhere, because no line merge ran.

Field strategies

strategy rule
scalar (default) One side changed it, that side wins. Both changed it differently, it conflicts — alone.
set Both sides' members survive, duplicates collapse, order normalised.
sequence Append-only. Both sides' additions survive in deterministic order.
timestamp Both sides must provide valid timestamps; the chronologically latest value wins.

A genuine disagreement still conflicts, and says exactly what disagreed:

$ pm vcs merge y --fail-on-conflict
Error: Merging y left 1 conflict(s): items/pm-1.json (status)

priority, tags and history merged. Only status is unresolved.

Records are canonicalised on the way in

A configured record path is parsed and re-encoded canonically when staged, so two agents whose editors disagree about key order or indentation produce one object id. A file whose formatting moved does not register as changed. This is not cosmetic: it is what keeps a reformat from presenting as a conflict.


How it is built

engine/objects.ts    SHA-256 content addressing over `<type> <byteLength>\0<payload>`.
                     zlib loose objects, temp-file-and-rename writes. Reads re-hash rather
                     than trusting the filename, so silent corruption is detectable.
engine/model.ts      Canonical encodings for trees, commits and records. Trees sort by byte
                     order, never locale collation — a tree must not hash two ways under two
                     LANG settings.
engine/refs.ts       Branches, tags, HEAD (symbolic or detached), compare-and-swap updates.
engine/diff.ts       Myers O(ND) line diff, hunk grouping, unified rendering.
engine/merge.ts      Commit-DAG reachability, minimal merge bases, diff3 content merge.
engine/records.ts    Per-field record merge; append-only log union.
engine/attribution.ts Stable file identities and PM item ↔ file/change history joins.
engine/worktree.ts   Versioned index with racy-clean-safe stat caching, working-tree scan,
                     untracked-safe tree materialization, and three-way status.
engine/ignore.ts     An always-ignored set plus `.pmvcsignore`.
engine/config.ts     Which paths hold records and how their fields merge — per repository,
                     so two agents cannot disagree about it.
engine/oplog.ts      Append-only operation log; `undo`.
engine/bundle.ts     Export and import, verifying every object against its own id.
engine/repo.ts       The porcelain.

Repository layout

.pmvcs/
  format          repository format version
  HEAD            "ref: refs/heads/main", or a raw object id when detached
  config.json     record paths and field strategies
  index           the staging area; updated atomically under index.lock
  refs/heads/*    branch tips
  refs/tags/*
  objects/ab/cd…  zlib-deflated, content-addressed by SHA-256
  oplog.jsonl     append-only operation log

The four object kinds

kind holds
blob raw bytes
tree sorted entries with mode, object id, stable file identity and copy provenance
commit a tree, parents, author, committer, message, stable change id and PM item associations
record a structured document as canonically ordered fields

record is the one git does not have, and the reason this system exists.


Safety

pm-vcs is usually initialised inside an existing checkout, so it treats the working tree as something it shares rather than owns.

.git, .hg, .svn, .bzr, _darcs, CVS and node_modules are always ignored and cannot be re-included — not by .pmvcsignore, and not by a commit whose tree names a path inside them. Materialization filters the target tree, so history recorded before the rules existed still cannot write over another tool's state. This is pinned by a test that builds a deliberately hostile commit naming .git/HEAD and asserts it materializes to nothing.

.pmvcsignore adds project patterns with gitignore-like semantics: # comments, a trailing slash for a directory, a pattern without / matching by basename at any depth, and ! to re-include. Staging an ignored path by name is refused rather than skipped — staging nothing while reporting success is how a commit ends up missing a file.


Distribution

pm vcs clone /srv/project work              # adopts the source's record configuration
pm vcs remote upstream ../other-checkout
pm vcs fetch upstream                       # lands on refs/remotes/upstream/*
pm vcs branch --remotes                     # what it fetched, and how far HEAD has moved
pm vcs merge upstream/main                  # the shorthand resolves
pm vcs push --branch feature

A fetched branch is reachable two ways: by its full refs/remotes/upstream/main, and by the upstream/main shorthand any command taking a revision accepts. A local branch or tag of the same name always wins that shorthand — the remote-tracking namespace is searched last — so adding it cannot retarget a name a repository already uses.

branch --remotes reports each tracking branch with how many commits HEAD is ahead of it and behind it. Both numbers are needed to act: behind 0 is a push that will be accepted, ahead 0 is a fast-forward, and two non-zero counts are a divergence to merge first. An agent that skips this learns the same fact from a push refusal instead — one round trip to a remote to discover something both sides already knew. An unborn HEAD omits both counts rather than reporting zero.

Three properties hold, and each exists because the alternative loses an agent's work:

  • A fetch cannot move a local branch. Branches it learns land under refs/remotes/. Importing a bundle wholesale would move the receiver's main onto the sender's, because a bundle names refs as the sender knows them — so the receiving agent's commits would become reachable from nothing, with nothing in the output saying so. Tags are the exception and keep their own names under refs/tags, since a tag identifies a point in history rather than one repository's view of a branch; one the receiver already uses at a different value is reported in conflictingTags rather than moved.
  • A push cannot discard a commit the remote has. The receiving side requires its current tip to be an ancestor of what is being pushed, and --force is the only way past it.
  • That check is atomic. Every ref lands as a compare-and-swap against the value the pusher observed, so a push decided against a stale advertisement fails instead of landing on top of whatever arrived in between.

Only missing objects move. The fetching side offers every commit it holds — its branches, its tags, and the tracking refs of every remote — the receiving side keeps the ones it recognises, and everything reachable from that agreed set is excluded from the transfer and declared as a prerequisite instead.

clone adopts the source's record configuration before it writes a single object. A clone that started from the defaults would store the same paths as blobs rather than records and merge them line by line: two repositories sharing commit ids while disagreeing about what those commits mean, each internally consistent and therefore undetectable.

The transport is an interface. The implementation that ships reaches a repository through the filesystem, which is the case that occurs today — several agents, several working trees, one host. A served implementation lands with the forge in Phase 5, when there is a repository service for it to speak to.

Bundles remain, for the times a file is the transport you have:

pm vcs export /tmp/work.bundle --ref refs/heads/feature
pm vcs import /tmp/work.bundle            # in another repository

Import reproduces identical commit ids, verifies every object against its own hash before storing it, and fails whole — naming the missing ids — when a bundle depends on history the receiver does not have.


Roadmap

Everything below is tracked as an epic in this repository's own tracker, under pm-vcs-tr2a. The per-capability status table lives in ARCHITECTURE.md §11.

phase what it adds epic
2 Change identities that survive rewriting, plus describe, rebase, squash, split, cherry-pick, revert, reset, restore, and automatic descendant rebase pm-vcs-ijj7
3 ✅ Named remotes, remote-tracking refs, and clone/fetch/push over a transport with reachability-based negotiation pm-vcs-wm40
4 pm-vcs versioning its own source, with a CI gate proving the tracked history matches the source tree byte for byte pm-vcs-390t
5 The forge: a patch series as an object kind, review state as records, and a served repository pm-vcs-5h6j
6 Scale: packed storage, a reachability index, shallow and partial history, and garbage collection bounded by the operation log pm-vcs-b7cb

Requirements

  • Node.js ≥ 22.18 (engines), tested on 22 and 26
  • @unbrained/pm-cli ≥ 2026.8.1 (peer dependency and host-bound SDK runtime)
  • Works under npm/npx and bun/bunx
  • No runtime dependencies beyond the Node standard library

Development

npm ci
npm run check        # typecheck, strict lint, and zero-duplication gate
npm run docstring    # every production API declaration is documented
npm run coverage     # all production TypeScript at 100/100/100/100
npm run release:check

The c8 coverage gate instruments every production TypeScript file with --all and requires 100% statements, branches, functions, and lines. A never-loaded module therefore fails instead of disappearing from the report. ESLint rejects unsafe or non-erasable TypeScript syntax, the docstring gate covers production declarations, and jscpd permits no production clone.

Tests run against real repositories and real filesystems. There are no mocks and no hand-built api doubles: this is filesystem and merge code, and a fake of either would only assert against the suite's own assumptions.

Known upstream issues this package works around

  • unbraind/pm-cli#825 — FlagDefinition's index signature defeats excess-property checking, so a misnamed flag field type-checks and then aborts activation, dropping every later sibling command. Flags here use long / value_name / value_type.
  • unbraind/pm-cli#826 — an extension command cannot both return a structured report and exit non-zero, and a thrown handler error's remediation is replaced by a generic line. Gate commands therefore throw, with the remediation folded into the message.
  • unbraind/pm-cli#832 — pm-cli bundles a private pm-vcs exemplar that claims the vcs alias and registers vcs log / vcs merge, with no way for a package author to detect the collision. Enabling both in one workspace is not supported.

License

MIT

Del README del paquete en GitHub. Ver en GitHub