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.
- package/CHANGELOG.md +33 -0
- package/README.md +20 -9
- package/docs/mutation-contract.md +1036 -0
- package/docs/work-claim-contract.md +599 -0
- package/package.json +5 -2
- package/scripts/migrate-schema-2.js +7 -0
- package/skills/wowbagger/SKILL.md +27 -13
- package/src/claim-journal.js +2 -2
- package/src/claim-publication.js +29 -3
- package/src/cli.js +46 -5
- package/src/git-reconciliation.js +10 -6
- package/src/ledger.js +1 -0
- package/src/mutation.js +1 -1
- package/src/schema-migration.js +1 -1
|
@@ -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.
|
|
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
|
+
});
|