Skip to content
unbraindPublic

About

Branch-aware merge safety for pm-cli trackers: preflight a checkout before multi-agent item work, predict a merge's per-field resolution before running it, and map a commit range onto the pm items it touched.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

136 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pm-vcs

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.

Ordinary operations retain objects, so rewinding a ref preserves history. Explicit authorized obliterate permanently removes selected payloads; undo can restore a pointer but cannot recover obliterated bytes.


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 a file or HTTP(S) remote, or remove one (--remove). remote add <name> <url> is also accepted.
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) with explicit live FileId leases. Refuses a non-fast-forward unless --force.
pm vcs lease <acquire|renew|release|break|inspect> <FileId> [remote] Manage receiver-owned fenced publication leases (--refs, --ttl-ms, --fence).
pm vcs serve Serve one repository or a directory of repositories over HTTP. --listen, --root and --auth configure the address, root and bearer grants.
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.

Repository links, private layers and permanent erasure

A committed link is a typed descriptor, distinct from vcs instance and its shared local store. Obtain the target's Repository.identity() and an exact commit ID, then write a JSON spec (pretty printing and a trailing newline are accepted):

{"version":1,"repository":"0123456789abcdef0123456789abcdef","revision":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","mappings":[{"source":"assets/model.bin","destination":"vendor/model.bin"}]}

The command validates every field using encodeLink and stores its canonical bytes. Stored link objects still require an exact canonical representation. Invalid JSON, root types and fields refuse before descriptor or index publication; unreadable spec files retain their filesystem errors. The descriptor commits metadata only. Exact file mappings declare the subset; branch movement cannot change the pin. Resolution requires the separately configured target read credential and creates an instance-private layer. Local target bindings and credentials are supplied at resolution and never appear in enclosing bundles. This implementation resolves local target repositories; it does not resolve HTTP pins or recursively follow nested descriptors.

command behavior
pm vcs authority --principal operator --read-token-file read.token --erase-token-file erase.token Initially configure two distinct clone-local version 2 scrypt grants only when the grant is absent. Token files should live outside tracked content.
pm vcs authority --principal operator --read-token-file new-read.token --erase-token-file new-erase.token --current-erase-token-file current-erase.token Replace a valid v2 grant using its separate current erase credential. Proposed new credentials cannot authorize replacement.
pm vcs authority --principal operator --read-token-file read.token --erase-token-file erase.token --regenerate-legacy Explicitly regenerate a recognized historical unversioned SHA-256 grant. This flag cannot replace v2, unknown, corrupt or unreadable grants.
pm vcs link dependency.link --spec link.json Stage a canonical typed descriptor; commit normally.
pm vcs link --list List descriptors without accessing target repositories.
pm vcs link resolve dependency.link --target ../target --read-token-file read.token --layer dependency Verify separate authority, target identity and exact revision, then overlay the mapped files privately.
pm vcs layer local destination.bin source.bin Snapshot one file privately; engine addLayer accepts multiple exact paths. --executable records private mode.
pm vcs layer --list Show names and excluded paths without printing private bytes.
pm vcs layer local --remove Restore the current underlying index; edited or missing overlays require --discard-edits.
pm vcs gc [--apply] Report unreachable objects by default; apply retains refs, oplog, leases, uploads and sparse instances.
pm vcs obliterate <FileId-or-indexed-path> --erase-token-file erase.token --reason incident Erase every historical payload and fragment of the stable identity, with a durable typed audit. Authorized retry resumes pending cleanup.
pm vcs recover-lock Recover a confirmed dead writer or aged empty legacy lock without erase authority. Pending erasure remains denied until authorized cleanup.

One-sided link changes retain their typed descriptor during merge. Competing pins raise link_merge_conflict; explicitly stage and commit an agreed descriptor, then retry the merge. Resolution never silently selects a pin or changes a private layer.

status reports excludedLayers and obliterated separately. Bulk staging skips layers; explicit staging refuses them. Checkout preserves edited layer bytes while changing the underlying index. Sparse-view changes and individual restore operations refuse while layers exist. Removing a layer restores its underlying staged bytes or removes overlay-only paths. To commit desired overlay changes, remove the layer, reapply those changes and stage them deliberately.

Erasure inventories unreachable trees, indexes, fragments, valid loose temporary copies and every registered or previously unlinked shared instance. It refuses ambiguous copies, affected layers, deduplicated payloads owned by another identity, missing inventory and unsupported pack/cache artefacts before mutation. Pending denial is fsynced before deletion and blocks writes until cleanup completes. A crashed writer's lock can be recovered with pm vcs recover-lock without erase authority. New locks publish complete PID metadata atomically; empty legacy locks require a one-minute grace period, and a live or unknown owner refuses. Coordinate recovery without concurrent recovery commands; pending erasure still needs authorized retry. Fresh bundles/clones reproduce intentional absence. Remote tombstones cannot erase held local bytes without local authorization. Stale bundles and novel payloads under a denied FileId fail preflight before storing bytes or moving refs.

The guarantee covers application-visible loose storage and known shared worktrees; it excludes external backups, independently owned clones, filesystem snapshots and physical-media recovery. The filesystem operator must exclude concurrent direct edits outside pm-vcs. There is no genuine pack backend: unknown storage is a refusal, never a successful erasure. Payload-only resumable upload batches refuse after erasure because that protocol carries no FileId provenance. Direct ObjectStore.write calls must then supply a non-denied FileId for new blob/record/manifest payloads.

Physical erasure inspects retained loose-object copies, including raw frames, zlib and base64 wrappers, before publishing denial. Supported encodings exceeding bounded inspection cause refusal. Windows erasure refuses before deletion because Node cannot provide directory fsync; ordinary metadata uses fsynced atomic replacement with that directory-durability limit. See the shipped composition and erasure design for exact bounds.

Engine APIs live in engine/repo.ts: identity, setAuthority, stageLink, links, resolveLink, addLayer, layers, removeLayer, obliterate, obliteratedPaths, readFileState and verify. ObjectStore.state returns the discriminated states present, obliterated, missing and corrupt; an erased read throws ObliteratedObjectError with its tombstone ID. See the shipped design for format and security boundaries.

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/transport.ts Async transport interface, capability negotiation and filesystem receiver policy.
engine/http-protocol.ts Shared HTTP envelopes, validation, denial bytes and transfer bounds.
engine/http-transport.ts HTTP(S) client implementing the same transport.
engine/serve.ts      node:http server delegating authorized operations to FileTransport.
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

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
manifest fixed or content-defined fragment addresses and byte lengths
series portable patch-series metadata
link immutable target repository identity, commit ID and exact file mappings
tombstone terminal FileId, affected addresses and bounded audit metadata

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.

.pmvcs, .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.

The .pmvcs name is reserved at every depth, including a leaf entry, and in every case spelling (.PMVCS, .PmVcS). A parent repository tracks a child repository's source and PM records, while keeping the child's credentials, objects, refs and private state out of staging, commits and status. Add removes legacy control entries from the index, including sparse entries; ordinary commits and merge continuation refuse such entries until they are removed. Stored trees cannot write or remove control paths through checkout, restore or sparse-view changes, and empty control directories survive pruning.

Filesystem aliases are fenced before content access: ancestor symlinks are never traversed, and materialization and restore never overwrite a leaf symlink. Staging can record an ordinary direct leaf link as target text; control targets, dangling control aliases and link chains are excluded. These protections cannot be negated. For a protected explicit add or restore, the command refuses with path_ignored.

On Linux with available procfs descriptor paths, materialization, restore and pruning pin directories across concurrent ancestor replacement. Other platforms, and Linux without usable procfs, use a portable fallback that verifies every captured ancestor's device/inode identity with lstat immediately before and after each mutation. A detected directory or leaf replacement refuses with worktree_path_changed. Exclusive file creation uses O_NOFOLLOW where available; leaf checks and descriptor identity checks also apply on Windows. Writes and chmod use fresh file descriptors, and deletions are non-recursive. The portable fallback narrows but cannot close the race between the final verification and a pathname syscall, because Node exposes no openat; a refusal can occur after a redirected mutation. A refused operation can leave ordinary worktree files partially changed; its replacement index is not published. See working-tree mutation protection for the exact limits.

.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.

Served pushes re-hash every arriving object and verify the complete reachable closure before publishing refs. The same fast-forward and compare-and-swap rules apply to file and HTTP remotes. Native .agents/pm/history/*.jsonl histories that preserve their base prefix merge through an append-only multiset union: each byte-identical event keeps the base count plus max(left_extra, right_extra). Cherry-picked or doubly merged events replay once; two identical appends on one branch remain two. Base bytes and timestamp ordering are preserved, and whitespace-distinct lines remain distinct. Rewritten prefixes retain conflict handling. See merge semantics for the line boundaries, ordering and separate PM hash-chain reconciliation contract.

Authorization runs before repository access. Missing repositories, invalid tokens, wrong repository scopes and unsafe names return identical HTTP response bytes, including headers. A read-only token receives 403 on write endpoints only when its repository exists and is readable. Traversal spellings, symlink repository/control aliases and linked instances are refused. Operators own the served filesystem: this is a repository authorization boundary, not protection against an operator changing the filesystem during a request.

Requests are limited to 512 MiB on write endpoints and 16 MiB on read endpoints, 10,000 fetch refs or ref updates, 100,000 haves or objects, and 4,096 upload sessions per repository. Old sessions are evicted; objects remain available for retry, but an evicted session loses its delivery receipt. Upload sessions are also bounded cumulatively — 1,000,000 accepted objects and 4 GiB of decoded object bytes between publications, configurable through the server's limits — because a reused session keeps every receipt it has accepted until it publishes. Past either cumulative bound the upload is refused with limit_exceeded, the session's receipts are released with it, and a publication under that session id afterwards reports a fresh session's empty receipt. A publication clears the charge with the receipts, refused publications included. Tokens load at startup and require a restart to rotate. Without --auth, served repositories are read-only unless --allow-unauthenticated-writes is passed. Keep the default loopback binding for local use; use TLS termination when carrying bearer tokens beyond loopback. The TLS-terminating proxy must forward Host as the bound server address and port, for example nginx proxy_set_header Host 127.0.0.1:<port>; for a loopback bind. Replace <port> with the server's listening port; forwarding the public hostname instead fails host validation and returns the fixed denial. Token-bearing remote URLs are sensitive local configuration; do not publish them. Client connection errors redact credentials and redirects are refused.


Serving a repository

Serve an existing repository in the foreground:

pm vcs serve --root project --listen 127.0.0.1:0 --auth tokens.json

The command reports the chosen ephemeral port and runs until stopped. A repository passed as --root is available at the base URL. A parent root exposes its repositories by relative name, including nested names such as tenant/project. At startup the server enumerates repository directories into a catalogue of canonical names and real paths. Requests resolve only by exact catalogue lookup; request text is never joined into a filesystem path. With authentication enabled, requests without any candidate repository in scope are denied before catalogue access. A request path that also reads as an endpoint — tenant/objects/fetch is either the repository tenant/objects with endpoint fetch or the repository tenant with endpoint objects/fetch — resolves among the candidates the token is in scope for (every candidate without --auth), and the deepest catalogued one wins. A repository whose name shares a segment with an endpoint suffix is therefore reachable under its own name, and an out-of-scope nested repository never changes the answer to an in-scope parent. An authorized lookup of an unknown name triggers at most one rescan per second. Each scan examines at most 10,000 directory entries and descends at most 32 levels; repositories beyond those bounds receive the same denial as unknown names. Use a narrower root for larger trees. Symlink aliases, escaping symlinks and linked instances are refused, including aliases introduced after discovery. The tokens file is a JSON array:

[
  { "token": "example-read-token", "repository": "", "access": "read" },
  { "token": "example-write-token", "repository": "", "access": "write" },
  { "token": "example-tenant-token", "repository": "tenant/project", "access": "write" }
]

The empty repository name grants the root repository. A write grant includes read access; all other repositories are denied. Replace the example tokens with private random secrets.

Without --auth, every repository is served readable: fetch and clone work for anyone who can reach the port, and every mutating endpoint refuses until the operator passes --allow-unauthenticated-writes — the explicit override for a port fenced another way. The server is an agent transport, not a web service, so it also refuses requests a browser would send: any request carrying an Origin header, any request whose Host is not the bound address and port (which is how a DNS-rebound page would read an answer it is not allowed to read), and any mutating request whose content type is not JSON (the one shape a cross-site page can send without a preflight this server never answers). A wildcard bind accepts address literals in Host at the bound port; reach such a server by its address, or bind to the name you use.

For a reported port of 43210, another agent uses the usual commands:

PM_VCS_TOKEN="$WRITE_TOKEN" pm vcs clone http://127.0.0.1:43210 agent-a
pm vcs remote add upstream http://127.0.0.1:43210
PM_VCS_TOKEN_UPSTREAM="$READ_TOKEN" pm vcs fetch upstream
pm vcs merge upstream/main
PM_VCS_TOKEN="$WRITE_TOKEN" pm vcs push origin
pm vcs verify

Use environment variables populated from your secret manager. PM_VCS_TOKEN_<REMOTE> (uppercase remote name, punctuation replaced with underscores) overrides PM_VCS_TOKEN, which overrides the stored credential. Environment credentials are never persisted. A non-empty PM_VCS_TOKEN is not host-scoped: a clone sends it to the host in the supplied URL (it takes precedence over URL userinfo), so set it only when every clone target is trusted; prefer the per-remote PM_VCS_TOKEN_<REMOTE> form. A non-empty bearer token is only ever sent to https:// remotes, or to a loopback http:// remote — a token on plain http:// to any other host is refused with an error naming the fix, because it would be a credential handed to every observer on the wire. For compatibility, URL userinfo supplied to clone or remote add is moved into the local .pmvcs/credentials.json; .pmvcs/remotes.json, receipts and diagnostics contain clean URLs. Existing credential-bearing remote maps migrate on first read. The credentials file is created with mode 0600 and existing permissions are repaired on read. If repair fails with EPERM or EROFS, reading is allowed only when no group or world permission bits are set; otherwise the error is raised. File fetch/push does not read bearer credentials. Windows does not enforce POSIX mode bits; restrict access with filesystem ACLs or use environment tokens. Removing a remote also removes its stored credential. Rotate any previously exposed tokens; migration cannot erase old transcripts or operation logs.

The native VCS always excludes the PM tracker's runtime caches, search indexes, locks, transactions and checkpoints using the SDK's runtime fence and resolved tracker root. search/eval-queries.json remains eligible for tracking. These exclusions apply to status, staging and materialization and cannot be overridden by .pmvcsignore negations; the next add removes runtime entries left in an older index. Item records and history remain tracked.

Clone adopts the same record configuration as a file clone. Ref advertisement, bundle fetch, verified object upload and publication use the existing transport capabilities. The optional object-fetch capability adds Transport.fetchObjects(ids), returning a verified bundle of individually named objects, including standalone patch series. Review records travel with ordinary trees and commits. A series consumer fetches its base and patch history as well as its series object before importing the series bundle.


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 with two shipped implementations: a filesystem transport, which is the case that occurs today — several agents, several working trees, one host — and HttpTransport, an HTTP transport that speaks to pm vcs serve (see Serving a repository).

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.


Depth-limited and blob-filtered clone/fetch, demand hydration, and conservative GC are documented in scale history and collection.

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 at the root and under src/, engine/ and scripts/ with --all. It requires exactly 100% statements, branches, functions and lines for each file, including self-hosting, bundle handling and the gates themselves. A never-loaded module fails instead of disappearing from the report. The command selects package.json explicitly so an ambient c8 configuration cannot narrow the inventory. Collection uses the project's own coverage/tmp directory: a nested c8 process must not inherit and clear its parent's raw records. A real marker-file regression checks that boundary.

ESLint uses the strict policy published by the pinned pm-ops package across production code and tests. It rejects explicit any, dynamic and inline type imports, non-erasable syntax and repository convention violations. The docstring gate uses pm-ops/docstrings. The duplication launcher uses pm-ops/duplication with the pinned jscpd 5 binary, a zero threshold, a 50-token floor and a one-line floor. It enumerates all production sources, reconciles the detector's scan count at a one-token floor, and rejects empty or incomplete scans. Detector subprocesses run outside consumer configuration. The optional fast-glob peer required by this pinned fleet analyzer is installed explicitly.

test/exact-gates.test.ts runs real processes against disposable projects to prove omitted sources, untested branches, forbidden syntax, short clones and hostile configuration fail closed. It also invokes the operational scripts directly. release:check includes lint, duplication, docstrings and coverage; prepublishOnly requires that complete release gate.

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

Publication requires explicit stable FileId leases, including renamed, deleted and historical files. Large fragmented content uses bounded manifest hierarchies; legacy flat manifests remain readable through verified bounded streams. See publication lease workflows and manifest API contracts.

About

Branch-aware merge safety for pm-cli trackers: preflight a checkout before multi-agent item work, predict a merge's per-field resolution before running it, and map a commit range onto the pm items it touched.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages