wowbagger 0.1.0-alpha.9 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +509 -0
  2. package/README.md +272 -136
  3. package/docs/adapter-contract.md +1 -1
  4. package/docs/host-contract.md +7 -1
  5. package/docs/mutation-contract.md +354 -82
  6. package/docs/work-claim-contract.md +466 -88
  7. package/package.json +2 -2
  8. package/schemas/core-capabilities-response.json +1 -1
  9. package/schemas/core-envelope.json +4 -3
  10. package/schemas/index.json +18 -0
  11. package/schemas/ledger-repair-proposal.json +170 -0
  12. package/schemas/ledger-repair-request.json +61 -0
  13. package/schemas/ledger-repair-response.json +90 -0
  14. package/schemas/report-config-v1.json +4 -0
  15. package/schemas/report-config-v2.json +5 -0
  16. package/skills/wowbagger/SKILL.md +242 -59
  17. package/src/adapter/core-probe.js +3 -4
  18. package/src/adapter/process-outcome.js +8 -1
  19. package/src/claim-capabilities.js +3 -3
  20. package/src/claim-coordinator.js +61 -12
  21. package/src/claim-journal.js +212 -7
  22. package/src/claim-prospective.js +1 -28
  23. package/src/claim-publication.js +412 -78
  24. package/src/claim-request.js +9 -0
  25. package/src/claim-store.js +9 -4
  26. package/src/cli.js +302 -56
  27. package/src/extensions.js +1 -0
  28. package/src/git-autocommit.js +106 -43
  29. package/src/git-reconciliation.js +74 -19
  30. package/src/git-worktrees.js +73 -0
  31. package/src/instrumentation.js +1 -0
  32. package/src/launch.js +2 -2
  33. package/src/ledger-repair.js +1170 -0
  34. package/src/mutation.js +73 -15
  35. package/src/reconciliation-classifier.js +117 -0
  36. package/src/report-evidence.js +158 -41
  37. package/src/report-graph.js +201 -73
  38. package/src/report-html.js +358 -155
  39. package/src/report-impact.js +106 -0
  40. package/src/report-selection.js +97 -0
  41. package/src/report-sequencing.js +4 -4
  42. package/src/report-svg.js +74 -20
  43. package/src/report-view.js +14 -1
  44. package/src/report.js +109 -23
  45. package/src/version-drift.js +98 -0
  46. package/src/worktree-identity.js +165 -0
@@ -1,11 +1,11 @@
1
1
  # Work-claim contract
2
2
 
3
3
  Status: accepted protocol design. The standalone Wowbagger CLI implements the
4
- version 2 claim operations and the merge-coordinated Git-journal profile for
4
+ version 3 claim operations and the merge-coordinated Git-journal profile for
5
5
  provisioned Git-backed ledgers. The no-I/O reference model and conformance
6
6
  fixtures remain the oracle for the strict fenced protocol.
7
7
 
8
- This document defines version 2 of the transport-neutral work-claim and
8
+ This document defines version 3 of the transport-neutral work-claim and
9
9
  claimed-publication API, plus the merge-coordinated capability profile. The
10
10
  words MUST, MUST NOT, SHOULD, and MAY are normative. JSON examples show objects
11
11
  before compact serialization; a CLI prints exactly one compact JSON object
@@ -23,6 +23,21 @@ Generic consumers migrate without a wire change: they first identify the
23
23
  work-claim envelope by `namespace: "work-claim"`, then require the advertised
24
24
  `api_version`.
25
25
 
26
+ ### Version 3
27
+
28
+ Version 3 retains every version 2 request, response, state, exit, fencing, and
29
+ recovery rule except for targeted verification. `claim-verify` accepts optional
30
+ `--id <wb_...>` without requiring that item to exist in the local checkout.
31
+ Bare verification remains strict repository-wide mode.
32
+
33
+ Every success result adds `verification_scope`: `{"mode":"repository"}` for
34
+ the bare command, or `{"mode":"target-item","item_id":"wb_..."}` for a target.
35
+ Every returned finding adds `blocks_verification_scope`. Target mode keeps all
36
+ repository findings visible but derives `ok`, exit status, and `state` only
37
+ from findings that block that target. Global safety findings still block every
38
+ target. This additive response and new CLI input move the negotiated API while
39
+ the top-level claim-envelope `contract_version` remains `1`.
40
+
26
41
  ### Version 2
27
42
 
28
43
  Version 2 retains every version 1 request, response, state, exit, fencing, and
@@ -86,20 +101,25 @@ carries one operating rule that binds every caller:
86
101
 
87
102
  **Commit each mutation to Git before running the next mutating command.**
88
103
 
