wowbagger 0.1.0-alpha.1 → 0.1.0-alpha.2

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.
@@ -0,0 +1,599 @@
1
+ # Work-claim contract
2
+
3
+ Status: accepted protocol design. The standalone Wowbagger CLI implements the
4
+ version 1 claim operations and the merge-coordinated Git-journal profile for
5
+ provisioned Git-backed ledgers. The no-I/O reference model and conformance
6
+ fixtures remain the oracle for the strict fenced protocol.
7
+
8
+ This document defines version 1 of the transport-neutral work-claim and
9
+ claimed-publication API, plus the merge-coordinated capability profile. The
10
+ words MUST, MUST NOT, SHOULD, and MAY are normative. JSON examples show objects
11
+ before compact serialization; a CLI prints exactly one compact JSON object
12
+ followed by LF.
13
+
14
+ Work-claim version negotiation uses
15
+ `result.operations.work_claim.api_version` from
16
+ `claim capabilities --ledger <dir> --json`. The top-level
17
+ `contract_version: 1` remains the legacy claim-envelope marker. It is not the
18
+ core mutation contract version and MUST NOT be compared with the
19
+ `contract_version` from core `capabilities --json`.
20
+
21
+ Generic consumers migrate without a wire change: they first identify the
22
+ work-claim envelope by `namespace: "work-claim"`, then require the advertised
23
+ `api_version`. Existing version 1 consumers can keep exact-member validation.
24
+
25
+ ## 1. Safety boundary
26
+
27
+ A claim is a durable lease for one work item in one ledger. It is not a Git
28
+ branch, file lock, lifecycle field, assignment, or proof that a later write
29
+ succeeded. Git history can retain evidence, but Git cannot atomically compare a
30
+ claim at a ledger publication boundary.
31
+
32
+ There are four state classes:
33
+
34
+ | State | Authority |
35
+ |---|---|
36
+ | ledger bytes and revision | durable ledger store |
37
+ | claim, epoch high-water mark, clock floor | durable coordinator |
38
+ | publication outcome by operation identity | durable coordinator |
39
+ | preflight, retry, and self-fencing cache | disposable process memory |
40
+
41
+ A backend is safely fenced only if one transactional coordinator serializes
42
+ claim decisions, the monotonic clock floor, every write path that can mutate a
43
+ claimed item, the ledger publication, and its idempotency outcome. A separate
44
+ claim service plus an ordinary file rename is advisory.
45
+
46
+ The shipped Git-journal profile is intentionally weaker. It serializes
47
+ cooperating claim decisions and records publication intent before the item
48
+ write. Git history and `claim-verify` then finalize or reject the outcome. It
49
+ does not make the claim decision and Git commit one atomic transaction, so it
50
+ MUST report `safe_exclusive_dispatch: false`.
51
+
52
+ ## 2. Ledger namespace and identity
53
+
54
+ Every claim key is the immutable tuple `(ledger_namespace, item_id)`. No state,
55
+ request, fence, read-back, or publication may omit either member.
56
+
57
+ `ledger_namespace` is a provisioned ASCII identifier matching exactly:
58
+
59
+ wbns_[a-f0-9]{32}
60
+
61
+ It is not inferred from a path, repository URL, clone, worktree, display name,
62
+ or item ID. Provisioning creates a namespace once and binds it to one logical
63
+ ledger. Moving or cloning that same logical ledger retains the binding; making
64
+ an independent logical ledger requires a new namespace. Rebinding a namespace
65
+ to different ledger history is forbidden. A shared endpoint MUST use an
66
+ explicit allowlist or equally strong durable mapping and MUST reject an
67
+ unprovisioned namespace before consulting claim state.
68
+
69
+ `item_id` retains Wowbagger's canonical `wb_` identity syntax. Equal item IDs
70
+ in different ledger namespaces are unrelated: their claims, epoch counters,
71
+ clock floors, publications, and idempotency outcomes cannot collide.
72
+
73
+ `owner_id` identifies one worker run and matches
74
+ `[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}`. A fresh collision-resistant value is
75
+ required for every run. It is not a credential.
76
+
77
+ `epoch` is a canonical unsigned 64-bit decimal string. `"0"` is only the
78
+ unallocated high-water mark. Active epochs match `[1-9][0-9]{0,19}` and are at
79
+ most `18446744073709551615`. Epochs never wrap, decrement, or get reused.
80
+
81
+ ## 3. Capability discovery and write-path closure
82
+
83
+ `work-claim.capabilities` accepts exactly `{}`. A strictly fenced response is:
84
+
85
+ ```json
86
+ {
87
+ "ok": true,
88
+ "namespace": "work-claim",
89
+ "command": "capabilities",
90
+ "contract_version": 1,
91
+ "result": {
92
+ "backend": {
93
+ "name": "example-backend",
94
+ "coordination_scope": "shared-transactional-coordinator",
95
+ "ledger_binding": {
96
+ "mode": "explicit-allowlist",
97
+ "namespaces": ["wbns_11111111111111111111111111111111"]
98
+ }
99
+ },
100
+ "operations": {
101
+ "work_claim": {
102
+ "supported": true,
103
+ "api_version": 1,
104
+ "mode": "fenced",
105
+ "claim_protected_publication": true,
106
+ "fencing_enforced_at": "ledger-publication-commit-boundary",
107
+ "safe_exclusive_dispatch": true,
108
+ "write_paths": {
109
+ "alternate": "none",
110
+ "claimed_publication_v1": "atomic-fence",
111
+ "legacy_create_v1": "reject-claimed-id",
112
+ "legacy_transition_v1": "reject-active-claim"
113
+ }
114
+ }
115
+ }
116
+ }
117
+ }
118
+ ```
119
+
120
+ The work-claim capability envelope reports one ledger's provisioned claim
121
+ profile. Its `namespace: "work-claim"` member and ledger-bound `backend`
122
+ identify this capability context. It is distinct from the unbound default claim
123
+ profile in the core `capabilities --json` response. A caller MUST use
124
+ `claim capabilities --ledger <dir> --json` and MUST gate `publish-claimed` on
125
+ that ledger-specific response.
126
+
127
+ `safe_exclusive_dispatch` may be `true` only when all of the following hold:
128
+
129
+ 1. claim state, epoch high-water marks, clock floors, and operation outcomes
130
+ are durable;
131
+ 2. acquire, renew, release, expiry, and takeover obey this contract;
132
+ 3. `publish-claimed` fences and publishes atomically;
133
+ 4. legacy transition rejects an active claimed item inside the same
134
+ coordinator transaction;
135
+ 5. legacy create rejects an identity with claim history inside that
136
+ transaction; and
137
+ 6. every other mutation entry point is absent or participates in the same
138
+ atomic fence.
139
+
140
+ The coordinator scope MUST be `shared-transactional-coordinator`, the ledger
141
+ binding MUST be an explicit non-empty allowlist, and the advertised binding
142
+ must cover the provisioned namespaces. A `local-filesystem` scope or an empty
143
+ allowlist is advisory even when the four write-path values otherwise match.
144
+
145
+ For a strictly fenced capability, the backend MUST enumerate every entry point.
146
+ An unknown, uncoordinated, plugin, maintenance, import, alternate, or
147
+ direct-write path is a bypass. Any bypass prevents `mode: "fenced"` and
148
+ `safe_exclusive_dispatch: true`.
149
+
150
+ A provisioned Git-journal backend MAY instead report:
151
+
152
+ ```json
153
+ {
154
+ "backend": {
155
+ "name": "local-filesystem-git-journal",
156
+ "coordination_scope": "shared-git-common-dir-serialized-journal",
157
+ "ledger_binding": {
158
+ "mode": "explicit-allowlist",
159
+ "namespaces": ["wbns_11111111111111111111111111111111"]
160
+ }
161
+ },
162
+ "operations": {
163
+ "work_claim": {
164
+ "supported": true,
165
+ "api_version": 1,
166
+ "mode": "merge-coordinated",
167
+ "claim_protected_publication": true,
168
+ "fencing_enforced_at": "git-history-reconciliation",
169
+ "safe_exclusive_dispatch": false,
170
+ "write_paths": {
171
+ "alternate": "none",
172
+ "claimed_publication_v1": "git-journal-fence",
173
+ "legacy_create_v1": "reject-claimed-id",
174
+ "legacy_transition_v1": "reject-active-claim"
175
+ }
176
+ }
177
+ }
178
+ }
179
+ ```
180
+
181
+ `merge-coordinated` means that one shared Git common-directory journal
182
+ serializes cooperating claim decisions, publication intents, and terminal
183
+ outcomes. `publish-claimed` MUST validate the active fence and expected
184
+ revision under that journal lock before it writes the item. It MUST record the
185
+ intent durably before the item write. `claim-verify` MUST reconcile the working
186
+ tree and Git history before a later fenced operation proceeds.
187
+
188
+ This profile does not control direct writes, hostile processes, other clones,
189
+ or alternate tools. A caller MUST NOT use it for exclusive dispatch. A
190
+ Git-backed ledger without a provisioned namespace remains advisory:
191
+ `mode: "advisory"`, `claim_protected_publication: false`,
192
+ `fencing_enforced_at: "none"`, and `safe_exclusive_dispatch: false`.
193
+ An advisory endpoint MUST reject `publish-claimed`; a caller must never upgrade
194
+ an advisory capability locally.
195
+
196
+ ## 4. Durable claim and authoritative time
197
+
198
+ The normalized durable record is:
199
+
200
+ ```json
201
+ {
202
+ "ledger_namespace": "wbns_11111111111111111111111111111111",
203
+ "item_id": "wb_01Q4837BM01W70T30B184GG1R6",
204
+ "last_epoch": "8",
205
+ "active": {
206
+ "owner_id": "agent-example-run-1",
207
+ "epoch": "8",
208
+ "issued_at": "2030-01-11T09:00:00.000Z",
209
+ "expires_at": "2030-01-11T09:05:00.000Z"
210
+ }
211
+ }
212
+ ```
213
+
214
+ `active` is either that exact object or `null`. Its epoch equals `last_epoch`.
215
+ An untouched tuple reads as `last_epoch: "0"` and `active: null`.
216
+
217
+ Instants use exactly `YYYY-MM-DDTHH:MM:SS.mmmZ`. `lease_duration_ms` is a JSON
218
+ integer from 1 through 86,400,000. A lease is active exactly while
219
+ `effective_now < expires_at`; equality is expired.
220
+
221
+ The backend chooses `effective_now = max(physical_utc, durable_clock_floor)`
222
+ within the declared namespace scope. Client clocks never authorize a lease.
223
+ For every authoritative lease decision—including successful and rejected
224
+ acquire, renew, release, takeover, publication fence check, and legacy
225
+ active-claim guard—the backend MUST durably persist a clock floor at least as
226
+ large as `effective_now` before returning the decision. On success, the floor,
227
+ claim or ledger change, and operation outcome commit atomically. On rejection,
228
+ the advanced floor and the unchanged claim/ledger result commit atomically.
229
+
230
+ Restart recovers the floor before deciding anything. A backward wall-clock
231
+ step therefore cannot resurrect earlier effective time. If floor persistence
232
+ fails or its durability is uncertain, the backend returns exit 6 with
233
+ `clock-floor-persistence-failed`, makes no claim or ledger change, and refuses
234
+ to guess. It cannot report a lease success or semantic rejection whose time
235
+ was not persisted.
236
+
237
+ When `last_epoch` is `18446744073709551615` (the unsigned 64-bit maximum), a
238
+ new acquire or takeover is impossible. After the authoritative decision time
239
+ has been persisted, the backend returns exit 6 `epoch-exhausted` with message
240
+ `The epoch high-water mark is exhausted.` and details containing the claim
241
+ tuple and `last_epoch`. The claim, epoch high-water mark, and ledger remain
242
+ unchanged; epochs MUST NOT wrap.
243
+
244
+ ## 5. Claim requests and CAS rules
245
+
246
+ All public requests are UTF-8 JSON with one top-level object, no duplicate
247
+ member at any depth, and exactly the listed members. Unknown members, wrong
248
+ types, noncanonical values, and unprovisioned namespaces are exit 2
249
+ `invalid-request`; no authoritative lease decision has then occurred.
250
+
251
+ ### Read
252
+
253
+ `work-claim.read` accepts exactly:
254
+
255
+ ```json
256
+ {"ledger_namespace":"wbns_11111111111111111111111111111111","item_id":"wb_01Q4837BM01W70T30B184GG1R6"}
257
+ ```
258
+
259
+ If the tuple has never been provisioned, it returns the same successful empty
260
+ state with `last_epoch: "0"` and `active: null`; namespaces remain isolated.
261
+ It returns `result.read_back` with exactly `ledger_namespace`, `item_id`,
262
+ `observed_at`, `last_epoch`, and `active`. A read is evidence, not a future
263
+ reservation. It is the recovery operation after a lost claim response: the
264
+ caller reads the tuple before retrying an acquire, renew, or release.
265
+
266
+ ### Acquire and takeover
267
+
268
+ `work-claim.acquire` accepts exactly:
269
+
270
+ ```json
271
+ {
272
+ "ledger_namespace": "wbns_11111111111111111111111111111111",
273
+ "item_id": "wb_01Q4837BM01W70T30B184GG1R6",
274
+ "owner_id": "agent-example-run-1",
275
+ "lease_duration_ms": 300000,
276
+ "expected": {"last_epoch":"7","active":null}
277
+ }
278
+ ```
279
+
280
+ `expected` is a CAS witness over the complete `last_epoch` and `active` object.
281
+ After persisting the decision time, precedence is:
282
+
283
+ 1. unequal witness: exit 4 `claim-conflict`;
284
+ 2. equal witness with unexpired active claim: exit 4 `claim-held`;
285
+ 3. exhausted high-water mark: exit 6 `epoch-exhausted`; or
286
+ 4. allocate exactly `last_epoch + 1`, replace `active`, atomically commit, and
287
+ return exit 0 with `claim` and `read_back`.
288
+
289
+ Acquiring an expired record is takeover and always advances the epoch.
290
+
291
+ ### Renew
292
+
293
+ `work-claim.renew` accepts exactly:
294
+
295
+ ```json
296
+ {
297
+ "ledger_namespace": "wbns_11111111111111111111111111111111",
298
+ "item_id": "wb_01Q4837BM01W70T30B184GG1R6",
299
+ "owner_id": "agent-example-run-1",
300
+ "epoch": "8",
301
+ "expected_expires_at": "2030-01-11T09:05:00.000Z",
302
+ "lease_duration_ms": 300000
303
+ }
304
+ ```
305
+
306
+ Owner, epoch, and expected expiry are one CAS tuple. Mismatch is exit 4
307
+ `claim-conflict`; an exactly matching but expired tuple is exit 4
308
+ `claim-expired`. Success retains `issued_at` and epoch, sets expiry from the
309
+ persisted decision time, and returns `claim` plus `read_back`.
310
+
311
+ ### Release
312
+
313
+ `work-claim.release` accepts the renew object without `lease_duration_ms`.
314
+ It uses the same precedence. Success sets `active` to `null`, retains
315
+ `last_epoch`, and returns `released_claim` plus `read_back`. A later acquire
316
+ must allocate a greater epoch, preventing ABA even across restart.
317
+
318
+ Success envelopes for these three commands have exactly `ok`, `namespace`,
319
+ `command`, `contract_version`, `state: "committed"`, and `result`. Semantic
320
+ failures replace `result` with `error`, use `state: "unchanged"`, and include
321
+ the exact normalized read-back in `error.details`.
322
+
323
+ ## 6. Claimed publication API
324
+
325
+ The public operation is `ledger-publication.publish-claimed` version 1. It
326
+ accepts exactly:
327
+
328
+ ```json
329
+ {
330
+ "operation_id": "pub_agent-example-run-1_0001",
331
+ "ledger_namespace": "wbns_11111111111111111111111111111111",
332
+ "item_id": "wb_01Q4837BM01W70T30B184GG1R6",
333
+ "expected_revision": "sha256:9160d4be34c8695bd172a76c7c7966587ea5a4d991ad22c87b2b91af54aa9ebb",
334
+ "candidate_source_base64": "YWZ0ZXIK",
335
+ "candidate_sha256": "sha256:7b9a72466d3960eb2aacccfc848939453490db0678bd4725def3f789b891c919",
336
+ "claim_fence": {
337
+ "ledger_namespace": "wbns_11111111111111111111111111111111",
338
+ "item_id": "wb_01Q4837BM01W70T30B184GG1R6",
339
+ "owner_id": "agent-example-run-1",
340
+ "epoch": "8"
341
+ }
342
+ }
343
+ ```
344
+
345
+ `operation_id` matches `[A-Za-z0-9][A-Za-z0-9._:-]{0,127}` and identifies the
346
+ entire immutable request. The backend computes `operation_digest` as
347
+ `sha256:` plus the SHA-256 of canonical UTF-8 JSON for the complete request
348
+ (object keys sorted lexicographically, no insignificant whitespace, and no
349
+ duplicate members). It stores that digest with the durable terminal outcome
350
+ and compares it before any revision, clock, fence, or candidate-ledger
351
+ decision. `expected_revision` and `candidate_sha256` are
352
+ lowercase `sha256:` plus 64 hexadecimal digits. `candidate_source_base64` is
353
+ canonical padded RFC 4648 base64 without whitespace and decodes to at most
354
+ 8,388,608 bytes. Its SHA-256 MUST equal `candidate_sha256`. The candidate is
355
+ the complete replacement ledger source, not a patch.
356
+
357
+ The CLI bounds the complete serialized `publish-claimed` request at 11,534,336
358
+ bytes before end-of-stream or JSON parsing. This limit admits every valid
359
+ 8,388,608-byte candidate plus the bounded envelope fields. Larger input returns
360
+ exit 2 `invalid-request`.
361
+
362
+ Every public commit attempt, including a retry after response loss, MUST carry
363
+ the complete request. An operation ID alone is not a retry request and returns
364
+ exit 2 `invalid-request` with message `The publish-claimed retry must include
365
+ its complete request.`
366
+
367
+ Validation and decision precedence is normative:
368
+
369
+ 1. strict JSON and exact schema;
370
+ 2. canonical identifiers, sizes, base64, and candidate digest;
371
+ 3. provisioned namespace and ledger binding;
372
+ 4. durable `operation_id` lookup: the same `operation_digest` returns its
373
+ stored terminal envelope; a different digest is exit 4
374
+ `idempotency-conflict`;
375
+ 5. ordinary candidate-ledger validation;
376
+ 6. enter the backend's serialized decision boundary and persist authoritative
377
+ decision time;
378
+ 7. require fence namespace equal request namespace, then fence item equal
379
+ request item, then an active claim, then matching owner, then matching epoch,
380
+ then an unexpired claim;
381
+ 8. require the durable ledger revision equal `expected_revision`; and
382
+ 9. publish the exact candidate bytes and store or journal the outcome as the
383
+ selected capability profile requires.
384
+
385
+ The first failure wins. Publication errors use these exact codes and messages:
386
+
387
+ Candidate validation MUST parse the complete replacement bytes as a schema
388
+ version 1 ledger item and require its canonical `id` to equal the request's
389
+ `item_id`. Arbitrary text, a different item, or an invalid schema is exit 3
390
+ `ledger-invalid` before a preflight is retained or any publication mutation.
391
+
392
+ | Step | Exit and code | Message |
393
+ |---:|---|---|
394
+ | 1 | 2 `invalid-request` | `The request is not unique-key UTF-8 JSON.` |
395
+ | 2 | 2 `invalid-request` | `The request does not match publish-claimed version 1.` |
396
+ | 2, base64 | 2 `invalid-request` | `The candidate source is not canonical base64.` |
397
+ | 2, digest | 2 `candidate-digest-mismatch` | `The candidate digest does not match the candidate source.` |
398
+ | 3 | 2 `ledger-namespace-unbound` | `The ledger namespace is not provisioned for this endpoint.` |
399
+ | 4 | 4 `idempotency-conflict` | `The operation identity is already bound to a different request.` |
400
+ | 5 | 3 `ledger-invalid` | `The candidate ledger is invalid.` |
401
+ | 6 | 6 `clock-floor-persistence-failed` | `The authoritative clock floor could not be persisted.` |
402
+ | 6 | 6 `publication-outcome-unknown` | `The publication outcome could not be determined.` |
403
+ | 7 | 4 `claim-fence-rejected` | `The supplied claim fence is not the active owner generation.` |
404
+ | 8 | 4 `ledger-revision-conflict` | `The durable ledger revision no longer matches this publication.` |
405
+
406
+ An advisory capability has no atomic publication boundary. It MUST reject
407
+ `publish-claimed` before preflight or commit with exit 2
408
+ `capability-unavailable`, message `Claim-protected publication is unavailable
409
+ on an advisory backend.`, and `details.reason: "advisory-capability"`.
410
+
411
+ The `operation_id` member of that refusal depends on when the backend
412
+ refuses. A backend that has read and validated the request — the
413
+ `advisory-publication-rejection` reference transcript's coordinator-backed
414
+ model — echoes the request's `operation_id`. A backend that refuses
415
+ categorically before reading any input, as the unprovisioned local CLI does,
416
+ MUST omit `operation_id`: it cannot echo what it never read, and inventing one
417
+ would be a guess. A conformance comparison against the reference transcript
418
+ therefore excludes `operation_id` when the backend under test refuses before
419
+ reading.
420
+
421
+ Steps 1 through 3 use `state: "unchanged"` and deterministic `details` naming
422
+ the first invalid JSON pointer or namespace. Step 5 details are the ordered
423
+ ledger validation issues. Steps 6 through 8 include `ledger_namespace` and
424
+ `item_id`; fence details additionally use the fields and reason defined below,
425
+ and revision details contain `expected_revision` then `actual_revision`.
426
+
427
+ For a strictly fenced backend, steps 6 through 9 are one serialized commit
428
+ boundary. No takeover can occur between the fence decision and publication. A
429
+ preflight read or candidate validation never authorizes a write. A worker
430
+ paused after preflight at epoch N is rejected at commit after epoch N+1 takes
431
+ over.
432
+
433
+ Fence rejection uses exit 4 `claim-fence-rejected`, message `The supplied
434
+ claim fence is not the active owner generation.`, and one of these ordered
435
+ `details.reason` values: `ledger-namespace-mismatch`, `item-id-mismatch`,
436
+ `no-active-claim`, `owner-mismatch`, `epoch-mismatch`, or `claim-expired`.
437
+ Wrong owner with the correct epoch and correct owner with the wrong epoch are
438
+ both failures. Wrong ledger and wrong item never fall through to another
439
+ record. Revision mismatch is exit 4 `ledger-revision-conflict`.
440
+
441
+ A success envelope contains `operation_id` at top level and result fields
442
+ `ledger_namespace`, `item_id`, `committed_revision`, the exact `claim_fence`,
443
+ and `claim_read_back`. Publication does not renew or release the claim.
444
+
445
+ For a merge-coordinated backend, the same public request and decision
446
+ precedence apply, but Git commit is outside the journal lock. Under the lock,
447
+ the backend reconciles prior intents, persists the clock floor, checks
448
+ idempotency, fence, and revision, then fsyncs a `publish-intent` before writing
449
+ the candidate item bytes. Before the first journal append, it fsyncs each new
450
+ journal-directory entry and the empty journal file. It then appends a terminal
451
+ `publish-final` outcome.
452
+ The caller commits or merges the resulting item change and runs
453
+ `claim-verify`.
454
+
455
+ The namespace lock records its process owner before publication. A later
456
+ process MAY recover the lock only when the operating system reports that owner
457
+ process as absent. A live or malformed lock remains `claim-store-unavailable`;
458
+ elapsed time alone never authorizes lock recovery.
459
+
460
+ `claim-verify` takes the ledger path and no request body. Under the namespace
461
+ lock, it replays the journal, advances and persists the clock floor, and
462
+ reconciles pending intents against the exact item revision. It also compares
463
+ successful publications with Git `HEAD`. When `HEAD` contains the committed
464
+ revision, it appends one idempotent `publish-finalization` entry that records
465
+ the Git commit. It writes a per-namespace reconciliation log outside the shared
466
+ journal; that log is a derived audit artifact, not authority.
467
+
468
+ A clean verification returns exit 0 and `state: "committed"`. Findings named
469
+ `pending-intent-resolved` are clean recovery. Any
470
+ `publication-outcome-unknown`, `revision-regression`, or
471
+ `stale-write-detected` finding returns exit 6 and `state: "unknown"`. A caller
472
+ MUST stop publication work and inspect those findings. Repeating verification
473
+ MUST NOT duplicate a publication finalization.
474
+
475
+ The top-level `state: "committed"` describes durable reconciliation state, not
476
+ Git finalization of every successful publication. A caller MUST gate Git
477
+ completion on each `result.publications` entry's `git_finalized` and
478
+ `git_commit` values. `git_finalized: false` with `git_commit: null` means the
479
+ publication outcome is durable but the committed revision is not yet present
480
+ in Git `HEAD`.
481
+
482
+ `ledger-publication.read` accepts exactly
483
+ `{"operation_id":"...","ledger_namespace":"...","item_id":"..."}`
484
+ and returns the durable operation identity, `operation_digest`, and terminal
485
+ `outcome`. A missing operation returns exit 2 `operation-not-found` with
486
+ message `The publication operation outcome was not found.` and unchanged
487
+ state. If a commit response is lost, the caller MUST read this operation
488
+ outcome before retrying; an identical request then returns the stored envelope
489
+ without a second ledger write.
490
+
491
+ ## 7. Legacy and alternate writes
492
+
493
+ For a fenced or merge-coordinated capability, legacy transition MUST check the
494
+ active claim under the same namespace lock and return exit 4
495
+ `active-claim-write-refused` before changing an active claimed item. Legacy
496
+ create MUST reject any item identity whose tuple has claim history with exit 4
497
+ `claimed-item-write-refused`; this prevents recreation from bypassing an epoch
498
+ high-water mark. Both checks persist authoritative decision time before their
499
+ response.
500
+
501
+ An implementation may instead route a legacy write through `publish-claimed`,
502
+ but it cannot silently omit a fence. Administrative repair, bulk import,
503
+ plugins, direct database writes, and filesystem writers count as alternate
504
+ mutation paths. Their presence prevents a strict fenced capability. A
505
+ merge-coordinated backend may still operate for cooperating writers, but it
506
+ MUST report `safe_exclusive_dispatch: false`.
507
+
508
+ ## 8. Errors, exits, and recovery
509
+
510
+ Error envelopes contain exactly `ok: false`, namespace, command,
511
+ `contract_version: 1`, state, and `error` with `code`, `message`, and `details`.
512
+ Publication envelopes also contain `operation_id` once schema validation has
513
+ accepted it.
514
+
515
+ | Exit | Meaning | Required state |
516
+ |---:|---|---|
517
+ | 0 | committed success | `committed` |
518
+ | 2 | invalid syntax, schema, canonical value, digest, binding, capability, missing operation, or missing fence | `unchanged` |
519
+ | 3 | candidate ledger invalid | `unchanged` |
520
+ | 4 | CAS, held, expired, fence, revision, idempotency, or legacy refusal | `unchanged` |
521
+ | 5 | authentication or authorization refusal | `unchanged` |
522
+ | 6 | durable floor/result unavailable or epoch exhausted | `unchanged` or `unknown` as the code defines |
523
+
524
+ Stable messages used by the reference vectors are part of version 1. A backend
525
+ must not substitute free-form prose for the specified codes and details.
526
+
527
+ Claim-operation semantic messages are likewise exact:
528
+
529
+ | Code | Message |
530
+ |---|---|
531
+ | `claim-conflict` (acquire) | `The observed claim state no longer matches this request.` |
532
+ | `claim-conflict` (renew/release) | `The active claim tuple no longer matches this request.` |
533
+ | `claim-held` | `The item has an unexpired active claim.` |
534
+ | `claim-expired` | `The matching claim has expired.` |
535
+ | `clock-floor-persistence-failed` | `The authoritative clock floor could not be persisted.` |
536
+ | `active-claim-write-refused` | `Legacy transition cannot write an item with an active claim.` |
537
+ | `claimed-item-write-refused` | `Legacy create cannot write an item identity with claim history.` |
538
+ | `epoch-exhausted` | `The epoch high-water mark is exhausted.` |
539
+ | `capability-unavailable` | `Claim-protected publication is unavailable on an advisory backend.` |
540
+ | `operation-not-found` | `The publication operation outcome was not found.` |
541
+ | `idempotency-conflict` | `The operation identity is already bound to a different request.` |
542
+ | `publication-outcome-unknown` | `The publication outcome could not be determined.` |
543
+ | `claim-store-unavailable` | `The durable claim store is unavailable.` |
544
+
545
+ `claim-store-unavailable` is exit 6 with `state: "unchanged"`. It means the
546
+ backend could not reach the durable store that holds claims, epoch high-water
547
+ marks, and the clock floor, so no authoritative decision was possible and
548
+ nothing changed. It is not a statement about the request, which may be
549
+ perfectly valid.
550
+
551
+ The condition is deliberately generic: a backend whose coordinator is
552
+ unreachable and a backend that cannot locate its store at all both use it.
553
+ `details.reason` names the specific cause and is backend-defined — for example
554
+ `git-directory-not-found` where a backend keeps claim state inside a git
555
+ directory. A caller distinguishes causes through `details.reason`, never
556
+ through the message.
557
+
558
+ This code was added after the version 1 vectors were written. It is additive:
559
+ it names a condition the original text did not model, changes no existing code,
560
+ message, or envelope, and no reference vector emits it.
561
+
562
+ For a strictly fenced backend, the publication outcome and ledger change are
563
+ one atomic record. If the commit succeeds but the response is lost, retrying
564
+ the identical `operation_id` and request returns the stored success without
565
+ writing twice. Reusing the identity with different bytes or fence fails. If
566
+ failure occurs before the atomic commit, ledger and outcome remain unchanged.
567
+ If an implementation cannot establish which side of its commit boundary
568
+ occurred, it returns exit 6 `publication-outcome-unknown`; the caller reads the
569
+ outcome by operation ID before attempting anything else.
570
+
571
+ ## 9. Reference vectors and backend conformance
572
+
573
+ [`spec/fixtures/work-claims`](../spec/fixtures/work-claims/README.md) contains
574
+ version 2 normative reference-model vectors. Each manifest declares explicit
575
+ durable and process-local initial state, exact source bytes and SHA-256 digests,
576
+ the clock authority and floor, ordered CAS/barrier/fault/restart actions, every
577
+ exact envelope, and the exact final state.
578
+
579
+ The no-I/O state-machine runner executes those committed manifests in tests.
580
+ The fixture loader separately requires every manifest and source to be a real
581
+ regular file beneath the fixture root, using `lstat` and no-follow open; it
582
+ rejects traversal, symlinks, directories, and special files.
583
+
584
+ A passing reference-model vector proves that the normative model and committed
585
+ expected transcript agree. Independent hand-authored goldens and invariant /
586
+ tamper tests check critical safety properties without using the model's
587
+ expected transcript as an oracle. It does **not** prove that a future storage backend
588
+ is conformant. Backend conformance requires running the same public requests,
589
+ barriers, restarts, and fault schedule against that backend and comparing its
590
+ envelopes, durable read-back, and exact ledger bytes to the manifest.
591
+
592
+ ## 10. Current compatibility
593
+
594
+ This contract adds no members to schema version 1 Markdown items and changes no
595
+ create or transition request shape. Existing parsers continue to reject
596
+ unknown claim members. The shipped CLI implements the merge-coordinated
597
+ Git-journal profile for provisioned ledgers and reports
598
+ `safe_exclusive_dispatch: false`. Unprovisioned Git ledgers remain advisory,
599
+ and non-Git ledgers remain claim-unsupported.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wowbagger",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.2",
4
4
  "description": "Plain-Markdown, Git-native work ledger for coordinating agents — validate, ready-select, and mutate a task ledger from the CLI.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,6 +36,9 @@
36
36
  "src",
37
37
  "skills",
38
38
  "adapters",
39
+ "docs/mutation-contract.md",
40
+ "docs/work-claim-contract.md",
41
+ "scripts/migrate-schema-2.js",
39
42
  "README.md",
40
43
  "CHANGELOG.md",
41
44
  "LICENSE"
@@ -43,7 +46,7 @@
43
46
  "scripts": {
44
47
  "check": "npm test && git diff --check && git diff --cached --check",
45
48
  "test": "node --test test/*.test.js",
46
- "prepublishOnly": "node bin/wowbagger.js validate --ledger ledger --json >/dev/null && node --check bin/wowbagger.js"
49
+ "prepublishOnly": "node bin/wowbagger.js validate --ledger ledger --json >/dev/null && node --check bin/wowbagger.js && node scripts/verify-release-tag.js"
47
50
  },
48
51
  "dependencies": {
49
52
  "yaml": "^2.9.0"
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { formatSchemaMigrationError, runSchema2MigrationCli } from '../src/schema-migration.js';
3
+
4
+ runSchema2MigrationCli(process.argv.slice(2)).catch((error) => {
5
+ process.stderr.write(formatSchemaMigrationError(error));
6
+ process.exitCode = 1;
7
+ });