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
@@ -81,8 +81,9 @@ complete difference against published version 2 (`0.1.0-alpha.4`):
81
81
  - **the patchable title.** `title` joins the patchable set as a non-empty
82
82
  schema string that replaces the current title whole (section 9), and section
83
83
  9 gains the frontmatter ownership table that states, member by member, which
84
- members are core-owned, which are consumer-editable through `patch`, and
85
- which are create-once. The version stays 3 by the same argument the relations
84
+ members are core-owned, which are consumer-editable through `patch`, which
85
+ move through a dedicated command, and which are create-once. The version
86
+ stays 3 by the same argument the relations
86
87
  and body deltas used: this widens the patch request schema, and adds, removes,
87
88
  and renames no response envelope member. **Version 3 is published, so state
88
89
  the consequence plainly: a consumer probing for title-patch support cannot
@@ -190,11 +191,11 @@ and stops.
190
191
 
191
192
  The bootstrap wire, adapter approval, instruction, handoff, and fixture-format
192
193
  versions are separate version domains and remain version 1. The adapter
193
- contract remains version 2. The work-claim API moves to 2 with this release,
194
- for its own reason stated in
195
- [the work-claim contract](work-claim-contract.md), section 6: the item-source
196
- refusal replaces the version 1 error that an oversized candidate used to
197
- receive.
194
+ contract remains version 2. The work-claim API moved to 2 with this core
195
+ release because the item-source refusal replaced the version 1 error that an
196
+ oversized publication candidate used to receive. It later moved to 3 for
197
+ target-scoped `claim-verify`; the current value is always negotiated from
198
+ capabilities, never inferred from the core version.
198
199
 
199
200
  Version negotiation uses distinct existing fields. A core consumer MUST read
200
201
  the top-level `contract_version` from `capabilities --json`. A work-claim
@@ -258,7 +259,7 @@ and stops.
258
259
 
259
260
  The bootstrap wire, adapter approval, instruction, handoff, and fixture-format
260
261
  versions are separate version domains and remain version 1. The adapter
261
- contract remains version 2 and the work-claim API remains version 2. The list
262
+ contract remains version 2 and the work-claim API remains version 3. The list
262
263
  query and the workbench projection each have their own version domain,
263
264
  `query_version` and `projection_version`, both `1`: a consumer negotiates them
264
265
  through `result.operations.list.query_version` and
@@ -321,16 +322,18 @@ wowbagger inspect --ledger <dir> --id <id> --workbench --as-of YYYY-MM-DD --json
321
322
  wowbagger list --ledger <dir> --input <json-file|-> --json
322
323
  wowbagger create --ledger <dir> --input <json-file|-> --json [--auto-commit]
323
324
  wowbagger transition --ledger <dir> --input <json-file|-> --json [--auto-commit]
325
+ wowbagger parent-migrate --ledger <dir> --input <json-file|-> --json [--auto-commit]
326
+ wowbagger snooze --ledger <dir> --input <json-file|-> --json [--auto-commit]
324
327
  wowbagger patch --ledger <dir> --input <json-file|-> --json [--auto-commit]
325
328
  wowbagger mint-id [--date YYYY-MM-DD] --json
326
329
  wowbagger mutation-finalize --ledger <dir> --recovery-token <token> --json
327
330
  ~~~
328
331
 
329
332
  `--auto-commit` is a bare opt-in flag. It is accepted once on `create`,
330
- `transition`, `patch`, and `publish-claimed`, and it is an unknown argument
331
- everywhere else. Repeating it is `invalid-request`. Section 13 defines what it
332
- does. Without it, every existing invocation keeps its exact stdout, exit,
333
- files, index, and Git `HEAD`.
333
+ `transition`, `parent-migrate`, `snooze`, `patch`, and `publish-claimed`, and it
334
+ is an unknown argument everywhere else. Repeating it is `invalid-request`.
335
+ Section 13 defines what it does. Without it, every existing invocation keeps
336
+ its exact stdout, exit, files, index, and Git `HEAD`.
334
337
 
335
338
  A dash for --input means standard input. File and standard-input requests have
336
339
  identical semantics. Request bytes must be valid UTF-8 JSON with one top-level
@@ -338,7 +341,8 @@ object and no duplicate member names at any depth. Duplicate members are
338
341
  invalid; a parser must not apply last-member-wins behaviour.
339
342
 
340
343
  Unknown, missing, and repeated command arguments are invalid-request. Create,
341
- transition, and patch use JSON input rather than parallel field flags.
344
+ transition, parent migration, snooze, and patch use JSON input rather than
345
+ parallel field flags.
342
346
 
343
347
  ### Standard output and standard error
344
348
 
@@ -372,6 +376,7 @@ answer in two domains.
372
376
  | work-claim | `work-claim` | 1, the legacy envelope marker | `result.operations.work_claim.api_version` of `claim capabilities --json` |
373
377
  | ledger-publication | `ledger-publication` | 1, the legacy envelope marker | the same work-claim `api_version` |
374
378
  | ledger-mutation | `ledger-mutation` | 1, the legacy envelope marker | the same work-claim `api_version` |
379
+ | ledger-repair | `ledger-repair` | 1 | `contract_version` of `number-repair-proposal` or `number-repair` |
375
380
  | bare result | absent, and no `ok` member either | none | none |
376
381
 
377
382
  The rule has three steps:
@@ -397,6 +402,7 @@ legacy claim-envelope marker, and a consumer must never compare it with the core
397
402
  | `inspect` | core | core |
398
403
  | `list` | core | core |
399
404
  | `mint-id` | core | core |
405
+ | `version-drift` | core | core |
400
406
  | `report` | core | core |
401
407
  | `create` | core | core, or ledger-mutation when the claim fence refuses |
402
408
  | `transition` | core | core, or ledger-mutation when the claim fence refuses |
@@ -411,13 +417,15 @@ legacy claim-envelope marker, and a consumer must never compare it with the core
411
417
  | `claim-sync` | work-claim | work-claim |
412
418
  | `claim-adopt` | work-claim | work-claim |
413
419
  | `mutation-finalize` | work-claim | work-claim |
414
- | `claim verify` | ledger-publication, `command: "read"` | ledger-publication |
420
+ | `number-repair-proposal` | ledger-repair | ledger-repair |
421
+ | `number-repair` | ledger-repair | ledger-repair |
415
422
  | `publish-claimed` | ledger-publication | ledger-publication |
416
423
 
417
424
  **Exact root members**
418
425
 
419
426
  A core response has exactly `ok`, `command`, `contract_version`, and one of
420
- `result` or `error`; `create`, `transition`, and `patch` add `state`. A
427
+ `result` or `error`; `create`, `transition`, `parent-migrate`, `snooze`, and
428
+ `patch` add `state`. A
421
429
  claim-domain response has exactly `ok`, `namespace`, `command`,
422
430
  `contract_version`, `state`, and one of `result` or `error`; a `claim
423
431
  capabilities` response omits `state`, and a claimed-publication response adds
@@ -425,8 +433,9 @@ capabilities` response omits `state`, and a claimed-publication response adds
425
433
  Successful mutation responses may carry `result.changed_paths`. It is the
426
434
  complete deterministic ledger-relative set whose bytes this invocation changed
427
435
  in the working tree. It does not mean Git committed those paths. `create`
428
- returns only its item path; `transition`, `patch`, and `publish-claimed` return
429
- their item path plus the tracked reconciliation log when that log changed.
436
+ returns only its item path; `transition`, `parent-migrate`, `snooze`, `patch`,
437
+ and `publish-claimed` return their item path plus the tracked reconciliation
438
+ log when that log changed.
430
439
  `--auto-commit` additionally returns `commit_paths` and `git_commit`; its
431
440
  `changed_paths` equals the committed set.
432
441
  `operation_id` once schema validation has accepted it. No expected envelope has
@@ -442,7 +451,8 @@ them would break every existing reader for no gain a consumer can use, because
442
451
  neither command mutates and neither participates in version negotiation. They
443
452
  stay bare, and step 3 of the rule is how a consumer recognizes them.
444
453
 
445
- A claim-fenced refusal to `create`, `transition`, or `patch` answers in the
454
+ A claim-fenced refusal to `create`, `transition`, `parent-migrate`, `snooze`,
455
+ or `patch` answers in the
446
456
  ledger-mutation domain with `command: "<command>-v1"` and
447
457
  `contract_version: 1`. This is not envelope drift: it is the work-claim
448
458
  contract answering, because a merge-coordinated backend refused the write
@@ -477,7 +487,7 @@ A successful read-only command has exactly:
477
487
  }
478
488
  ~~~
479
489
 
480
- A successful create, transition, or patch adds state:
490
+ A successful create, transition, parent migration, snooze, or patch adds state:
481
491
 
482
492
  ~~~json
483
493
  {
@@ -504,7 +514,8 @@ A read-only error has exactly:
504
514
  }
505
515
  ~~~
506
516
 
507
- Every create, transition, or patch error has a state member:
517
+ Every create, transition, parent-migrate, snooze, or patch error has a state
518
+ member:
508
519
 
509
520
  ~~~json
510
521
  {
@@ -546,7 +557,7 @@ presence is reported separately as bounded recovery_artifacts.
546
557
  | 6 | An unexpected operating or post-publication recovery condition. | operation-failed, post-commit-recovery-required, write-outcome-unknown, git-commit-failed, git-commit-outcome-unknown, post-commit-reconciliation-failed |
547
558
 
548
559
  Only exit 0 is normal completion. A client must inspect mutation state on every
549
- nonzero create, transition, or patch result.
560
+ nonzero create, transition, parent-migrate, snooze, or patch result.
550
561
 
551
562
  ## 3. Deterministic invalid-request issues
552
563
 
@@ -681,7 +692,7 @@ capability paths; all omitted paths retain their version 1 values:
681
692
  | `result.operations.list` | `{"supported":true,"write_scope":"none","cas_scope":"none","query_version":1}` |
682
693
  | `result.operations.patch` | `{"supported":true,"write_scope":"single-item","cas_scope":"exact-byte-sha256"}` |
683
694
  | `result.operations.report` | `{"supported":true,"write_scope":"derived-output","config_versions":[1,2],"named_views":true}` |
684
- | `result.operations.work_claim.api_version` | `2` |
695
+ | `result.operations.work_claim.api_version` | `3` |
685
696
  | `result.limits.max_item_source_bytes` | `8388608` |
686
697
  | `result.limits.default_list_page_size` | `50` |
687
698
  | `result.limits.max_list_page_size` | `200` |
@@ -759,7 +770,8 @@ defines the advertisement.
759
770
  `result.limits.max_item_source_bytes` is the first member of `result.limits`,
760
771
  before `multi_item_atomicity`. It is the exact number of bytes the complete
761
772
  serialized item source may occupy, and it applies to successor bytes accepted
762
- by `create`, `transition`, `patch`, and `publish-claimed`. It does not claim a
773
+ by `create`, `transition`, `parent-migrate`, `snooze`, `patch`, and
774
+ `publish-claimed`. It does not claim a
763
775
  raw request limit and it does not claim an `inspect` output limit; the
764
776
  serialized `publish-claimed` request keeps its own separate transport bound.
765
777
 
@@ -783,10 +795,18 @@ another. It is **not** a statement that worktrees write independently.
783
795
 
784
796
  On a provisioned Git-backed ledger they do not. One claim journal lives in the
785
797
  shared Git common directory, so it serializes every worktree of that
786
- repository: a recorded `transition` or `patch` in one worktree refuses every
787
- mutation in the others with exit 6 `claim-store-unavailable`, reason
798
+ repository. The serialization is scoped to the item: a recorded `transition` or
799
+ `patch` in one worktree refuses a mutation in the others that targets *that
800
+ item* with exit 6 `claim-store-unavailable`, reason
788
801
  `publication-reconciliation-required`, until the writing commit is visible in
789
- the blocked checkout. Clones do not share the common directory, so
802
+ the blocked checkout. A mutation targeting an unrelated item still runs, and
803
+ the sibling's finding remains visible. Own uncommitted work and out-of-protocol
804
+ revisions are global barriers and refuse every mutation. Bare `claim-verify`
805
+ reports exit 6 while any item carries a blocking finding.
806
+ `claim-verify --id <item>` keeps every finding visible but returns success when
807
+ only unrelated target-scoped findings remain; global findings still block.
808
+ See [the work-claim contract](work-claim-contract.md), section 3.2, for the
809
+ scope rules. Clones do not share the common directory, so
790
810
  `limits.cross_clone_coordination: false` carries no such consequence.
791
811
 
792
812
  That serialization is discoverable, per ledger, at
@@ -889,8 +909,8 @@ snapshot the successful item shape above defines, for the item the request
889
909
  selected. It is present when the request resolves an item and no validation
890
910
  error names that item's path. It is absent when nothing resolves, and absent
891
911
  when the resolved item is itself faulted, because that item's own frontmatter
892
- or placement is what validation rejects. A create, transition, or patch
893
- ledger-invalid refusal never carries it.
912
+ or placement is what validation rejects. A create, transition, parent-migrate,
913
+ snooze, or patch ledger-invalid refusal never carries it.
894
914
 
895
915
  This keeps the section 5 rule intact — inspect loads and validates the complete
896
916
  ledger, and every member of the attached snapshot comes from the one raw byte
@@ -1291,11 +1311,11 @@ A writer creates the lock file exclusively as valid UTF-8 JSON no larger than
1291
1311
  ~~~
1292
1312
 
1293
1313
  writer_id is an opaque ASCII string of 1 through 128 characters. operation is
1294
- create, transition, or patch — the three operations that take per-ID locks.
1295
- `publish-claimed` writes no lock file, so no lock file names it and a reader
1296
- never has to classify one. The remaining values must match their schema and
1297
- lock path. Metadata contains no credentials, user name, host name, or command
1298
- arguments.
1314
+ create, transition, parent-migrate, snooze, or patch — the operations that
1315
+ take per-ID locks. `publish-claimed` writes no lock file, so no lock file
1316
+ names it and a reader never has to classify one. The remaining values must
1317
+ match their schema and lock path. Metadata contains no credentials, user name,
1318
+ host name, or command arguments.
1299
1319
 
1300
1320
  A reader reads at most 4097 bytes. A lock larger than 4096 bytes, invalid UTF-8,
1301
1321
  duplicate-key JSON, invalid JSON, unknown members, or invalid field values is
@@ -1304,6 +1324,7 @@ one of too-large, invalid-utf8, duplicate-key, invalid-json, or invalid-shape.
1304
1324
  Valid metadata returns owner and owner_diagnostic null. Raw invalid bytes are
1305
1325
  never returned.
1306
1326
 
1327
+
1307
1328
  Locks are never removed automatically merely because started_at is old. Manual
1308
1329
  recovery follows ADR 0003.
1309
1330
 
@@ -1346,6 +1367,12 @@ Create accepts exactly:
1346
1367
  | item extension members | No | Permitted schema extensions. |
1347
1368
  | body | Yes | JSON string; empty and LF-leading strings are distinct and valid. |
1348
1369
 
1370
+ The member name `extensions` is reserved for the `patch` request container.
1371
+ Create refuses `item.extensions`; extension members must be named directly on
1372
+ the item, such as `item.tags` or `item.tier`. This prevents a nested mapping
1373
+ from becoming an extension value that `patch` and the declaration workflow
1374
+ cannot address.
1375
+
1349
1376
  If a file named by `--input` cannot be read before a request ID is known,
1350
1377
  create or transition returns `invalid-request` with one `invalid-value` issue at
1351
1378
  `/input`, the stable message `Request input could not be read.`, and mutation
@@ -1536,6 +1563,68 @@ no-clobber publication still protects an intervening creator.
1536
1563
  Successful create returns state committed and the inspect item shape from
1537
1564
  section 5.
1538
1565
 
1566
+ ### Journal-fenced allocation on a provisioned ledger
1567
+
1568
+ On a provisioned merge-coordinated ledger, a schema-version-2 create is a
1569
+ journaled legacy mutation. Under the shared namespace lock it reconciles,
1570
+ applies the allocation fence below, loads and validates the ledger, derives
1571
+ `1 + max(existing numbers)`, serializes the candidate, validates the complete
1572
+ candidate ledger, reserves journal capacity, and only then appends its
1573
+ `legacy-mutation-intent` with `command: "create-v1"` before any byte reaches
1574
+ the item path. Publication, exact-byte verification, and the terminal append
1575
+ all happen under that same lock. The intent carries
1576
+ `expected_revision: null`, which is valid only for `create-v1`. The committed
1577
+ terminal is `legacy-mutation` with `command: "create-v1"`; the create abort
1578
+ carries `command: "create-v1"` and `observed_revision: null`. No entry records
1579
+ the assigned number, because the candidate revision binds the complete item
1580
+ bytes and the ledger remains the one number authority.
1581
+
1582
+ The allocation fence reads reconciliation with one extra barrier. Every global
1583
+ finding blocks create as it blocks every write. On top of those, any
1584
+ coordinated item this checkout does not hold blocks `create`, because the
1585
+ journal records a committed terminal for an item whose number this working
1586
+ ledger cannot read. A stale revision of an item this checkout holds does not
1587
+ block `create`, because a number is immutable and the local maximum is already
1588
+ correct. A fenced create refuses with exit 6, `state: "unchanged"`,
1589
+ `error.code: "claim-store-unavailable"`, and
1590
+ `error.details.reason: "publication-reconciliation-required"` in the
1591
+ `ledger-mutation` domain as `create-v1`, having written no item byte. Resolve
1592
+ each finding by its own `remediation`, run `claim-verify` until it exits 0, and
1593
+ retry the same request ID; the retry takes the next number.
1594
+
1595
+ A create has no authorized predecessor, so Git `HEAD` is the only surface that
1596
+ can carry its authorized bytes, and an uncommitted create raises the global
1597
+ `git-finalization-required` barrier for every later mutation. Commit each
1598
+ created item before the next mutating command. This release adds no batch
1599
+ mutation; the supported bulk pattern remains the create-then-commit loop.
1600
+ A ledger that already carries duplicate numbers is item #182 recovery work and
1601
+ is repaired through the separate `ledger-repair` contract, not through core
1602
+ version 5 mutation. Run
1603
+ `number-repair-proposal --ledger <dir> --json`, review the complete mapping, then
1604
+ run `number-repair --ledger <dir> --input <repair.json> --json`. The repair
1605
+ command operates only when duplicate-number errors are the complete validation
1606
+ failure, preserves ULID identities and relation values, and publishes all
1607
+ affected items under the shared namespace fence. Arbitrary hand edits remain
1608
+ unsupported because they can damage IDs, paths, or references.
1609
+
1610
+ **What this guarantees, and what it does not.** The fence closes the reported
1611
+ PropertyCompass2 collision: cooperating alpha.14 worktrees of one clone that
1612
+ share one Git common directory can no longer commit two items carrying the same
1613
+ number, because every such create is visible through the shared journal before
1614
+ publication even without branch integration. Separate clones, separate
1615
+ machines, alpha.13 writers before the hard cutover, and noncooperating writes
1616
+ stay outside the fence and still rely on branch integration plus `validate`.
1617
+
1618
+ The widened journal grammar forces a hard cutover, so upgrade every writer in
1619
+ one Git coordination domain before the first alpha.14 create. An alpha.13
1620
+ binary cannot read a `create-v1` intent: it refuses with exit 6,
1621
+ `error.code` `claim-store-unavailable`, message
1622
+ `The durable claim store is unavailable.`, and `error.details.reason`
1623
+ `claim-store-unreadable`, leaving state unchanged and writing no item. That
1624
+ refusal means **this repository was written by a newer Wowbagger; upgrade this
1625
+ worktree to continue.** There is no automatic migration and no mixed-version
1626
+ grace period.
1627
+
1539
1628
  ## 8. Transition
1540
1629
 
1541
1630
  ### Request
@@ -1856,6 +1945,84 @@ temporary file followed by the platform's existing-file atomic replacement
1856
1945
  primitive. It then re-reads exact final bytes. This remains a local filesystem
1857
1946
  operation without universal crash durability or hostile-writer protection.
1858
1947
 
1948
+ ## 8.1 Parent migration
1949
+
1950
+ `parent-migrate` moves one item to an epic or detaches it without changing item
1951
+ identity or lifecycle. Parent-migrate accepts exactly:
1952
+
1953
+ ~~~json
1954
+ {
1955
+ "id": "wb_...",
1956
+ "expected_revision": "sha256:<64 lowercase hexadecimal characters>",
1957
+ "expected_parent": "wb_...",
1958
+ "parent": null,
1959
+ "date": "2030-01-11"
1960
+ }
1961
+ ~~~
1962
+
1963
+ `id`, `expected_revision`, `expected_parent`, `parent`, and `date` are required.
1964
+ `expected_parent` and `parent` are each a canonical item ID or `null`.
1965
+ `expected_parent` is a second compare-and-swap witness: it must equal the
1966
+ current parent after the locked re-read. A mismatch returns exit 4
1967
+ `parent-migration-precondition-failed` with a `parent-revision-conflict` issue
1968
+ and both `expected_parent` and `actual_parent`. An unknown parent, a non-epic
1969
+ parent, self-parenting, or a date before `created` or `updated` returns exit 2
1970
+ with the same error code and deterministic issues. Revision and lock conflicts
1971
+ retain their ordinary exit 4 precedence.
1972
+
1973
+ The command sets `updated` to request `date` and changes only `parent`. For an
1974
+ item whose status is `done`, `killed`, `archived`, or `deferred`, the request
1975
+ date must equal the existing `updated` date. No status or liveness precondition
1976
+ exists; the complete candidate ledger still validates every parent and rollup
1977
+ invariant before publication.
1978
+
1979
+ A core success answers as command `parent-migrate`, contract version 5, state
1980
+ `committed`, and returns the lossless item. On a provisioned ledger, a claim
1981
+ fence refusal answers in the `ledger-mutation` domain as command
1982
+ `parent-migrate-v1`. `--auto-commit` commits the changed item, when its bytes
1983
+ move, plus the reconciliation log under the fixed parent-migrate subject.
1984
+
1985
+ Parent migration reuses the legacy `patch-v1` fence family in the durable claim
1986
+ journal. That journal `command` is a fence-family and recovery classifier, not
1987
+ the public operation name. Internally, `responseCommand` identifies the
1988
+ response operation and keeps `parent-migrate-v1` distinct in envelopes. The
1989
+ journal's attempt ID and expected, candidate, and committed revisions remain
1990
+ the recovery evidence; Git history remains the reviewable operation audit.
1991
+
1992
+ ## 8.2 Snooze
1993
+
1994
+ `snooze` sets or clears one item's snooze date without changing lifecycle.
1995
+ Snooze accepts exactly:
1996
+
1997
+ ~~~json
1998
+ {
1999
+ "id": "wb_...",
2000
+ "expected_revision": "sha256:<64 lowercase hexadecimal characters>",
2001
+ "snoozed_until": "2030-02-01",
2002
+ "date": "2030-01-11"
2003
+ }
2004
+ ~~~
2005
+
2006
+ `id`, `expected_revision`, `snoozed_until`, and `date` are required.
2007
+ `snoozed_until` is an ISO calendar date or `null`; `null` removes the member.
2008
+ `date` is an ISO calendar date not before `created` or `updated`. The command
2009
+ sets `updated` to request `date` and changes only `snoozed_until`. For an item
2010
+ whose status is `done`, `killed`, `archived`, or `deferred`, the request date
2011
+ must equal the existing `updated` date.
2012
+
2013
+ Invalid input returns exit 2 `invalid-request`; stale item revision and lock
2014
+ conflicts retain exit 4; date and candidate validation use the same bounded
2015
+ issue and error shapes as patch. A core success answers as command `snooze`,
2016
+ contract version 5, state `committed`, and returns the lossless item. On a
2017
+ provisioned ledger, a claim fence refusal answers in the `ledger-mutation`
2018
+ domain as command `snooze-v1`. `--auto-commit` commits the changed item, when
2019
+ its bytes move, plus the reconciliation log under the fixed snooze subject.
2020
+
2021
+ Snooze also reuses the legacy `patch-v1` fence family in the durable claim
2022
+ journal. The journal `command` identifies the fence and recovery family, while
2023
+ `responseCommand` identifies the response operation as `snooze-v1`. This split
2024
+ is intentional and does not change the journal format.
2025
+
1859
2026
  ## 9. Patch
1860
2027
 
1861
2028
  Patch changes the mutable non-lifecycle content of one existing item — its
@@ -1905,6 +2072,14 @@ Patch accepts exactly:
1905
2072
  | date | Yes | ISO calendar date not earlier than existing created or updated. |
1906
2073
  | set | Yes | Mapping naming at least one patchable field. |
1907
2074
 
2075
+ For an item whose status is `done`, `killed`, `archived`, or `deferred`, the
2076
+ request date must equal the existing `updated` date for `patch`, `snooze`, and
2077
+ `parent-migrate`. The active lifecycle date must equal `updated`; these three
2078
+ verbs update `updated` without changing that lifecycle date. An earlier request
2079
+ fails the date floor, while a later request produces `candidate-invalid` with
2080
+ `terminal-date-must-match-updated`. Inspect immediately before the mutation and
2081
+ reuse the item's current `updated` date.
2082
+
1908
2083
  The patchable field set is exactly `title`, `priority`, `depends_on`,
1909
2084
  `related`, `body`, `body_append`, and `extensions`. A set member outside it is an invalid-request issue at
1910
2085
  its /set pointer — the boundary is stated here, not discovered from the
@@ -2077,6 +2252,27 @@ before it can correct anything. `validate` is therefore unchanged by this file,
2077
2252
  and an item whose extension member disagrees with the declaration is still a
2078
2253
  valid item — it is simply an item a patch can correct.
2079
2254
 
2255
+ **Provisioning a declaration on an existing ledger.**
2256
+
2257
+ `extensions-provision --ledger <dir> --input <request> --json [--dry-run]`
2258
+ accepts an explicit non-empty `members` mapping in the same version-1 member
2259
+ and type vocabulary. It never infers a type from stored values. Before it
2260
+ proposes anything, it requires the complete ledger to validate. For each
2261
+ selected member, it validates every stored occurrence against the selected
2262
+ type, requires at least one occurrence, and reports that occurrence count.
2263
+ The member need not appear on every item. Empty `string-list` values are valid;
2264
+ scalars, null, maps, nested lists, and lists containing non-strings conflict
2265
+ with `string-list`.
2266
+
2267
+ Dry-run returns the canonical declaration bytes and writes nothing. Publication
2268
+ creates exactly `.wowbagger/extensions.json` with no-clobber semantics.
2269
+ Repeating the identical declaration is idempotent; a different existing
2270
+ declaration is `extension-declaration-conflict`, exit 4, `unchanged`.
2271
+ Neither form changes an item byte or revision, records a claim-journal entry,
2272
+ or creates an item revision. The generated declaration is committed before
2273
+ the first patch that uses it. Anchors and aliases remain item-specific patch
2274
+ preconditions and do not prevent declaration provisioning.
2275
+
2080
2276
  Absence is fail-closed and total. A ledger with no `extensions.json` has **no**
2081
2277
  patchable extension member, and a `set.extensions` request against it is
2082
2278
  refused `patch-precondition-failed`, exit 2, `unchanged`, with one
@@ -2130,8 +2326,8 @@ that parses as something else is caught before publication, not after.
2130
2326
 
2131
2327
  ### Frontmatter ownership
2132
2328
 
2133
- Every frontmatter member belongs to exactly one of three classes, and this
2134
- table is the whole boundary. A consumer reads it instead of sending a patch and
2329
+ Every frontmatter member belongs to exactly one of four classes, and this table
2330
+ is the whole boundary. A consumer reads it instead of sending a patch and
2135
2331
  interpreting the refusal.
2136
2332
 
2137
2333
  | Member | Class | How it changes |
@@ -2141,7 +2337,7 @@ interpreting the refusal.
2141
2337
  | `number` | Core-owned | Create assigns it on schema version 2. It is the item handle and never moves. |
2142
2338
  | `status` | Core-owned | `transition` only, along an allowed lifecycle edge. |
2143
2339
  | `created` | Core-owned | Create derives it from the UTC date the ID encodes. |
2144
- | `updated` | Core-owned | Every `transition` and `patch` sets it to `request.date`. |
2340
+ | `updated` | Core-owned | Every `transition`, `parent-migrate`, `snooze`, and `patch` sets it to `request.date`. |
2145
2341
  | `completed` | Core-owned | `transition` writes it on completion and clears it on any other edge. |
2146
2342
  | `killed` | Core-owned | `transition` writes it on a kill and clears it on any other edge. |
2147
2343
  | `archived` | Core-owned | `transition` writes it on an archive and clears it on any other edge. |
@@ -2154,8 +2350,8 @@ interpreting the refusal.
2154
2350
  | `body` (the region after the frontmatter, not a member) | Consumer-editable through `patch` | `set.body` replaces the whole body; `""` empties it. `set.body_append` appends without a merge; the two are mutually exclusive in one request. |
2155
2351
  | `kind` | Create-once | Create fixes it. `patch` refuses it. |
2156
2352
  | `provenance` | Create-once | Create writes it. Every later verb preserves it byte for byte. |
2157
- | `parent` | Create-once | Create writes it. No verb moves an item between epics. |
2158
- | `snoozed_until` | Create-once | Create writes it. No verb changes it. |
2353
+ | `parent` | Dedicated mutation through `parent-migrate` | Create may set it; `parent-migrate` repoints the existing item to an epic or detaches it with compare-and-swap witnesses. |
2354
+ | `snoozed_until` | Dedicated mutation through `snooze` | Create may set it; `snooze` sets or clears it with a compare-and-swap witness. |
2159
2355
  | declared extension members (`tags`, `tier`, a consumer's own identifier fields) | Consumer-owned, patchable through `set.extensions` | `set.extensions.<member>` replaces the member whole; `null` removes it. Patchable only where `<ledger>/.wowbagger/extensions.json` declares the member and its value type, and only where the item does not write it with a YAML anchor or alias. |
2160
2356
  | undeclared extension members | Consumer-owned, not patchable | Supplied at create, preserved byte for byte by every verb, and otherwise a reviewable hand-edit. A ledger with no extension declaration has no patchable extension member at all. |
2161
2357
 
@@ -2304,9 +2500,9 @@ on code, mutation state, and documented details.
2304
2500
 
2305
2501
  ### The bounded item source
2306
2502
 
2307
- `create`, `transition`, and `patch` each measure the complete serialized
2308
- successor before validating it as a ledger candidate. A successor larger than
2309
- `result.limits.max_item_source_bytes` returns:
2503
+ `create`, `transition`, `parent-migrate`, `snooze`, and `patch` each measure
2504
+ the complete serialized successor before validating it as a ledger candidate.
2505
+ A successor larger than `result.limits.max_item_source_bytes` returns:
2310
2506
 
2311
2507
  - code `item-source-too-large`;
2312
2508
  - message `The proposed item source exceeds the supported byte limit.`;
@@ -2497,8 +2693,8 @@ claim-fenced mutation refusals and the bare `validate` and `ready` results.
2497
2693
 
2498
2694
  A ledger becomes provisioned when `provision` binds a namespace to the
2499
2695
  repository and `claim capabilities` reports `mode: "merge-coordinated"`. On
2500
- such a ledger, `create`, `transition`, and `patch` run inside the claim
2501
- coordinator described by the [work-claim
2696
+ such a ledger, `create`, `transition`, `parent-migrate`, `snooze`, and `patch`
2697
+ run inside the claim coordinator described by the [work-claim
2502
2698
  contract](work-claim-contract.md).
2503
2699
 
2504
2700
  ### The rule
@@ -2506,17 +2702,42 @@ contract](work-claim-contract.md).
2506
2702
  **Commit each mutation to Git before running the next mutating command.**
2507
2703
 
2508
2704
  The coordinator records every authorized mutation in the durable journal and
2509
- validates the recorded revisions against Git `HEAD`, not against working-tree
2510
- bytes. An uncommitted mutation is therefore an unreconciled mutation, and the
2511
- next `create`, `transition`, or `patch` refuses rather than writing on top of
2512
- work that is not yet durable.
2705
+ reconciles those records with Git `HEAD` and working-tree bytes. It refuses the
2706
+ next mutation when reconciliation finds an `unauthorized-revision`, requires
2707
+ Git finalization, or requires synchronization for the target item. A
2708
+ `worktree-synchronization-required` finding on an unrelated item remains
2709
+ visible without blocking that mutation.
2710
+
2711
+ An existing item's latest authorized working-tree bytes and an earlier
2712
+ authorized revision at `HEAD` form an authorized predecessor/successor window.
2713
+ That window produces no finding, so another mutation can run before the first
2714
+ one is committed. The later command's acceptance does not prove durability;
2715
+ the operating rule remains write, commit, `claim-verify`, next write.
2716
+
2717
+ `create` never occupies that window. A created item has no earlier authorized
2718
+ revision, so Git `HEAD` is the only surface that can carry its authorized
2719
+ bytes, and an uncommitted create raises the global `git-finalization-required`
2720
+ barrier for every later mutation, including the next create. Filing ten items
2721
+ is therefore ten serial `create --auto-commit` cycles, not one commit at the
2722
+ end. The accepted batch-create decision permanently rejects a batch mutation
2723
+ for the direct-Markdown architecture and keeps
2724
+ `limits.multi_item_atomicity: false`; see
2725
+ [`docs/design/2026-08-30-batch-create.md`](design/2026-08-30-batch-create.md).
2726
+
2727
+ The journal is bounded at 65,536 entries and 8,388,608 bytes. If capacity is
2728
+ known before a legacy create, transition, parent migration, snooze, or patch
2729
+ intent is published, the command returns exit 6 `claim-store-unavailable`,
2730
+ reason `journal-capacity-exceeded`, and `unchanged`. Existing journal bytes
2731
+ remain an exact prefix and no item byte is written. A genuinely unreadable
2732
+ journal retains `claim-store-unreadable`; an indeterminate outcome after an
2733
+ intent retains its outcome-unknown classification.
2513
2734
 
2514
2735
  The loop that works:
2515
2736
 
2516
2737
  ~~~sh
2517
2738
  wowbagger create --ledger <dir> --input request.json --json
2518
2739
  git add <dir> && git commit -m "Record the mutation"
2519
- wowbagger claim-verify --ledger <dir> --json
2740
+ wowbagger claim-verify --ledger <dir> --id <item> --json
2520
2741
  wowbagger transition --ledger <dir> --input next.json --json
2521
2742
  ~~~
2522
2743
 
@@ -2526,10 +2747,11 @@ refusals below name it by design.
2526
2747
 
2527
2748
  ### The refusal
2528
2749
 
2529
- An uncommitted prior mutation makes the next mutating command return exit 6.
2530
- The refusal answers in the ledger-mutation domain, not the core domain, because
2531
- the claim coordinator refused before the core mutation ran. Section 2 states
2532
- that rule; `namespace` is what a consumer reads to recognize it.
2750
+ A blocking reconciliation finding makes the next mutating command return exit
2751
+ 6. The refusal answers in the ledger-mutation domain, not the core domain,
2752
+ because the claim coordinator refused before the core mutation ran. Section 2
2753
+ states that rule; `namespace` is what a consumer reads to recognize it. The
2754
+ example below is an authorized new item that is still absent from `HEAD`.
2533
2755
 
2534
2756
  ~~~json
2535
2757
  {
@@ -2564,8 +2786,11 @@ that rule; `namespace` is what a consumer reads to recognize it.
2564
2786
  `details.findings`. Every finding that blocks a mutation carries a
2565
2787
  `remediation` string, and every such string names both the path to act on and
2566
2788
  `claim-verify`. `actual_revision: null` with `observed_surface: "git-head"`
2567
- means the authorized revision is not in `HEAD` at all, which is what an
2568
- uncommitted mutation looks like.
2789
+ means the authorized item is absent from `HEAD`, so Git finalization is
2790
+ required. Not every uncommitted mutation produces this refusal: an existing
2791
+ item whose working tree has the latest authorized bytes while `HEAD` has an
2792
+ earlier authorized revision is the nonblocking predecessor/successor window
2793
+ defined above.
2569
2794
 
2570
2795
  `spec/fixtures/mutation-refusals/uncommitted-prior-mutation/manifest.json` is
2571
2796
  the normative envelope for this refusal.
@@ -2578,10 +2803,19 @@ envelope, subject and commit set included.
2578
2803
  For every blocking finding:
2579
2804
 
2580
2805
  1. Do what `remediation` says, for each finding, using its `expected_path`.
2581
- `git-finalization-required` means commit that path.
2582
- 2. Run `wowbagger claim-verify --ledger <dir> --json`.
2583
- 3. Exit 0 with `state: "committed"` means the ledger is reconciled and the next
2584
- mutating command may run. Exit 6 means findings remain; repeat from step 1.
2806
+ `git-finalization-required` means commit that path. A
2807
+ `worktree-synchronization-required` finding means wait only when its
2808
+ `remediation` names an owner to wait for or says the expected revision is not
2809
+ yet reachable. When it says the revision is reachable in Git while no active
2810
+ named worktree owner is established, there is no commit left to wait for:
2811
+ inspect that reachable history, then restore or explicitly adopt reviewed
2812
+ bytes.
2813
+ 2. Run `wowbagger claim-verify --ledger <dir> --id <finding.item_id> --json`
2814
+ for each affected item. Use the bare command only for strict repository
2815
+ diagnosis.
2816
+ 3. Exit 0 with `state: "committed"` means that item is reconciled and its next
2817
+ mutating command may run. Exit 6 means blocking findings remain; repeat from
2818
+ step 1.
2585
2819
 
2586
2820
  The reasons a `stale-write-detected` finding can carry, and the other blocking
2587
2821
  finding codes, are enumerated in the [work-claim
@@ -2628,22 +2862,30 @@ by sending the flag, not by reading a version.
2628
2862
 
2629
2863
  Only the paths this invocation owns:
2630
2864
 
2631
- | Command | Commit set |
2632
- |---|---|
2633
- | `create` | the created item |
2865
+ | `create` | the created item and one `<ledger>/.wowbagger/reconcile-<namespace>.md` |
2634
2866
  | `transition` | the changed item and one `<ledger>/.wowbagger/reconcile-<namespace>.md` |
2635
- | `patch` | the changed item and the same one reconciliation log |
2636
- | `publish-claimed` | the published item and the same one reconciliation log |
2637
-
2638
- `create` is journal-silent by design, so its commit set has no log. For the
2639
- other three the log must already carry this invocation's terminal entry before
2640
- it may be staged; if it does not, the invocation reports `git-commit-failed`
2641
- with `reason: "log-unavailable"` rather than making an item-only commit.
2867
+ | `parent-migrate` | the changed item and one reconciliation log |
2868
+ | `snooze` | the changed item and one reconciliation log |
2869
+ | `patch` | the changed item and one reconciliation log |
2870
+ | `publish-claimed` | the published item and one reconciliation log |
2871
+
2872
+ The item path appears in the set only when the successor bytes differ from the
2873
+ item bytes at `HEAD`. A byte-identical `parent-migrate`, `snooze`, or `patch`
2874
+ still records its decision in the reconciliation log, so its auto-commit set
2875
+ contains the log alone.
2876
+
2877
+ `create` is journal-visible from the item's birth, so a successful `create`
2878
+ commits exactly the created item and its reconciliation log. For every command,
2879
+ the log must already carry this invocation's terminal entry before it may be
2880
+ staged; if it does not, the invocation reports `git-commit-failed` with
2881
+ `reason: "log-unavailable"`.
2642
2882
 
2643
2883
  Commit subjects are fixed:
2644
2884
 
2645
2885
  wowbagger: create item #N
2646
2886
  wowbagger: transition item #N
2887
+ wowbagger: parent-migrate item #N
2888
+ wowbagger: snooze item #N
2647
2889
  wowbagger: patch item #N
2648
2890
  wowbagger: publish claimed item #N
2649
2891
 
@@ -2664,12 +2906,16 @@ Before the mutation runs, the invocation takes a per-working-tree mutex and
2664
2906
  requires all of:
2665
2907
 
2666
2908
  - No path staged anywhere in the repository.
2667
- - No dirty path under the ledger: tracked, untracked, or partially staged.
2909
+ - No dirty path under the ledger, with one exception: `transition`,
2910
+ `parent-migrate`, `snooze`, `patch`, and `publish-claimed` tolerate a dirty
2911
+ current-namespace reconciliation log, because each rebuilds that one derived
2912
+ file from the authoritative journal. `create` does not tolerate it.
2668
2913
  - A Git identity Git can resolve without committing.
2669
2914
  - A `HEAD` that exists. A detached `HEAD` is supported, because a commit works
2670
2915
  from one; an unborn `HEAD` refuses.
2671
- - A clean internal `claim-verify`, which is also what makes an unreconciled
2672
- prior mutation refuse before `publish-claimed`.
2916
+ - An internal `claim-verify` with no findings blocking the target item, which
2917
+ is also what makes an unreconciled prior mutation refuse before
2918
+ `publish-claimed`.
2673
2919
 
2674
2920
  Unstaged and untracked paths **outside** the ledger are allowed and stay
2675
2921
  byte-identical; they are never staged. Any other failure returns exit 4
@@ -2678,11 +2924,27 @@ staging, or commit occurs. `details.reason` is one of `staged-paths-present`,
2678
2924
  `ledger-not-clean`, `identity-unavailable`, `unborn-head`, `mutex-held`,
2679
2925
  `claim-state-unreconciled`, or `git-unavailable`.
2680
2926
 
2681
- The rule is deliberately strict rather than preserving foreign staged work in a
2682
- temporary index. Reconciliation excludes `.wowbagger/` from the Git item
2683
- surface, and a dirty reconciliation log does not itself refuse a mutation, so a
2684
- broad add would silently commit foreign ledger work. Refusing is the cheaper
2685
- correct answer.
2927
+ Every `auto-commit-preflight-failed` refusal also carries
2928
+ `details.retryable`. `mutex-held` is retryable inside one working tree.
2929
+ `claim-state-unreconciled` also carries optional `claim_verify_code`,
2930
+ `claim_verify_reason`, `identity_diagnostic`, and bounded `findings`;
2931
+ `claim-store-locked` is retryable, while persistent reconciliation is not
2932
+ retryable. An `identity_diagnostic` reports an ambiguous or unreadable
2933
+ worktree identity — `duplicate-worktree-identity` with `worktree_id` and
2934
+ `live_worktree_count`, or `worktree-enumeration-failed` with no further member
2935
+ — and is never retryable. Every other
2936
+ preflight reason is not retryable. Clients branch on this boolean and the
2937
+ underlying verification reason, never on the generic message.
2938
+
2939
+ The rule is deliberately strict rather than preserving foreign staged work in
2940
+ a temporary index. A journal-owning auto-commit validates and rebuilds the
2941
+ dirty derived reconciliation log from the authoritative journal, then commits
2942
+ it with the mutation. Owning the log is not absorbing what was already in it:
2943
+ `create` still refuses a dirty reconciliation log, because residue that
2944
+ predates the invocation is foreign to the entries create is about to write.
2945
+ Every other dirty ledger path refuses. Claim refusal evidence is never
2946
+ suppressed: both the authoritative journal and its projected log retain the
2947
+ decision before a later command rebuilds or commits the log.
2686
2948
 
2687
2949
  A flagged invocation on an advisory or non-provisioned ledger returns exit 5
2688
2950
  `capability-unavailable` with `state: "unchanged"` before the mutation. It does
@@ -2704,12 +2966,18 @@ not fall back to manual mode.
2704
2966
  dirt: refusing on it would make an already-published item impossible to
2705
2967
  commit.
2706
2968
 
2969
+ When a successful mutation produces item bytes identical to `HEAD`, the item
2970
+ path is omitted from `commit_paths`; the journal log remains the only changed
2971
+ path. This avoids reporting a staged-path mismatch for a valid audit-only
2972
+ mutation and keeps `mutation-finalize` idempotent after an advanced `HEAD`.
2973
+
2707
2974
  On success the response keeps its original domain and adds three members to
2708
2975
  `result`: `git_commit`, `commit_paths`, and `claim_verified: true`. A successful
2709
2976
  invocation does not return until an internal `claim-verify` exits 0 with no
2710
- findings and a valid ledger. For `publish-claimed` the same `claim-verify` must
2711
- also report `git_finalized: true` and the new commit in the publication's
2712
- finalization row.
2977
+ findings blocking the target item and a valid ledger. Nonblocking findings
2978
+ remain available to `claim-verify`; the success envelope does not duplicate
2979
+ them. For `publish-claimed`, the same verification must also report
2980
+ `git_finalized: true` and the new commit in the publication's finalization row.
2713
2981
 
2714
2982
  ### The commit-failed contract
2715
2983
 
@@ -2718,8 +2986,9 @@ Exit 6, `state: "committed"`, code `git-commit-failed`, message
2718
2986
  still describes item publication as section 2 defines it. It does not claim Git
2719
2987
  finalization.
2720
2988
 
2721
- `create`, `transition`, and `patch` keep the core domain: no `namespace`, the
2722
- original command name, and the core `contract_version`. `ledger-mutation` is
2989
+ `create`, `transition`, `parent-migrate`, `snooze`, and `patch` keep the core
2990
+ domain: no `namespace`, the original command name, and the core
2991
+ `contract_version`. `ledger-mutation` is
2723
2992
  not used, because that domain means the fence refused before the core mutation
2724
2993
  ran. `publish-claimed` keeps `namespace: "ledger-publication"`,
2725
2994
  `command: "publish-claimed"`, its legacy envelope marker, and its top-level
@@ -2743,7 +3012,10 @@ it match.
2743
3012
 
2744
3013
  A commit that is established while reconciliation then refuses is exit 6
2745
3014
  `post-commit-reconciliation-failed` with `state: "committed"`. It carries
2746
- `git_commit`, `commit_paths`, and the `findings`. The commit stands.
3015
+ `git_commit`, `commit_paths`, and optional `claim_verify_code`,
3016
+ `claim_verify_reason`, and bounded `findings` from the failed verification. The
3017
+ commit stands; callers inspect the preserved cause and never replay the
3018
+ mutation.
2747
3019
 
2748
3020
  No failure envelope contains raw hook output, signing output, an absolute path,
2749
3021
  an environment value, or platform exception text. A bounded human diagnostic may