89
- A merge-coordinated backend validates recorded revisions against Git `HEAD`,
90
- never against working-tree bytes. An uncommitted mutation is an unreconciled
91
- mutation. The next `create`, `transition`, `patch`, or `publish-claimed`
92
- therefore refuses with exit 6 `claim-store-unavailable` and
93
- `details.reason: "publication-reconciliation-required"` rather than writing on
94
- top of work that is not yet durable.
95
-
96
- `claim-verify` is the reconciliation procedure for that refusal. The loop is
97
- write, commit, `claim-verify`, next write. Section 6 defines `claim-verify`,
98
- section 7 defines the refusal the legacy write paths emit, and section 8
99
- defines the error envelope. The [mutation
104
+ A merge-coordinated backend reconciles recorded revisions with Git `HEAD` and
105
+ the working tree. It refuses the next mutation when it finds an
106
+ `unauthorized-revision`, requires Git finalization, or requires synchronization
107
+ for the target item. A `worktree-synchronization-required` finding on an
108
+ unrelated item remains visible without blocking the command.
109
+
110
+ An existing item's latest authorized working-tree bytes and an earlier
111
+ authorized revision at `HEAD` form an authorized predecessor/successor window.
112
+ That window produces no finding, so another mutation can run before the first
113
+ one is committed. Acceptance is not finalization: callers still follow write,
114
+ commit, `claim-verify`, next write.
115
+
116
+ `claim-verify` is the reconciliation procedure for every refusal. Section 6
117
+ defines `claim-verify`, section 7 defines the refusal the legacy write paths
118
+ emit, and section 8 defines the error envelope. The [mutation
100
119
  contract](mutation-contract.md) section 12 states the same rule for
101
- `create`, `transition`, and `patch` callers, together with the
102
- considered-and-rejected alternative of validating against working-tree bytes.
120
+ `create`, `transition`, `parent-migrate`, `snooze`, and `patch` callers,
121
+ together with the considered-and-rejected alternative of validating only
122
+ against working-tree bytes.
103
123
 
104
124
  The optional `--auto-commit` flag performs that whole loop inside one
105
125
  invocation on a provisioned ledger: it runs the pre-mutation `claim-verify`,
@@ -123,6 +143,15 @@ runs `claim-verify`. Repeating it creates no second commit. [Mutation
123
143
  contract](mutation-contract.md) section 13 defines the flag, its strict
124
144
  preflight, its commit sets, its failure envelopes, and this command.
125
145
 
146
+ When a provisioned ledger is opened in a fresh Git clone, the local claim
147
+ journal may not exist even though `HEAD` carries a committed reconciliation
148
+ log. The first reconciliation hydrates the local journal from that committed
149
+ projection, preserving its sequence gaps with non-projected clock entries. A
150
+ lock-free claim read projects the same committed evidence in memory without
151
+ writing the journal. This makes committed claim and publication history visible
152
+ without inventing new authority; later writes still require the clone's own
153
+ Git history to finalize them.
154
+
126
155
  ## 2. Ledger namespace and identity
127
156
 
128
157
  Every claim key is the immutable tuple `(ledger_namespace, item_id)`. No state,
@@ -178,7 +207,7 @@ most `18446744073709551615`. Epochs never wrap, decrement, or get reused.
178
207
  "operations": {
179
208
  "work_claim": {
180
209
  "supported": true,
181
- "api_version": 2,
210
+ "api_version": 3,
182
211
  "mode": "fenced",
183
212
  "claim_protected_publication": true,
184
213
  "fencing_enforced_at": "ledger-publication-commit-boundary",
@@ -268,7 +297,7 @@ A provisioned Git-journal backend MAY instead report:
268
297
  "operations": {
269
298
  "work_claim": {
270
299
  "supported": true,
271
- "api_version": 2,
300
+ "api_version": 3,
272
301
  "mode": "merge-coordinated",
273
302
  "claim_protected_publication": true,
274
303
  "fencing_enforced_at": "git-history-reconciliation",
@@ -313,38 +342,199 @@ the local working tree and the local Git HEAD. A revision written in another
313
342
  worktree is absent in both, so reconciliation reports
314
343
  `stale-write-detected` and the mutation refuses with exit 6
315
344
  `claim-store-unavailable`, reason `publication-reconciliation-required` only
316
- when the mutation targets that item. `claim-verify` remains repository-wide and
317
- reports every unresolved finding.
345
+ when the mutation targets that item. Bare `claim-verify` remains
346
+ repository-wide. `claim-verify --id <item>` reports every unresolved finding
347
+ but blocks only on that target and global safety barriers.
318
348
 
319
349
  The plain statement: **a recorded private write in one worktree blocks
320
350
  mutations targeting that item, not unrelated item mutations in sibling
321
351
  worktrees.** Own uncommitted work and out-of-protocol revisions remain global
322
352
  safety barriers. This is item-scoped availability, not exclusive dispatch.
353
+ `create` reads that same reconciliation with one extra barrier, because it is
354
+ the one mutation whose output depends on an identity no caller supplied.
355
+
356
+ **Superseded: create no longer stays journal-silent.** Through alpha.13 this
357
+ contract decided that create records nothing, and three reasons held that
358
+ decision: create already protects its own instant, because publication is
359
+ atomic, refuses to clobber an existing path, and verifies the published bytes
360
+ exactly; a journaled create would serialize every worktree on the
361
+ highest-volume mutation; and the remaining exposure window was judged narrow
362
+ because it closed at the item's first journal-visible mutation.
363
+
364
+ Item #181 overturns that decision. The first reason answers the wrong
365
+ question. Create's atomic instant protects the path it publishes to, and
366
+ nothing protected the `number` it derives. A schema-version-2 create takes
367
+ `1 + max(existing numbers)` from the items its own checkout holds, so two
368
+ worktrees that have not integrated each other's commits derive the same number
369
+ and both publish. They need not overlap in time: a create today and a create
370
+ tomorrow collide as reliably as two simultaneous ones, because the loser is a
371
+ stale checkout rather than a lost race, and a wider mutex cannot fix a
372
+ sequential failure. Only durable evidence can. `number` is immutable and
373
+ `patch` correctly rejects it, so an undetected collision surfaces at
374
+ integration as a global `duplicate-number` validation failure that stops the
375
+ whole ledger.
376
+
377
+ **The fixed ordering.** On a provisioned ledger a create is a legacy mutation
378
+ like any other, and the shared namespace lock is held across every step:
379
+
380
+ 1. Acquire the shared namespace lock.
381
+ 2. Reconcile the repository state and apply the allocation fence below.
382
+ 3. Load this worktree's ledger.
383
+ 4. Acquire the item, relation, and number-index lock closure.
384
+ 5. Reload and validate the ledger under those locks.
385
+ 6. Derive `1 + max(existing numbers)`.
386
+ 7. Serialize the candidate item.
387
+ 8. Validate the complete candidate ledger.
388
+ 9. Reserve journal capacity, then append the create intent.
389
+ 10. Publish with atomic no-clobber semantics.
390
+ 11. Verify the final path holds the exact candidate bytes.
391
+ 12. Append the committed or aborted terminal.
392
+
393
+ A create appends its `legacy-mutation-intent` with `command: "create-v1"`
394
+ before any byte reaches the item path, and it appends that intent only after
395
+ step 8, so a refusal the command would have returned anyway records no
396
+ attempt. The intent carries `expected_revision: null`, which is valid only for
397
+ `create-v1`; every other command still requires a string revision. The
398
+ committed terminal is `legacy-mutation` with `command: "create-v1"`. The
399
+ create abort carries `command: "create-v1"` and `observed_revision: null`,
400
+ while an abort for any other command remains valid without `command` and still
401
+ requires a string revision. No entry records the assigned number: the
402
+ candidate revision binds the complete item bytes, and the ledger stays the one
403
+ number authority.
404
+
405
+ **The allocation fence.** Create reconciles with its own item as the target,
406
+ exactly like every other mutation, and one extra barrier reads the result.
407
+ Every global finding blocks create because it blocks every write:
408
+ `git-finalization-required` for this worktree's own uncommitted authorized
409
+ bytes, `unauthorized-revision` for out-of-protocol bytes, and an unresolved
410
+ `legacy-mutation-outcome-unknown`. On top of those, any coordinated item this
411
+ checkout does not hold blocks `create`, because the journal records a
412
+ committed terminal for an item whose number this working ledger cannot read,
413
+ so the next number derived here may be one a sibling already published. A
414
+ stale revision of an item this checkout holds does not block `create`: a
415
+ number is immutable, so the local maximum is already correct, and that finding
416
+ keeps its ordinary target scope.
417
+
418
+ **The old exposure window is closed on a provisioned ledger.** The journal used
419
+ to learn of an item only at its first `transition` or `patch`, so an
420
+ out-of-protocol overwrite before that mutation went unreported. A journaled
421
+ create records an authorized revision at the item's birth, so the ordinary
422
+ surfaces cover the item from then on: an out-of-protocol overwrite reports
423
+ `unauthorized-revision` and the next mutation refuses. An unprovisioned or
424
+ non-Git ledger keeps only its local number-index lock and its atomic
425
+ no-clobber publication; nothing there coordinates across checkouts, and nothing
426
+ here defends against an actor that bypasses this tool.
427
+
428
+ **The refusal.** A fenced create refuses before it allocates a number and
429
+ before it writes a byte:
430
+
431
+ ```text
432
+ exit: 6
433
+ ok: false
434
+ namespace: "ledger-mutation"
435
+ command: "create-v1"
436
+ contract_version: 1
437
+ state: "unchanged"
438
+ error.code: "claim-store-unavailable"
439
+ error.details.reason: "publication-reconciliation-required"
440
+ ```
323
441
 
324
- **Decided: create stays journal-silent.** The asymmetry is intended. Three
325
- reasons hold it:
326
-
327
- 1. Create already protects its own instant. Publication is atomic, it refuses
328
- to clobber an existing path, and it verifies the published bytes exactly
329
- after the write. A journal entry adds no protection to that instant.
330
- 2. A journaled create would serialize every worktree on the highest-volume
331
- mutation. The field reports name create as the most frequent mutation and as
332
- the most frequent victim of a block. Making create a blocker as well as a
333
- victim multiplies a cost that consumers already report.
334
- 3. The remaining exposure window is real but narrow, and it closes at the
335
- item's first journal-visible mutation.
336
-
337
- **The exposure window, stated honestly.** The journal does not know a created
338
- item until that item's first journal-visible mutation, which is a `transition`
339
- or a `patch`. Until then an out-of-protocol overwrite of the item's bytes is
340
- not detected. A commit alone does not close the window, because reconciliation
341
- compares only the revisions the journal expects, and the journal expects none
342
- for that item. From the first `transition` or `patch`, the ordinary surfaces
343
- cover the item: with the authorized revision committed, an out-of-protocol
344
- overwrite of the working-tree bytes reports `unauthorized-revision` and the
345
- next mutation refuses. Inside the window, only an actor that bypasses this tool
346
- can overwrite the item, and this protocol does not defend against that actor.
347
- It is merge-coordinated, not exclusive.
442
+ `error.details.findings` names the item whose authorized create this checkout
443
+ cannot see, and each finding's own `remediation` string stays authoritative:
444
+ commit this worktree's bytes for `git-finalization-required`; synchronize the
445
+ named sibling revision when `owner_ref` names a live worktree; inspect the
446
+ reachable or dangling history and restore or explicitly adopt reviewed bytes
447
+ for a locally absent or reachable-unowned revision; and restore or adopt for
448
+ unauthorized bytes. The reproduced stale-sibling case is locally absent on both
449
+ surfaces, so it renders the cannot-be-established sentence, which is an
450
+ instruction to inspect and integrate, never an instruction to wait. Once the
451
+ ledger validates and `claim-verify` exits 0, retry the same request ID: the
452
+ refusal published no byte, so the retry is an ordinary no-clobber create and
453
+ takes the next number. The fence does not trap recovery — `claim-verify` stays
454
+ read-only, `claim-adopt` keeps its explicit override, claim release stays
455
+ available under barriers, and Git synchronization was never this tool's to
456
+ block.
457
+
458
+ **Recovery from an interrupted create.** An intent with no terminal resolves on
459
+ the next invocation from the item path alone: the candidate revision present
460
+ appends the committed terminal, the path absent appends the create abort, and
461
+ any third revision reports `legacy-mutation-outcome-unknown` as a global
462
+ barrier. Recovery never guesses a number and never publishes an item. Because
463
+ capacity for the intent, one clock entry, and one terminal is reserved before
464
+ the intent is appended, an exhausted journal refuses unchanged before
465
+ publication rather than stranding an unresolvable attempt.
466
+
467
+ **Create owns its item and its reconciliation log.** A successful create is
468
+ journal-visible from the item's birth, so its `changed_paths` and its
469
+ `--auto-commit` commit set are exactly the created item and
470
+ `<ledger>/.wowbagger/reconcile-<namespace>.md`. Owning the log it writes is not
471
+ permission to absorb residue: a reconciliation log already dirty before the
472
+ invocation is foreign, and create still refuses it.
473
+
474
+ **Commit every create before the next mutation.** A create has no authorized
475
+ predecessor — there is no earlier revision of an item that did not exist — so
476
+ Git `HEAD` is the only surface that can hold its authorized bytes, and an
477
+ uncommitted create reports the global `git-finalization-required` until it is
478
+ committed. A `patch` or `transition` may instead occupy the documented
479
+ authorized predecessor/successor window and run before its predecessor is
480
+ committed, but that window is an accepted overlap, not a license: commit every
481
+ mutation before the next one. Batch create is permanently rejected for the
482
+ direct-Markdown architecture. The supported bulk pattern is serial
483
+ `create --auto-commit`, one invocation and one commit per successful item; see
484
+ the [accepted batch-create decision](design/2026-08-30-batch-create.md).
485
+
486
+ **Cost.** A provisioned create already took the namespace lock, replayed the
487
+ journal, loaded the ledger, read Git `HEAD`, and reconciled. The fence changes
488
+ which findings refuse; it adds no new Git roster or history traversal to a
489
+ clean create, and the measured cost of the change is two extra fsync'd journal
490
+ appends. The durable cost is journal growth: each successful create adds one
491
+ intent and one committed terminal, so with no other activity the
492
+ 65,536-entry limit permits at most 21,845 three-entry create cycles and the
493
+ 8 MiB byte limit may bind first. Other claim and mutation activity lowers that
494
+ ceiling. Capacity is checked before publication and fails closed with exit 6
495
+ `claim-store-unavailable`, reason `journal-capacity-exceeded`, and `unchanged`;
496
+ no current-operation intent, terminal, or item byte is published. The same
497
+ reason applies to claim lifecycle, verification, adoption, publication reads,
498
+ and every legacy mutation while capacity is known before publication. A
499
+ genuine persistence failure remains `clock-floor-persistence-failed`, and an
500
+ ambiguous outcome after an intent remains an outcome-unknown refusal. Journal
501
+ history is never truncated, and automatic compaction is not part of this
502
+ contract.
503
+
504
+ A ledger that already carries duplicate numbers is item #182 recovery work and
505
+ is repaired through the separate `ledger-repair` contract, not through claim
506
+ operations. Run `number-repair-proposal --ledger <dir> --json`, review the
507
+ complete mapping, then run `number-repair --ledger <dir> --input <repair.json>
508
+ --json`. The repair command runs only when duplicate-number errors are the
509
+ complete validation failure, preserves ULID identities and relation values, and
510
+ publishes all affected items under the shared namespace fence. Arbitrary hand
511
+ edits remain unsupported because they can damage IDs, paths, or references.
512
+
513
+ **What alpha.14 guarantees, and what it does not.** Alpha.14 closes the
514
+ reported PropertyCompass2 collision: cooperating alpha.14 worktrees of one
515
+ clone that share one Git common directory can no longer commit two items
516
+ carrying the same number, because every such create is visible through the
517
+ shared journal before publication even without branch integration. Separate
518
+ clones, separate machines, alpha.13 writers before the hard cutover, and
519
+ noncooperating writes stay outside that fence and still rely on branch
520
+ integration plus `validate`.
521
+
522
+ **The upgrade is a hard cutover.** Alpha.13 cannot read a `create-v1` legacy
523
+ intent. Executed against a ledger whose shared journal already carries one, its
524
+ create exits 6 with `error.code` `claim-store-unavailable`, message
525
+ `The durable claim store is unavailable.`, and `error.details.reason`
526
+ `claim-store-unreadable`, leaving the state unchanged and writing no item. That
527
+ fail-closed behavior is the compatibility guarantee: an old writer cannot
528
+ commit another duplicate. It is also the operational limit, because alpha.13 is
529
+ immutable and emits a generic unreadable-store message rather than upgrade
530
+ guidance. Read that exact refusal as **this repository was written by a newer
531
+ Wowbagger; upgrade this worktree to continue.** There is no automatic migration
532
+ and no mixed-version grace period: upgrade every writer in one Git coordination
533
+ domain before the first alpha.14 create. If a partial upgrade writes the new
534
+ grammar first, every remaining alpha.13 worktree stops making claim-protected
535
+ mutations until it is upgraded. Item #185 owns the general ability of a running
536
+ old binary to diagnose its own version drift; no change here can retrofit a
537
+ message into an already-published executable.
348
538
 
349
539
  **`unauthorized-revision` has two remedies, and only one of them is
350
540
  destructive.** Restoring the authorized revision discards the out-of-protocol
@@ -359,26 +549,158 @@ Each refusal carries a `reason` that separates the cases:
359
549
  | `reason` | Cause | Remedy |
360
550
  |---|---|---|
361
551
  | `git-finalization-required` | this worktree wrote the item and has not committed it | commit here, then `claim-verify` |
362
- | `worktree-synchronization-required` | another worktree wrote the item | wait for the owner named by `owner_ref`/`owner_commit`, then synchronize this checkout and run `claim-verify`; if `owner_unavailable` is true, inspect reachable or dangling commits and use explicit restore/adopt authority |
552
+ | `worktree-synchronization-required` | another worktree wrote the item | wait for the owner named by `owner_ref`/`owner_commit`, then synchronize this checkout and run `claim-verify`; if `owner_unavailable` is true, follow the finding's `remediation` (section 3.2 step 4) |
363
553
  | `unauthorized-revision` | the item was changed outside the protocol | restore the authorized revision and discard the edit, then `claim-verify`; or adopt the committed revision and keep the edit, then `claim-verify` (section 3.3) |
364
554
 
555
+ **Scope is the coordinator's judgement, not a published member.** What a
556
+ finding refuses — every write, only a write against the item it names, or
557
+ nothing — is decided from the topology and travels beside the finding, never
558
+ inside it. No response carries it. A consumer MUST NOT infer scope from the
559
+ `reason` string, from the `remediation` sentence, or from whether `owner_ref`
560
+ is present: those are diagnostics for a person, and reading them as a scope
561
+ signal is exactly how alpha.11 treated an out-of-protocol revision as advisory
562
+ synchronization. The only supported scope signal is the refusal itself: a
563
+ mutation that returns exit 6 was blocked, and one that returns exit 0 was not.
564
+
565
+ Ownership on the current symbolic ref is never reported as a foreign worktree
566
+ owner, and a detached `HEAD` applies the same guard through its reachable
567
+ commit history. If the expected revision is already reachable from the current
568
+ checkout, a same-branch regression cannot be repaired by waiting for another
569
+ worktree; it remains `unauthorized-revision` and blocks unrelated mutations.
570
+ This distinguishes a real sibling's uncommitted successor from history the
571
+ current checkout already owns.
572
+
573
+ **`owner_ref` names an active named worktree, and nothing else.** Owner
574
+ evidence is the worktree roster Git reports live, searched this checkout first,
575
+ then every live worktree that has a branch checked out, ordered by branch ref
576
+ and then by path so one revision carried by several live worktrees always names
577
+ the same owner. A bare record, a record Git marks prunable, and a worktree
578
+ whose `HEAD` names no commit are excluded. `owner_ref` is always the branch of
579
+ one of those live worktrees, and `owner_commit` is the commit in that
580
+ worktree's history whose item bytes are the expected revision.
581
+
582
+ Reachability alone is never ownership. A tag, a remote-tracking ref, a branch
583
+ no worktree has checked out, and a live worktree on a detached `HEAD` can each
584
+ reach the expected revision while naming nobody who could publish it. All of
585
+ them, and a revision no reachable commit carries at all, produce
586
+ `owner_unavailable: true` and no `owner_ref`. So `owner_unavailable` means one
587
+ of three things: the expected revision is not reachable at all, a live sibling
588
+ holds it on a detached `HEAD`, or it is reachable only from refs no active
589
+ worktree has checked out.
590
+
591
+ Three `owner_unavailable` remediation sentences separate those cases. The
592
+ sentence naming ownership that cannot be established from reachable refs is
593
+ emitted only when this checkout has no item path and `HEAD` has none either, so
594
+ the item has never existed here. A revision Git does reach — from a tag, a
595
+ remote-tracking ref, a branch no worktree has checked out, or a live worktree on
596
+ a detached `HEAD` — gets the sentence saying the revision is reachable in Git
597
+ while no active named worktree owner is established. Its remedy is to inspect
598
+ the reachable history and restore or explicitly adopt reviewed bytes, then run
599
+ `claim-verify`; there is no commit left to wait for. Only a revision no
600
+ reachable commit carries at all gets the not-yet-reachable sentence, whose
601
+ remedy is to wait for the owning worktree to commit, then synchronize this
602
+ checkout and run `claim-verify`.
603
+
604
+ Through alpha.13 the reachable cases were given the not-yet-reachable sentence
605
+ too, which told a reader to wait for a commit Git already had. The wait never
606
+ ended, because no named worktree was ever going to publish those bytes. The
607
+ `reason`, the codes, the scope, and the finding members are unchanged; only the
608
+ `remediation` text for the reachable-unowned cases is corrected.
609
+
610
+ **Writer identity.** A legacy mutation and a claimed publication record the
611
+ worktree that authorized them in their journal entries as
612
+ `writer_worktree_id`: `legacy-mutation-intent`, `legacy-mutation`,
613
+ `publish-intent`, and `publish-final`. The field is optional on all of them: an
614
+ entry written before the field existed, which is every entry alpha.12 and
615
+ earlier wrote, stays valid, attributes nothing, and leaves the expected writer
616
+ `unknown`. Writer attribution alone therefore cannot turn such a finding into a
617
+ global barrier: it stays advisory `worktree-synchronization-required`. Local
618
+ state and current-checkout ownership are still judged first, and both remain
619
+ global barriers whatever the entry records. A publication resolves its identity
620
+ once under the claim lock, so its intent and its terminal name one writer, and
621
+ a terminal that recovery reconstructs after a lost response carries the
622
+ identity its intent recorded. When the journal names the current worktree as
623
+ the writer of the expected revision and no reachable ref carries that revision,
624
+ no sibling can ever produce it, so the finding is
625
+ `unauthorized-revision` and blocks every mutation instead of
626
+ `worktree-synchronization-required`, which blocks only its own item.
627
+
628
+ Recording the field on publication entries is an additive optional journal
629
+ change, so core `contract_version` stays `5` and every public request, success
630
+ envelope, and refusal envelope is unchanged. The item #122 lock-coarsening
631
+ parity golden, `test/publication-parity-baseline.json`, was regenerated to
632
+ record it: that recording is a work-claim contract change, never routine
633
+ fixture maintenance.
634
+
635
+ **What that identity discloses.** The identity is an opaque random UUID,
636
+ created once per worktree in that worktree's private Git directory. It contains
637
+ no path, branch, ref, hostname, user, or machine data. Journal entries are
638
+ projected into the tracked reconciliation log, so the identity persists in
639
+ committed Git history: any reader of that history can correlate every entry
640
+ written from one worktree, and an identity stays in that history indefinitely
641
+ after the worktree it named is removed. It never becomes a claim owner, and no
642
+ protocol decision reads it other than the writer comparison above.
643
+
644
+ **Ambiguous identity.** A UUID names one worktree. Before it reasons from a
645
+ recorded writer, the coordinator enumerates the worktrees Git currently reports
646
+ live and reads the identity each one already holds; it creates none. Two live
647
+ worktrees answering to one UUID, or a roster the coordinator could not finish
648
+ reading, refuses `claim-verify`, every claim-protected mutation,
649
+ `publish-claimed`, and `claim-adopt` before anything is classified or written.
650
+ The refusal keeps the existing exit `6`, `claim-store-unavailable`,
651
+ `claim-store-unreadable`, `state: "unchanged"` form and adds one
652
+ `error.details.identity_diagnostic`: `code: "duplicate-worktree-identity"` with
653
+ `worktree_id` and `live_worktree_count`, or `code:
654
+ "worktree-enumeration-failed"` with no further member. Auto-commit surfaces the
655
+ same diagnostic inside its `auto-commit-preflight-failed` details with
656
+ `retryable: false`. Nothing else about the envelope changes, and core
657
+ `contract_version` stays `5`.
658
+
659
+ Detection covers only what Git reports live. A worktree Git marks prunable —
660
+ including one whose path is temporarily unavailable or unmounted — is excluded,
661
+ so a duplicate identity held there is not detected until Git sees that worktree
662
+ live again. Removing a worktree removes the private Git directory that held its
663
+ identity, and nothing restores it: a worktree recreated at the same path earns
664
+ a new UUID, and the removed UUID stays in journal history attributing nothing.
665
+
365
666
  ### 3.2 Recovering from a foreign-writer block
366
667
 
367
668
  1. Stop writing the affected item in the blocked worktree. Unrelated item
368
669
  mutations may proceed when the finding is only
369
670
  `worktree-synchronization-required`.
671
+
370
672
  2. Read the finding. It names the item path and expected revision. When Git can
371
673
  prove ownership, it also names `owner_ref` and `owner_commit`; otherwise it
372
674
  carries `owner_unavailable: true`.
373
675
  3. If an owner is named, WAIT for that owner to publish the commit, then
374
676
  synchronize this checkout to it. Do not merge unrelated live work.
375
- 4. If ownership is unavailable, inspect reachable or dangling commits. Restore
376
- the exact authorized bytes or use explicit `claim-adopt` authority after
377
- review. Do not copy peer working-tree bytes.
378
- 5. Run `claim-verify --ledger <dir> --json` in the blocked worktree and require
379
- exit 0.
677
+ 4. If ownership is unavailable, the `remediation` string separates three cases.
678
+ When it says the expected revision is not yet reachable, the owning
679
+ worktree has not committed it yet: wait as in step 3, then synchronize.
680
+ When it says the revision is reachable in Git while no active named worktree
681
+ owner is established, do not wait: inspect the reachable history that carries
682
+ it, then restore the exact authorized bytes or use explicit `claim-adopt`
683
+ authority after review. When it says ownership cannot be established from
684
+ reachable refs, inspect reachable or dangling commits and remedy it the same
685
+ way. In neither inspection case do you copy peer working-tree bytes.
686
+ 5. Run `claim-verify --ledger <dir> --id <finding.item_id> --json` in the
687
+ blocked worktree and require exit 0.
380
688
  6. Resume the affected item.
381
689
 
690
+ Auto-commit applies this target scope both before and after its Git commit. A
691
+ successful mutation requires a valid ledger and no findings blocking the target
692
+ item, not a globally empty findings list. Nonblocking findings remain visible
693
+ in targeted verification.
694
+
695
+ **A target-scoped success is not a globally clean claim store.** The two scopes
696
+ answer different questions and a caller MUST NOT substitute one for the other.
697
+ `claim-verify --id <item>` keeps every finding visible, marks each with
698
+ `blocks_verification_scope`, and derives its exit from that item plus global
699
+ barriers. Bare `claim-verify` remains strict repository diagnosis and exits 6
700
+ while any item carries a blocking finding. A cooperating worker can therefore
701
+ finish its own item while bare verification still reports sibling work. Do not
702
+ hand-edit or adopt a sibling's item to force either scope clean.
703
+
382
704
  `claim-verify` in the writing worktree finalizes that worktree's own state; it
383
705
  does not make a sibling checkout able to see the item. Synchronization or
384
706
  explicit recovery is still required for the affected item.
@@ -420,10 +742,12 @@ answering with `namespace: "work-claim"`, `command: "claim-adopt"`, and
420
742
  mutation runs and no item file changes. It is not a `claim-verify` flag, because
421
743
  `claim-verify` is the read-mostly reconciliation report every remediation string
422
744
  names, and one command name must not mean both "tell me the state" and "change
423
- the authorization baseline". It is not a `claim` subcommand, because every claim
424
- lifecycle subcommand refuses with `publication-reconciliation-required` while
425
- reconciliation reports blocking findings, and adoption exists to clear exactly
426
- that state, so it runs while those findings stand.
745
+ the authorization baseline". It is not a `claim` subcommand, because
746
+ `work-claim.acquire` and `work-claim.renew` refuse with
747
+ `publication-reconciliation-required` while reconciliation reports blocking
748
+ findings, and adoption exists to clear exactly that state, so it runs while
749
+ those findings stand. `work-claim.release` also runs while they stand, for the
750
+ narrower reason that surrendering a lease takes no authority (section 5).
427
751
 
428
752
  The request is UTF-8 JSON with exactly these members:
429
753
 
@@ -556,6 +880,23 @@ fails or its durability is uncertain, the backend returns exit 6 with
556
880
  to guess. It cannot report a lease success or semantic rejection whose time
557
881
  was not persisted.
558
882
 
883
+ **A refused reconciliation has already advanced the floor.** Reconciliation
884
+ persists its clock entry before it classifies anything, so a command that then
885
+ refuses with exit 6 `claim-store-unavailable`, reason
886
+ `publication-reconciliation-required`, has still moved the durable floor to
887
+ `max(physical_utc, previous_floor)`. No claim, epoch, or ledger byte changes,
888
+ and the refusal reports none. The floor is monotonic, so the effect is on time
889
+ alone and it is permanent: once a refused reconciliation has published a floor
890
+ at or past a lease's `expires_at`, that lease can never read live again, and a
891
+ later acquire, renew, or fence check on the same tuple observes it expired.
892
+ On a ledger whose floor already runs ahead of this caller's wall clock, every
893
+ refused reconciliation republishes the higher floor, so a holder whose lease
894
+ looks live by its own clock may find it expired the moment it asks. A caller
895
+ that meets a barrier during a long recovery SHOULD expect to reacquire rather
896
+ than assume its lease survived. This is section 5's rule for a rejected
897
+ decision applied one step earlier: an advanced floor is never evidence that
898
+ anything succeeded.
899
+
559
900
  When `last_epoch` is `18446744073709551615` (the unsigned 64-bit maximum), a
560
901
  new acquire or takeover is impossible. After the authoritative decision time
561
902
  has been persisted, the backend returns exit 6 `epoch-exhausted` with message
@@ -570,6 +911,22 @@ member at any depth, and exactly the listed members. Unknown members, wrong
570
911
  types, noncanonical values, and unprovisioned namespaces are exit 2
571
912
  `invalid-request`; no authoritative lease decision has then occurred.
572
913
 
914
+ **A reconciliation barrier stops a caller from taking or extending authority,
915
+ never from surrendering it.** `work-claim.acquire` and `work-claim.renew` MUST
916
+ refuse with exit 6 `claim-store-unavailable`, reason
917
+ `publication-reconciliation-required`, and the reconciliation `findings`,
918
+ whenever reconciliation reports a finding that blocks the request's item.
919
+ `work-claim.release` MUST NOT refuse for that reason. The holder has to be able
920
+ to hand the lease back: no other worktree can take the item over while the
921
+ claim is held, and the worktree holding it is often the one least able to clear
922
+ the barrier. Refusing the surrender strands the lease and the item with it.
923
+
924
+ Release keeps every other refusal. An identity the domain cannot resolve, a
925
+ journal it cannot read, and a clock floor it cannot persist all refuse before
926
+ any lease decision, and the owner, epoch, and expected-expiry CAS tuple below
927
+ still rules on the request: a mismatch is exit 4 `claim-conflict` and the claim
928
+ stays held. A barrier never turns a release into an unconditional discard.
929
+
573
930
  ### Read
574
931
 
575
932
  `work-claim.read` accepts exactly:
@@ -641,6 +998,10 @@ It uses the same precedence. Success sets `active` to `null`, retains
641
998
  `last_epoch`, and returns `released_claim` plus `read_back`. A later acquire
642
999
  must allocate a greater epoch, preventing ABA even across restart.
643
1000
 
1001
+ A reconciliation barrier does not refuse a release; it refuses only acquire and
1002
+ renew (section 5 preamble). The CAS tuple still applies, so a release under a
1003
+ barrier is exactly as conditional as a release without one.
1004
+
644
1005
  Success envelopes for these three commands have exactly `ok`, `namespace`,
645
1006
  `command`, `contract_version`, `state: "committed"`, and `result`. Semantic
646
1007
  failures replace `result` with `error`, use `state: "unchanged"`, and include
@@ -805,18 +1166,21 @@ recovers an owner only when the OS reports the PID absent.
805
1166
 
806
1167
  That reconciliation is unconditional. It is not conditioned on an unresolved
807
1168
  `publish-intent`, because the commit-per-mutation invariant (section 1) binds
808
- `publish-claimed` exactly as it binds `create`, `transition`, and `patch`, and
809
- an uncommitted legacy mutation leaves no publish-intent behind. When
810
- reconciliation produces any blocking finding, `publish-claimed` MUST refuse
811
- with exit 6 `claim-store-unavailable`,
1169
+ `publish-claimed` exactly as it binds `create`, `transition`, `parent-migrate`,
1170
+ `snooze`, and `patch`, and an uncommitted legacy mutation leaves no
1171
+ publish-intent behind. When reconciliation produces any blocking finding,
1172
+ `publish-claimed` MUST refuse with exit 6 `claim-store-unavailable`,
812
1173
  `details.reason: "publication-reconciliation-required"`, and
813
1174
  `details.findings` set to those findings. `state` MUST be `unchanged`: the
814
1175
  refused publication wrote no item byte. Reconciliation itself still writes —
815
1176
  a clock entry, the terminals it resolved, and the finalizations it observed —
816
- because those record what was already true, never a new item revision. This is
817
- the identical refusal section 7 defines for a legacy write, so one uncommitted
818
- mutation blocks every mutating command alike, and one `claim-verify` clears
819
- them all.
1177
+ because those record what was already true, never a new item revision.
1178
+
1179
+ This is the identical refusal section 7 defines for a legacy write. The
1180
+ authorized predecessor/successor window from section 1 produces no blocking
1181
+ finding, so not every uncommitted mutation refuses the next command. When
1182
+ blocking findings do exist, `claim-verify` is the one reconciliation procedure
1183
+ for all of them.
820
1184
 
821
1185
  That refusal also outranks step 5. The numbered precedence orders a backend
822
1186
  that judges a candidate against an authoritative ledger; a merge-coordinated
@@ -839,13 +1203,14 @@ process MAY recover the lock only when the operating system reports that owner
839
1203
  process as absent. A live or malformed lock remains `claim-store-unavailable`;
840
1204
  elapsed time alone never authorizes lock recovery.
841
1205
 
842
- `claim-verify` takes the ledger path and no request body. Under the namespace
843
- lock, it replays the journal, advances and persists the clock floor, and
844
- reconciles pending intents against the exact item revision. It also compares
845
- successful publications with Git `HEAD`. When `HEAD` contains the committed
846
- revision, it appends one idempotent `publish-finalization` entry that records
847
- the Git commit. It writes a per-namespace reconciliation log outside the shared
848
- journal; that log is a derived audit artifact, not authority.
1206
+ `claim-verify` takes the ledger path, optional target item ID, and no request
1207
+ body. The target ID is syntax-validated without requiring local item presence.
1208
+ Under the namespace lock, it replays the journal, advances and persists the
1209
+ clock floor, and reconciles pending intents against exact item revisions. It
1210
+ also compares successful publications with Git `HEAD`. When `HEAD` contains a
1211
+ committed revision, it appends one idempotent `publish-finalization` entry that
1212
+ records the Git commit. It writes a per-namespace reconciliation log outside
1213
+ the shared journal; that log is a derived audit artifact, not authority.
849
1214
 
850
1215
  The log projects only journal entries that record a decision. `clock` and
851
1216
  `publish-finalization` entries are never projected. A command therefore writes
@@ -880,13 +1245,19 @@ not a claim finding, and a caller MUST NOT treat it as one. It is the honest
880
1245
  statement that a clean claim answer is not a clear road, and the caller must
881
1246
  repair validation before the next mutation will run.
882
1247
 
883
- A clean verification returns exit 0 and `state: "committed"`. Findings named
884
- `pending-intent-resolved` are clean recovery. Any
885
- `legacy-mutation-outcome-unknown`, `publication-outcome-unknown`,
886
- `revision-regression`, or `stale-write-detected` finding returns exit 6 and
887
- `state: "unknown"`. A caller MUST stop publication work and inspect those
888
- findings. Repeating verification MUST NOT duplicate a publication
889
- finalization.
1248
+ A verification result always names `verification_scope`, and every finding
1249
+ states `blocks_verification_scope`. Bare mode treats every blocking repository
1250
+ finding as blocking. Target mode keeps unrelated target-scoped findings
1251
+ visible but nonblocking; findings classified as global still block. A clean
1252
+ scope returns exit 0 and `state: "committed"`. This state describes durable
1253
+ coordinator reconciliation, not Git finalization of every successful
1254
+ publication; callers must inspect each `result.publications` row's
1255
+ `git_finalized` and `git_commit`. Findings named `pending-intent-resolved` are
1256
+ visible and nonblocking. Any blocking `legacy-mutation-outcome-unknown`,
1257
+ `publication-outcome-unknown`, `revision-regression`, or
1258
+ `stale-write-detected` finding returns exit 6 and `state: "unknown"`. A caller
1259
+ MUST stop publication work and inspect those findings. Repeating verification
1260
+ MUST NOT duplicate a publication finalization.
890
1261
 
891
1262
  Every finding that blocks a mutation MUST carry a `remediation` string, and
892
1263
  that string MUST name both the action to take and `claim-verify`. It MUST also
@@ -948,9 +1319,9 @@ without a second ledger write.
948
1319
 
949
1320
  Every refusal in this section answers in the `ledger-mutation` namespace with
950
1321
  `contract_version: 1` and `command: "<core command>-v1"`, even though the caller
951
- invoked the core `create`, `transition`, or `patch` command. This is a pinned
952
- version 1 consumer surface; it is not the core mutation envelope and MUST NOT be
953
- re-wrapped in one.
1322
+ invoked the core `create`, `transition`, `parent-migrate`, `snooze`, or `patch`
1323
+ command. This is a pinned version 1 consumer surface; it is not the core
1324
+ mutation envelope and MUST NOT be re-wrapped in one.
954
1325
 
955
1326
  For a fenced or merge-coordinated capability, legacy transition MUST check the
956
1327
  active claim under the same namespace lock and return exit 4
@@ -960,18 +1331,24 @@ create MUST reject any item identity whose tuple has claim history with exit 4
960
1331
  high-water mark. Both checks persist authoritative decision time before their
961
1332
  response.
962
1333
 
963
- Before a merge-coordinated backend permits a legacy transition or patch to
964
- write bytes, it MUST fsync a `legacy-mutation-intent` with the expected and
965
- candidate revisions. Under the same namespace lock, it MUST reserve journal
966
- capacity for that intent, one recovery clock entry, and one terminal entry. A
967
- committed write appends `legacy-mutation`; an unchanged write appends
968
- `legacy-mutation-abort`. Reconciliation resolves a pending intent to the
969
- committed terminal when the candidate revision is present, or to the abort
1334
+ Before a merge-coordinated backend permits a legacy create, transition, or
1335
+ patch to write bytes, it MUST fsync a `legacy-mutation-intent` carrying the
1336
+ expected and candidate revisions. Under the same namespace lock, it MUST
1337
+ reserve journal capacity for that intent, one recovery clock entry, and one
1338
+ terminal entry. A committed write appends `legacy-mutation`; an unchanged write
1339
+ appends `legacy-mutation-abort`. Reconciliation resolves a pending intent to
1340
+ the committed terminal when the candidate revision is present, or to the abort
970
1341
  terminal when the expected revision remains. Any third revision produces
971
1342
  `legacy-mutation-outcome-unknown`. The latest committed terminal is the
972
1343
  authorized expected revision. A later unrecorded revision remains a stale
973
1344
  write.
974
1345
 
1346
+ A create has no predecessor, so its intent carries `expected_revision: null`,
1347
+ which is valid only for `create-v1`, and the item path's absence is what
1348
+ resolves it to the abort terminal. Section 3.1 states the complete create
1349
+ ordering, its allocation fence, and the alpha.13 cutover the widened grammar
1350
+ forces.
1351
+
975
1352
  A merge-coordinated backend MUST reconcile before it authorizes a legacy write,
976
1353
  and section 6 states the same requirement for `publish-claimed`.
977
1354
  When reconciliation produces any blocking finding, the legacy command MUST
@@ -1086,9 +1463,10 @@ merge-coordinated backend returns when reconciliation found blocking findings,
1086
1463
  and it is the reason an uncommitted prior mutation produces. **`claim-verify`
1087
1464
  is its reconciliation procedure.** The envelope also carries
1088
1465
  `details.findings`; act on each finding's `remediation` string, then run
1089
- `wowbagger claim-verify --ledger <dir> --json` and require exit 0 before
1090
- repeating the refused command. Nothing else reconciles the journal, and no
1091
- other verb is needed.
1466
+ `wowbagger claim-verify --ledger <dir> --id <finding.item_id> --json` for the
1467
+ affected item and require exit 0 before repeating that item's refused command.
1468
+ Use bare verification only for strict repository diagnosis. Nothing else
1469
+ reconciles the journal, and no other verb is needed.
1092
1470
 
1093
1471
  This code was added after the version 1 vectors were written. It is additive:
1094
1472
  it names a condition the original text did not model, changes no existing code,