Skip to content

0.9.12: every write runs bare capy first - #396

Merged
cvince merged 13 commits into
mainfrom
vince/cap-712-one-commit-path
Oct 21, 2026
Merged

cvince merged 13 commits into
mainfrom
vince/cap-712-one-commit-path

Conversation

@cvince

@cvince cvince commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Every command that writes from a folder now runs bare capy as its first step and writes nothing until the folder and Capy agree. Spec: conflict resolution for every write.

Gated: capy edit (screen and NAME), add, remove, connect (and --discover, per folder), rotate, push, checkout <existing branch>, org, and every capy deploy path.
Not gated: capy checkout -b (it carries uncommitted values to the new branch), capy decrypt, recovery-mode checkout, the system store, new project creation.

  • The choices are bare capy's: pinned | local | remote | delete.
  • Uploads carry base_keep_hash. If the server refuses one as stale, the command runs bare capy again and retries once.
  • capy secrets and batch deploy compare each value they write the same way.
  • Dokploy: a value changed on the platform is a choice. local delivers Capy's value; remote keeps the platform's.
  • Without a terminal: exit 3 with SECRETS_CONFLICT, a conflict_id and the differing names (fingerprints only). Rerun with --resolve NAME=<choice> and --conflict <id>.
  • Docs: README, the capy agents block and the deploy --yes help. This wording is still a draft.

Draft for local testing.

cvince added 13 commits October 15, 2026 00:54
…es on top of the server's latest

Every folder-based save (capy edit, edit NAME, add, remove, connect,
rotations, push, bare capy commit-local, token revoke, the system store)
used to upload the folder's whole env merged into its own keep.lock, so a
folder pinned behind the server silently reverted newer values and dropped
server-only variables.

src/sync/commit.ts now owns every write: read the server's latest for the
branch, apply only the intended set/remove on top of it (unchanged names
keep the server's ciphertext verbatim), stop with SECRETS_CONFLICT when a
changed name was also changed on the server since the folder's pin, upload
with base_keep_hash, and re-read and re-plan once on SECRETS_STALE_BASE.
capy secrets set and the deploy records send the base too.

compareSecrets moves to src/sync/. The unused SyncEngine.compareEnvironments,
applyDecisions, generateSyncResult, formatSyncSummary and validateDecisions
are removed.
…h deploy sends only picked vars

Off a terminal a conflict stops the write, exits 3 and prints
{ok:false, code:SECRETS_CONFLICT, conflict_id, conflicts} with
fingerprints only; rerun with --resolve NAME=mine|theirs and
--conflict <id>. The id binds the decision to the state seen; a moved
state returns SECRETS_CONFLICT_STALE with a fresh id. --dry-run
previews. Shared module src/sync/conflicts.ts, used by bare capy, edit
NAME, add, remove, connect import, rotate, push, secrets set, deploy
and batch deploy.

capy deploy checks the folder against Capy's latest (a stale folder
delivers Capy's value) and, for Dokploy, the live value against what
Capy last delivered: mine delivers, theirs skips and records nothing.
Batch deploy delivers and records only the picked names, keeping the
rest of Capy's block byte-identical. capy push uploads deletions.
capy deploy's keep update goes through the commit path.
…ks before minting; deploy commits folder edits

- Dokploy target resolutions carry the theirs/yours fingerprints the caller
  answered; a live value that moved between check and write reopens the
  conflict. The target comparison runs on the values actually delivered
  after source decisions.
- capy rotate at a terminal asks about conflicts before issuing a key.
- capy deploy commits a folder edit nobody else touched before delivering
  it, so Capy and the platform agree.
- capy secrets set reuses the shared conflict printer.
…atch deploy; generated command list

- capy edit compares against the server's latest (not the folder's pin),
  shows local / remote / conflict honestly, resolves a conflict row with
  m (mine) or t (theirs), and blocks commit until every conflict is
  decided. Decisions are bound to the values shown; a server move between
  showing and committing reopens the conflict.
- capy secrets edit reads every location first and asks mine/theirs for
  a location that changed since you looked; nothing is written before.
- The batch deploy screen shows Dokploy values changed outside Capy and
  asks before delivering.
- The unknown-command list is generated from the registered commands.
capy deploy never delivers a value Capy does not hold and never saves
one unasked. A folder edit nobody else changed asks "save and deploy?"
in a terminal; off a terminal it stops with exit 3 and
{ok:false, code:DEPLOY_UNSAVED_CHANGES, names} (names only). Source
conflicts are reported first; choosing mine saves without a second
question. --dry-run reports unsaved edits and writes nothing.
Every command that writes from a folder (edit, edit NAME, add, remove,
connect, rotate, push, deploy) runs bare capy's own sync as its first
step and repeats it until the folder and Capy agree, or the person
stops (nothing written). Then it applies only its own change on top of
Capy's latest with base_keep_hash; a stale refusal re-runs the sync.

Bare capy is restored to its documented behaviour (its tests match
v0.9.11); its upload carries base_keep_hash. Off a terminal the
differences are bare capy's compareSecrets rows: exit 3 with a
conflict_id, answered with --resolve NAME=pinned|local|remote|delete
and --conflict. Writes without a folder (capy secrets, batch deploy)
compare the shown value, the new value and Capy's latest with the same
choices. The Dokploy live-value check uses local/remote.

Removed the conflict logic invented on this branch: findConflicts,
mine/theirs and the m/t keys, DEPLOY_UNSAVED_CHANGES, the separate
capy secrets check, and the narrowed commit-local.
…ehind them

- capy deploy (token + docs), deploy revoke, deploy targets-remove and
  deploy --discover run bare capy first.
- capy connect --discover gates unconditionally, and runs bare capy in
  each discovered folder before writing into it.
- Off a terminal, an answer whose source has no value for that row is
  refused (SECRETS_RESOLVE_INVALID) instead of deleting the variable.
- capy secrets set offers pinned / local / remote / delete; pinned
  writes back the shown value when it can be recovered.
- No active branch off a terminal is NO_ACTIVE_BRANCH with a hint.
- Bootstrap branches on the NO_SECRETS code, not the message.
…b is the one exception

Switching to an existing branch and re-linking the folder to another
organization both rewrite the folder's keep.lock, so they run bare capy
first like every folder write. capy checkout -b does not: creating a
branch carries the folder's uncommitted values onto a brand-new branch,
and bare capy offers to push them there on the next sync.
… discovery failures; recovery-mode checkout

- As another command's first step, bare capy compares against the
  branch the .env was written for when .capy/branch disagrees, so
  capy checkout can recover an interrupted checkout. Run directly it
  keeps the recovery hint.
- Inside connect --discover that state is BRANCH_STATE_INCONSISTENT for
  the folder, and the run continues.
- capy checkout in seed-phrase recovery mode switches offline.
- The gate recovers a deleted keep.lock from .env metadata like bare
  capy does.
- Real-gate tests for rotate, connect, deploy targets-remove, checkout
  and org.
Comment thread src/sync/compareSecrets.ts Dismissed
@cvince
cvince marked this pull request as ready for review October 18, 2026 09:44
@cvince
cvince merged commit 30e47ea into main Oct 21, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants