wowbagger 0.1.0-alpha.7 → 0.1.0-alpha.8

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 (51) hide show
  1. package/CHANGELOG.md +193 -1
  2. package/README.md +194 -16
  3. package/adapters/claude-code/wowbagger-adapter.json +1 -1
  4. package/adapters/codex/wowbagger-adapter.json +1 -1
  5. package/adapters/opencode/wowbagger-adapter.json +1 -1
  6. package/docs/adapter-contract.md +66 -6
  7. package/docs/host-contract.md +287 -0
  8. package/docs/mutation-contract.md +558 -17
  9. package/docs/work-claim-contract.md +56 -24
  10. package/package.json +14 -1
  11. package/schemas/bare-ready-result.json +27 -0
  12. package/schemas/bare-validation-result.json +23 -0
  13. package/schemas/common.json +470 -0
  14. package/schemas/core-capabilities-response.json +346 -0
  15. package/schemas/core-envelope.json +141 -0
  16. package/schemas/core-inspect-response.json +37 -0
  17. package/schemas/core-inspect-workbench-response.json +340 -0
  18. package/schemas/core-list-error-response.json +242 -0
  19. package/schemas/core-list-query.json +98 -0
  20. package/schemas/core-list-response.json +140 -0
  21. package/schemas/core-report-response.json +379 -0
  22. package/schemas/core-transition-error-response.json +852 -0
  23. package/schemas/core-transition-request.json +47 -0
  24. package/schemas/core-transition-response.json +49 -0
  25. package/schemas/index.json +108 -0
  26. package/schemas/ledger-mutation-refusal.json +288 -0
  27. package/schemas/report-config-v1.json +140 -0
  28. package/schemas/report-config-v2.json +242 -0
  29. package/skills/wowbagger/SKILL.md +151 -38
  30. package/src/adapter/core-probe.js +87 -7
  31. package/src/claim-capabilities.js +4 -5
  32. package/src/claim-coordinator.js +15 -2
  33. package/src/claim-prospective.js +151 -0
  34. package/src/claim-publication.js +49 -9
  35. package/src/claim-sync.js +66 -0
  36. package/src/cli.js +671 -49
  37. package/src/extension-provision.js +33 -0
  38. package/src/git-autocommit.js +36 -6
  39. package/src/git-reconciliation.js +53 -9
  40. package/src/launch.js +52 -0
  41. package/src/lifecycle.js +153 -0
  42. package/src/limits.js +39 -0
  43. package/src/list.js +365 -0
  44. package/src/mutation.js +145 -118
  45. package/src/projection.js +39 -0
  46. package/src/report-graph.js +122 -21
  47. package/src/report-html.js +219 -51
  48. package/src/report-view.js +189 -0
  49. package/src/report.js +145 -15
  50. package/src/validate.js +4 -1
  51. package/src/workbench.js +117 -0
@@ -1,7 +1,7 @@
1
1
  # Local mutation contract
2
2
 
3
- Status: versions 1, 2, and 3 are defined; the pre-alpha standalone
4
- local-filesystem runtime currently emits version 3.
3
+ Status: versions 1 through 5 are defined; the pre-alpha standalone
4
+ local-filesystem runtime currently emits version 5.
5
5
 
6
6
  This document defines the machine contract implemented by the local-filesystem
7
7
  mutation backend. It supplements [SPEC.md](../SPEC.md) and
@@ -10,7 +10,10 @@ mutation backend. It supplements [SPEC.md](../SPEC.md) and
10
10
 
11
11
  The executable supports `validate`, `ready`, and the commands below. Clients
12
12
  must still call `capabilities` and honor its advertised limits before assuming a
13
- backend can provide a particular write guarantee.
13
+ backend can provide a particular write guarantee. A non-agent consumer that
14
+ launches this executable directly reads
15
+ [docs/host-contract.md](host-contract.md) for the package seam, the process
16
+ tuple, and the packaged JSON Schemas.
14
17
 
15
18
  ## Contract versions
16
19
 
@@ -159,10 +162,9 @@ and stops. That is the intended outcome, not a regression.
159
162
 
160
163
  ### Version 4
161
164
 
162
- Version 4 is the version this document defines and the runtime emits. It
163
- retains every version 3 request, response, state, exit, locking, CAS,
164
- publication, and recovery rule except for these explicit deltas, which are the
165
- complete difference against published version 3 (`0.1.0-alpha.6`):
165
+ Version 4 retained every version 3 request, response, state, exit, locking,
166
+ CAS, publication, and recovery rule except for these explicit deltas, which are
167
+ the complete difference against published version 3 (`0.1.0-alpha.6`):
166
168
 
167
169
  - every core command envelope carries `contract_version: 4`;
168
170
  - **the bounded item source.** `MAX_ITEM_SOURCE_BYTES` is 8,388,608 and bounds
@@ -211,6 +213,58 @@ members exactly must negotiate version 3 before reading a date refusal, and a
211
213
  consumer that must not have its accepted input narrowed underneath it must
212
214
  negotiate version 4 before writing an item.
213
215
 
216
+ ### Version 5
217
+
218
+ Version 5 is the version this document defines and the runtime emits. It
219
+ retains every version 4 request, response, state, exit, locking, CAS,
220
+ publication, and recovery rule except for these explicit deltas, which are the
221
+ complete difference against published version 4 (`0.1.0-alpha.7`):
222
+
223
+ - every core command envelope carries `contract_version: 5`;
224
+ - **the bounded list operation.** `list` is a new read-only core command
225
+ (section 5.1). It enumerates a validated ledger as bounded item summaries
226
+ under closed filters, one selected sort field, and cursor pagination. It adds
227
+ the refusals `list-snapshot-changed` (exit 4) and `list-response-too-large`
228
+ (exit 2);
229
+ - **the advertised list bounds.** `result.operations.list` and four new
230
+ `result.limits` members carry the list contract's exact numbers (section 4).
231
+ A version 4 consumer that validated `result.operations` or `result.limits` by
232
+ exact members refuses the new members, which is the same fail-closed outcome
233
+ the version field already produces;
234
+ - **the bounded workbench projection.** `inspect` accepts the opt-in flag pair
235
+ `--workbench --as-of YYYY-MM-DD` and answers with a bounded per-item lifecycle
236
+ affordance projection under `result.workbench` (section 5.2). It adds the
237
+ refusal `workbench-response-too-large` (exit 2) and the advertisement
238
+ `result.operations.inspect.workbench` with three new `result.limits` members
239
+ (section 4). An `inspect` invocation without `--workbench` keeps its exact
240
+ result members and its exact bytes; and
241
+ - **named custom report views.** `report` accepts an optional `--view <name>`
242
+ against report configuration `report_version: 2`, adds `result.view` to a
243
+ named success, adds the refusal `report-view-not-found` (exit 2), and is
244
+ advertised at `result.operations.report` (section 4). An invocation without
245
+ `--view` keeps its exact result members and its exact bytes under either
246
+ configuration version.
247
+
248
+ Nothing else moves. `validate` and `ready` carry no version member and their
249
+ bytes are unchanged. `create`, `transition`, `patch`, `mint-id`, and
250
+ `capabilities` change only in the `contract_version` number, and an `inspect`
251
+ invocation without `--workbench` changes only in that number too: no root
252
+ member, result member, issue member, error code, or exit meaning of an existing
253
+ invocation changes.
254
+
255
+ A version 4 consumer fails closed against a version 5 core the same way: it
256
+ reads `contract_version: 5` from `capabilities --json`, does not recognize it,
257
+ and stops.
258
+
259
+ The bootstrap wire, adapter approval, instruction, handoff, and fixture-format
260
+ 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
+ query and the workbench projection each have their own version domain,
263
+ `query_version` and `projection_version`, both `1`: a consumer negotiates them
264
+ through `result.operations.list.query_version` and
265
+ `result.operations.inspect.workbench.projection_version`, not through the core
266
+ contract version.
267
+
214
268
  ## 1. Scope
215
269
 
216
270
  The contract keeps four concerns separate:
@@ -263,6 +317,8 @@ The local commands are:
263
317
  ~~~text
264
318
  wowbagger capabilities --json
265
319
  wowbagger inspect --ledger <dir> --id <id> --json
320
+ wowbagger inspect --ledger <dir> --id <id> --workbench --as-of YYYY-MM-DD --json
321
+ wowbagger list --ledger <dir> --input <json-file|-> --json
266
322
  wowbagger create --ledger <dir> --input <json-file|-> --json [--auto-commit]
267
323
  wowbagger transition --ledger <dir> --input <json-file|-> --json [--auto-commit]
268
324
  wowbagger patch --ledger <dir> --input <json-file|-> --json [--auto-commit]
@@ -312,7 +368,7 @@ answer in two domains.
312
368
 
313
369
  | Domain | Root `namespace` | `contract_version` | Version to negotiate |
314
370
  |---|---|---|---|
315
- | core | absent | 4 | top-level `contract_version` of `capabilities --json` |
371
+ | core | absent | 5 | top-level `contract_version` of `capabilities --json` |
316
372
  | work-claim | `work-claim` | 1, the legacy envelope marker | `result.operations.work_claim.api_version` of `claim capabilities --json` |
317
373
  | ledger-publication | `ledger-publication` | 1, the legacy envelope marker | the same work-claim `api_version` |
318
374
  | ledger-mutation | `ledger-mutation` | 1, the legacy envelope marker | the same work-claim `api_version` |
@@ -339,14 +395,20 @@ legacy claim-envelope marker, and a consumer must never compare it with the core
339
395
  | `ready` | bare result `{as_of, valid, ready}` | bare result `{valid, errors}` on an invalid ledger, exit 1 |
340
396
  | `capabilities` | core | core |
341
397
  | `inspect` | core | core |
398
+ | `list` | core | core |
342
399
  | `mint-id` | core | core |
343
400
  | `report` | core | core |
344
401
  | `create` | core | core, or ledger-mutation when the claim fence refuses |
345
402
  | `transition` | core | core, or ledger-mutation when the claim fence refuses |
403
+ | `parent-migrate` | core | core, or ledger-mutation when the claim fence refuses |
404
+ | `snooze` | core | core, or ledger-mutation when the claim fence refuses |
346
405
  | `patch` | core | core, or ledger-mutation when the claim fence refuses |
406
+ | `extensions-provision` | core | core |
347
407
  | `provision` | work-claim | work-claim |
348
408
  | `claim capabilities/read/acquire/renew/release` | work-claim | work-claim |
349
409
  | `claim-verify` | work-claim | work-claim |
410
+ | `claim-merge-verify` | work-claim | work-claim |
411
+ | `claim-sync` | work-claim | work-claim |
350
412
  | `claim-adopt` | work-claim | work-claim |
351
413
  | `mutation-finalize` | work-claim | work-claim |
352
414
  | `claim verify` | ledger-publication, `command: "read"` | ledger-publication |
@@ -359,6 +421,14 @@ A core response has exactly `ok`, `command`, `contract_version`, and one of
359
421
  claim-domain response has exactly `ok`, `namespace`, `command`,
360
422
  `contract_version`, `state`, and one of `result` or `error`; a `claim
361
423
  capabilities` response omits `state`, and a claimed-publication response adds
424
+
425
+ Successful mutation responses may carry `result.changed_paths`. It is the
426
+ complete deterministic ledger-relative set whose bytes this invocation changed
427
+ 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.
430
+ `--auto-commit` additionally returns `commit_paths` and `git_commit`; its
431
+ `changed_paths` equals the committed set.
362
432
  `operation_id` once schema validation has accepted it. No expected envelope has
363
433
  undocumented root members.
364
434
 
@@ -468,9 +538,10 @@ presence is reported separately as bounded recovery_artifacts.
468
538
  | Exit | Condition | Error codes |
469
539
  |---:|---|---|
470
540
  | 0 | Successful command; a mutation is state committed. | none |
471
- | 2 | Argument, request, lookup, or candidate/lifecycle/layout-precondition failure. | invalid-request, item-not-found, transition-precondition-failed, patch-precondition-failed, candidate-invalid, item-source-too-large, items-directory-unavailable |
472
- | 3 | The complete configured ledger is invalid. | ledger-invalid |
473
- | 4 | Cooperative comparison, lock, identity, or default-path conflict. | revision-conflict, lock-held, id-collision, path-collision, auto-commit-preflight-failed, mutation-finalize-refused |
541
+ | 1 | A bare-result command found the ledger invalid, or `report` failed before or during publication. `report` answers an invalid ledger here rather than at exit 3. | bare result `{valid, errors}`, ledger-invalid (`report` only), report-read-failed, report-write-failed |
542
+ | 2 | Argument, request, lookup, or candidate/lifecycle/layout-precondition failure. | invalid-request, item-not-found, transition-precondition-failed, patch-precondition-failed, candidate-invalid, item-source-too-large, items-directory-unavailable, list-response-too-large, report-config-invalid, report-view-not-found |
543
+ | 3 | The complete configured ledger is invalid, for every command except `report`. | ledger-invalid |
544
+ | 4 | Cooperative comparison, lock, identity, or default-path conflict. | revision-conflict, lock-held, id-collision, path-collision, auto-commit-preflight-failed, mutation-finalize-refused, list-snapshot-changed |
474
545
  | 5 | The backend lacks the required capability or write scope. | atomic-scope-required, capability-unavailable |
475
546
  | 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 |
476
547
 
@@ -596,21 +667,95 @@ members above. This is the one input to `capabilities`, so the response is
596
667
  deterministic for a given working directory but not fixed across working
597
668
  directories.
598
669
 
599
- ### Contract version 4 capability delta
670
+ ### Contract version 5 capability delta
600
671
 
601
672
  The preceding JSON and three-member Git-dependent coupling remain the exact
602
- version 1 definition. Versions 2, 3, and 4 change only the following capability
603
- paths; all omitted paths retain their version 1 values:
673
+ version 1 definition. Versions 2, 3, 4, and 5 change only the following
674
+ capability paths; all omitted paths retain their version 1 values:
604
675
 
605
- | Path | Version 4 value |
676
+ | Path | Version 5 value |
606
677
  |---|---|
607
- | `contract_version` | `4` |
678
+ | `contract_version` | `5` |
608
679
  | `result.backend.coordination_scope` | `"same-working-copy-cooperative-writers"` |
680
+ | `result.operations.inspect.workbench` | `{"supported":true,"projection_version":1}` |
681
+ | `result.operations.list` | `{"supported":true,"write_scope":"none","cas_scope":"none","query_version":1}` |
609
682
  | `result.operations.patch` | `{"supported":true,"write_scope":"single-item","cas_scope":"exact-byte-sha256"}` |
683
+ | `result.operations.report` | `{"supported":true,"write_scope":"derived-output","config_versions":[1,2],"named_views":true}` |
610
684
  | `result.operations.work_claim.api_version` | `2` |
611
685
  | `result.limits.max_item_source_bytes` | `8388608` |
686
+ | `result.limits.default_list_page_size` | `50` |
687
+ | `result.limits.max_list_page_size` | `200` |
688
+ | `result.limits.max_list_title_characters` | `120` |
689
+ | `result.limits.max_list_response_bytes` | `131072` |
690
+ | `result.limits.max_workbench_title_characters` | `120` |
691
+ | `result.limits.max_workbench_collection_entries` | `50` |
692
+ | `result.limits.max_workbench_response_bytes` | `65536` |
612
693
  | `result.limits.cross_worktree_coordination` | `false` |
613
694
 
695
+ `result.operations` advertises in the fixed order `inspect`, `list`, `create`,
696
+ `transition`, `patch`, `report`, `work_claim`. `result.limits` advertises
697
+ `max_item_source_bytes` first, then the four list bounds in the order above,
698
+ then the three workbench bounds, then the version 1 booleans. `list` is
699
+ read-only, so its `write_scope` and `cas_scope` are `none`, exactly like
700
+ `inspect`. `inspect` keeps its version 1 members and gains the nested
701
+ `workbench` member, which is how a consumer learns the opt-in affordance
702
+ projection of section 5.2 exists and which projection shape it will receive.
703
+
704
+ The four list bounds are exact numbers, not advice:
705
+
706
+ - `default_list_page_size` is the page size a query that omits `page_size` receives;
707
+ - `max_list_page_size` is the largest `page_size` a query may request;
708
+ - `max_list_title_characters` is the exact number of Unicode code points a row's
709
+ projected title may carry before it is truncated and flagged; and
710
+ - `max_list_response_bytes` bounds the complete `list` response, envelope and
711
+ trailing LF included.
712
+
713
+ A full page of maximum-width rows can exceed `max_list_response_bytes`. When it
714
+ does, the command refuses the page whole with `list-response-too-large` and the
715
+ caller lowers `page_size`; neither bound silently rewrites the other, and a
716
+ `list` response is never a partial page. A consumer with its own transport
717
+ budget uses the lower of that budget and `max_list_response_bytes`.
718
+
719
+ The three workbench bounds are exact numbers on the same terms:
720
+
721
+ - `max_workbench_title_characters` is the exact number of Unicode code points
722
+ the projected item title may carry before it is truncated and flagged;
723
+ - `max_workbench_collection_entries` is the largest number of entries any one
724
+ variable-size workbench collection carries — the relation lists, an option's
725
+ precondition issues, an option's blockers, and the related IDs inside one
726
+ issue. A longer collection carries its first entries, its observed `total`,
727
+ and `truncated: true`; and
728
+ - `max_workbench_response_bytes` bounds the complete workbench response,
729
+ envelope and trailing LF included.
730
+
731
+ The entry bound keeps every projection this core can build well inside the
732
+ response bound, so `max_workbench_response_bytes` is a promise rather than a
733
+ knob: a workbench request carries no size to lower. A projection that would
734
+ exceed it is refused whole with `workbench-response-too-large` at exit 2 rather
735
+ than written short.
736
+
737
+ `result.operations.report` is how a consumer learns what the read-only report
738
+ command accepts, so it never has to probe by generating an artifact.
739
+ `write_scope: "derived-output"` states that the command writes one configured
740
+ output path and no ledger item. `config_versions` lists the accepted
741
+ `<ledger>/.wowbagger/report.json` versions: version 1 is unchanged, and
742
+ `report_version: 2` adds a `views` object whose named members — for example
743
+ `security-blockers` — each project the validated ledger through grouped filters
744
+ with **OR within one filter group; AND across groups**. `named_views: true` says
745
+ the core accepts a selection by name:
746
+
747
+ ~~~text
748
+ wowbagger report --ledger <dir> --view <name> --as-of YYYY-MM-DD --json
749
+ ~~~
750
+
751
+ A named success adds `result.view` and nothing else; a base report's result is
752
+ unchanged. A selection that names no configured view, or any `--view` against a
753
+ version 1 configuration, is refused with `report-view-not-found` at exit 2,
754
+ `details.view` naming the requested name, and every existing output left as it
755
+ was. The report request, the configuration, and the generated artifact are
756
+ defined in [the README](../README.md#named-custom-report-views); this contract
757
+ defines the advertisement.
758
+
614
759
  `result.limits.max_item_source_bytes` is the first member of `result.limits`,
615
760
  before `multi_item_atomicity`. It is the exact number of bytes the complete
616
761
  serialized item source may occupy, and it applies to successor bytes accepted
@@ -706,7 +851,7 @@ core contains only fields defined by supported item schema versions:
706
851
  - schema_version, id, title, kind, status, created, updated;
707
852
  - provenance.source and provenance.recorded_at;
708
853
  - depends_on and related;
709
- - optional parent, snoozed_until, completed, killed, archived;
854
+ - optional parent, snoozed_until, completed, killed, archived, deferred;
710
855
  - optional number, the core-assigned item identity on schema version 2, and
711
856
  optional priority, the caller-supplied schema priority; and
712
857
  - decisions with only their defined action, date, summary, rationale, and
@@ -755,6 +900,332 @@ An operator repairing an invalid ledger reads the bytes and revision of the
755
900
  items around the fault with `inspect`, and reads the fault itself from
756
901
  `validation_errors`, whose `expected_path` and `remediation` name the repair.
757
902
 
903
+ ### 5.1 The bounded list query
904
+
905
+ `list` enumerates a validated ledger. It is read-only, takes no lock, and
906
+ returns no item source and no body: a caller selects a row, then reads that one
907
+ item with `inspect`. `--ledger` is required; `list` performs no ledger discovery
908
+ and binds no implicit path.
909
+
910
+ `list` loads and validates the complete ledger before it projects anything. An
911
+ invalid ledger returns `ledger-invalid`, exit 3, with `details.validation_errors`
912
+ and no rows: there is no partial list.
913
+
914
+ #### The query
915
+
916
+ The request is UTF-8 JSON with one top-level object and exactly these members:
917
+
918
+ | Member | Required | Value |
919
+ |---|---|---|
920
+ | `query_version` | yes | `1` |
921
+ | `as_of` | yes | ISO calendar date; readiness is projected as of this date |
922
+ | `sort` | yes | object with exactly `field` and `direction` |
923
+ | `filters` | no | object with a subset of the closed filter members below |
924
+ | `page_size` | no | integer from 1 to `max_list_page_size`; default `default_list_page_size` |
925
+ | `cursor` | no | an opaque cursor from a previous `list` response |
926
+
927
+ `sort.field` is one of `created`, `id`, `number`, `priority`, `status`, `title`,
928
+ `updated`. `sort.direction` is `ascending` or `descending`.
929
+
930
+ `filters` members are:
931
+
932
+ | Filter | Value | Meaning |
933
+ |---|---|---|
934
+ | `status` | non-empty array of distinct statuses | row status is in the set |
935
+ | `kind` | non-empty array of distinct kinds | row kind is in the set |
936
+ | `ready` | boolean | row readiness as of `as_of` equals the value |
937
+ | `number` | non-empty array of distinct positive integers | row number is in the set |
938
+ | `title_contains` | non-empty string | the stored title contains it |
939
+
940
+ Filters are a conjunction: every named filter must accept the item. A value
941
+ filter is a set, so an empty array or a repeated entry is `invalid-request`
942
+ rather than a silently widened or deduplicated query. `title_contains` is a
943
+ case-sensitive substring test against the whole stored title, not against the
944
+ bounded excerpt a row carries; there is no locale collation, case folding, or
945
+ Unicode normalization.
946
+
947
+ Unknown, missing, and mistyped members are `invalid-request`, exit 2, with the
948
+ aggregated issues of section 3. A JSON number is an integer only when its
949
+ literal is canonical, so `page_size: 1.0` is `invalid-value`.
950
+
951
+ #### Order
952
+
953
+ The selected field decides the primary order. An item that does not carry the
954
+ selected member sorts after every item that does in `ascending` order;
955
+ `descending` is the exact reverse of the ascending primary comparison. Every
956
+ order then breaks ties on ascending immutable ID, in both directions, so the
957
+ total order is stable across invocations and a full traversal never depends on
958
+ file order.
959
+
960
+ #### The response
961
+
962
+ ~~~json
963
+ {
964
+ "ok": true,
965
+ "command": "list",
966
+ "contract_version": 5,
967
+ "result": {
968
+ "query_version": 1,
969
+ "as_of": "2026-08-21",
970
+ "snapshot": { "revision": "sha256:…", "item_count": 5 },
971
+ "page": {
972
+ "size": 2,
973
+ "offset": 0,
974
+ "returned": 2,
975
+ "matched": 5,
976
+ "has_more": true,
977
+ "next_cursor": "…"
978
+ },
979
+ "items": []
980
+ }
981
+ }
982
+ ~~~
983
+
984
+ `snapshot.revision` is the ledger-snapshot witness: a digest over every item's
985
+ ledger-relative path and exact item revision, so any item added, removed,
986
+ renamed, or byte-modified changes it. `snapshot.item_count` counts the whole
987
+ validated ledger, not the filtered page.
988
+
989
+ `page.matched` counts every row the filters accept in that snapshot.
990
+ `page.next_cursor` is a string when `has_more` is `true` and `null` otherwise.
991
+
992
+ Each row has exactly:
993
+
994
+ | Member | Presence | Value |
995
+ |---|---|---|
996
+ | `id` | always | immutable item ID; the only identity |
997
+ | `number` | when the item carries one | the short handle, never identity |
998
+ | `title` | always | the title, truncated to `max_list_title_characters` code points |
999
+ | `title_truncated` | always | `true` when the projection dropped code points |
1000
+ | `kind` | always | `task` or `epic` |
1001
+ | `status` | always | the item status |
1002
+ | `priority` | when the item carries one | the supplied priority, never recomputed |
1003
+ | `created` | always | ISO calendar date |
1004
+ | `updated` | always | ISO calendar date |
1005
+ | `revision` | always | the exact item revision, as `inspect` reports it |
1006
+ | `ready` | always | readiness as of `as_of` |
1007
+
1008
+ #### Cursor pagination
1009
+
1010
+ A cursor is opaque and carries no ledger content. It binds three things: the
1011
+ digest of the query it was issued for, the snapshot revision it was issued
1012
+ against, and the offset to resume at. A caller passes it back unchanged with the
1013
+ same query.
1014
+
1015
+ A string that is not a cursor this core issued is `invalid-request`, exit 2, at
1016
+ `/cursor`. It is never treated as a restart from offset zero.
1017
+
1018
+ When either binding no longer holds, the cursor is refused with
1019
+ `list-snapshot-changed`, exit 4:
1020
+
1021
+ ~~~json
1022
+ {
1023
+ "ok": false,
1024
+ "command": "list",
1025
+ "contract_version": 5,
1026
+ "error": {
1027
+ "code": "list-snapshot-changed",
1028
+ "message": "The ledger snapshot the cursor was issued against is no longer current.",
1029
+ "details": {
1030
+ "mismatch": "snapshot",
1031
+ "cursor_snapshot_revision": "sha256:…",
1032
+ "current_snapshot_revision": "sha256:…"
1033
+ }
1034
+ }
1035
+ }
1036
+ ~~~
1037
+
1038
+ `details.mismatch` is `snapshot` when the ledger changed and `query` when the
1039
+ same cursor was replayed under a different `as_of`, `filters`, or `sort`. Both
1040
+ carry one remedy: restart pagination with no cursor. A caller MUST NOT combine
1041
+ pages from different snapshot revisions; across one unchanged traversal every
1042
+ matching item appears exactly once.
1043
+
1044
+ The response is a live projection on every invocation. A consumer may cache it
1045
+ as transient interface state only; it never becomes a second ledger store.
1046
+
1047
+ #### Exit and error summary
1048
+
1049
+ | Exit | Code | Condition |
1050
+ |---:|---|---|
1051
+ | 0 | none | the page was projected and fits the response bound |
1052
+ | 2 | `invalid-request` | arguments, JSON, query schema, or cursor form |
1053
+ | 2 | `list-response-too-large` | the exact rows exceed `max_list_response_bytes` |
1054
+ | 3 | `ledger-invalid` | the complete configured ledger is invalid |
1055
+ | 4 | `list-snapshot-changed` | the cursor's snapshot or query binding moved |
1056
+
1057
+ ### 5.2 The bounded workbench projection
1058
+
1059
+ A workbench shows a person which native lifecycle transitions an item can take
1060
+ before it asks for consent. `inspect` answers that read opt-in:
1061
+
1062
+ ~~~text
1063
+ wowbagger inspect --ledger <dir> (--id <id> | --number <n>) --workbench --as-of YYYY-MM-DD --json
1064
+ ~~~
1065
+
1066
+ `--workbench` requires `--as-of`, and `--as-of` is accepted only with
1067
+ `--workbench`: the projection reports readiness for an explicit date, so the
1068
+ date is part of the request rather than a default this core invents. An
1069
+ unpaired flag is an `invalid-request` issue at `/arguments`
1070
+ (`missing-argument` for the missing `--as-of`, `conflicting-argument` for the
1071
+ unpaired `--as-of`), and a date that is not an ISO calendar date is
1072
+ `invalid-value`. An invocation without `--workbench` is byte-identical to the
1073
+ lossless read of section 5: the two reads share one item resolution, and only
1074
+ the flagged one answers with a projection.
1075
+
1076
+ The read changes no ledger item, lock, claim journal, reconciliation log, or Git
1077
+ state. It takes no lock: a lock would make a read look like a lease.
1078
+
1079
+ `inspect --workbench` loads and validates the complete ledger and answers from
1080
+ that one snapshot. An invalid ledger returns `ledger-invalid`, exit 3, with
1081
+ `details.validation_errors` and nothing else. The lossless read attaches the
1082
+ resolved item to that refusal so an operator can read the bytes to repair; the
1083
+ workbench read cannot, because an affordance derived from a ledger this core has
1084
+ not judged would be a claim it cannot support. An unresolved selector is
1085
+ `item-not-found`, exit 2, with exactly the selector the caller used, as in
1086
+ section 5.
1087
+
1088
+ #### The response
1089
+
1090
+ ~~~json
1091
+ {
1092
+ "ok": true,
1093
+ "command": "inspect",
1094
+ "contract_version": 5,
1095
+ "result": {
1096
+ "workbench": {
1097
+ "projection_version": 1,
1098
+ "as_of": "2026-08-22",
1099
+ "snapshot": { "revision": "sha256:<hex>", "item_count": 5 },
1100
+ "observation": {
1101
+ "authority": "observed-snapshot",
1102
+ "rechecked_by": [
1103
+ "revision", "lock", "claim-fence", "reconciliation", "candidate-validation"
1104
+ ]
1105
+ },
1106
+ "item": {
1107
+ "id": "wb_...",
1108
+ "number": 10,
1109
+ "title": "Epic awaiting disposition",
1110
+ "title_truncated": false,
1111
+ "kind": "epic",
1112
+ "status": "backlog",
1113
+ "priority": 1,
1114
+ "parent": "wb_...",
1115
+ "created": "2026-08-02",
1116
+ "updated": "2026-08-05",
1117
+ "revision": "sha256:<hex>",
1118
+ "ready": false,
1119
+ "depends_on": { "entries": ["wb_..."], "total": 1, "truncated": false },
1120
+ "related": { "entries": [], "total": 0, "truncated": false }
1121
+ },
1122
+ "transition_options": [
1123
+ {
1124
+ "to_status": "done",
1125
+ "action": "complete",
1126
+ "decision_required": true,
1127
+ "minimum_date": "2026-08-05",
1128
+ "enabled": false,
1129
+ "precondition_issues": {
1130
+ "entries": [
1131
+ {
1132
+ "code": "live-dependencies",
1133
+ "field": "depends_on",
1134
+ "message": "Completion requires every depends_on target to be done.",
1135
+ "related_ids": { "entries": ["wb_..."], "total": 1, "truncated": false }
1136
+ }
1137
+ ],
1138
+ "total": 1,
1139
+ "truncated": false
1140
+ },
1141
+ "blockers": { "entries": [], "total": 0, "truncated": false }
1142
+ }
1143
+ ]
1144
+ }
1145
+ }
1146
+ }
1147
+ ~~~
1148
+
1149
+ `result` carries exactly one member, `workbench`, and its members are in the
1150
+ order above. The projection is negotiated by `projection_version`, its own
1151
+ version domain, exactly as `list` is negotiated by `query_version`.
1152
+
1153
+ `snapshot` is the same witness `list` returns: it covers every item's
1154
+ ledger-relative path and exact revision, so any item added, removed, renamed, or
1155
+ byte-modified changes it. It is in the response because an option's blockers and
1156
+ precondition issues are functions of the whole ledger, not of the one item, so
1157
+ the item revision alone would not name what was observed.
1158
+
1159
+ `item` is a bounded summary: `number`, `priority`, and `parent` are present only
1160
+ when the item carries them, `title` is projected to
1161
+ `max_workbench_title_characters` with `title_truncated` saying whether it was
1162
+ cut, `depends_on` and `related` are bounded collections, and `ready` is the
1163
+ section 5.1 readiness state for `as_of`. The projection carries no item source
1164
+ and no body: those are the lossless read of section 5, and this response stays
1165
+ inside an advertised bound.
1166
+
1167
+ `revision` is the exact revision of the item's own bytes, and it is what a caller
1168
+ sends as `transition`'s `expected_revision`.
1169
+
1170
+ #### Transition options
1171
+
1172
+ `transition_options` holds one option for every lifecycle target the section 8
1173
+ edge table allows out of this item's kind and status, in ascending status order.
1174
+ A target the table does not allow is absent, so `invalid-edge` never appears in a
1175
+ projected issue. A terminal item has no allowed target and its
1176
+ `transition_options` is `[]`.
1177
+
1178
+ | Member | Meaning |
1179
+ |---|---|
1180
+ | `to_status` | the target status a `transition` request would name |
1181
+ | `action` | the generated decision action, or `null` for the two no-decision edges |
1182
+ | `decision_required` | whether the request must carry `decision.summary` and `decision.rationale` |
1183
+ | `minimum_date` | the earliest legal transition date, `max(created, updated)` |
1184
+ | `enabled` | whether the observed ledger refuses nothing for this target at `minimum_date` |
1185
+ | `precondition_issues` | the observed section 8 precondition issues, bounded |
1186
+ | `blockers` | the observed section 8 multi-item blockers, bounded |
1187
+
1188
+ An option is evaluated at `minimum_date`, so neither date precondition can be
1189
+ what disables it: a caller reads `minimum_date` and sends that date or a later
1190
+ one. Issues and blockers use exactly the `transition` vocabulary — the same
1191
+ `code`, `field`, `message`, `item_id`, and ordering — so a consumer renders one
1192
+ set of refusal reasons. The one reshaped member is an issue's `related_ids`,
1193
+ which is a bounded collection here because that list grows with the ledger.
1194
+
1195
+ `enabled` is `true` exactly when both bounded collections observed nothing:
1196
+ `precondition_issues.total` and `blockers.total` are `0`. A disabled option still
1197
+ names why, so a workbench can show a target and the reason it cannot be taken
1198
+ rather than hiding it.
1199
+
1200
+ #### What the projection does not promise
1201
+
1202
+ `observation` states the semantics in the response rather than leaving them to a
1203
+ reader of this contract. `authority: "observed-snapshot"` says the projection is
1204
+ one observation of the named snapshot: it is not a lease, not a reservation, and
1205
+ not a promise that the option remains executable after the returned revision.
1206
+ `rechecked_by` names what a later `transition` rechecks under lock — the exact
1207
+ revision, the cooperative lock, the claim fence, reconciliation, and complete
1208
+ candidate validation — in the refusal precedence of section 8. A transition
1209
+ dispatched against a returned option can still be refused by any of them, and
1210
+ that refusal is the authority.
1211
+
1212
+ The affordance projection and `transition` share one implementation-level
1213
+ lifecycle definition (`src/lifecycle.js`): the edge table, the generated actions,
1214
+ the precondition issues, and the blockers have exactly one home, so an
1215
+ advertised option cannot drift from what the mutation does. A consumer therefore
1216
+ never reimplements the edge table, the date derivation, the decision
1217
+ requirement, or the blocker rules.
1218
+
1219
+ #### Exit and error summary
1220
+
1221
+ | Exit | Code | Condition |
1222
+ |---:|---|---|
1223
+ | 0 | none | the projection was answered and fits the response bound |
1224
+ | 2 | `invalid-request` | arguments, including the `--workbench`/`--as-of` pairing |
1225
+ | 2 | `item-not-found` | the selector resolved no item in a valid ledger |
1226
+ | 2 | `workbench-response-too-large` | the exact projection exceeds `max_workbench_response_bytes` |
1227
+ | 3 | `ledger-invalid` | the complete configured ledger is invalid |
1228
+
758
1229
  ## 6. Cooperative lock and snapshot protocol
759
1230
 
760
1231
  Per-ID locks live at:
@@ -1812,6 +2283,9 @@ patch.
1812
2283
  | patch-precondition-failed | id, issues |
1813
2284
  | candidate-invalid | id, validation_errors |
1814
2285
  | item-source-too-large | id, size_bytes, limit_bytes |
2286
+ | list-response-too-large | max_list_response_bytes, response_bytes, page_size |
2287
+ | list-snapshot-changed | mismatch, cursor_snapshot_revision, current_snapshot_revision |
2288
+ | workbench-response-too-large | id, max_workbench_response_bytes, response_bytes |
1815
2289
  | revision-conflict | id, expected_revision, actual_revision |
1816
2290
  | lock-held | id, lock_path, owner, owner_diagnostic |
1817
2291
  | id-collision | id, path, actual_revision |
@@ -1923,6 +2397,58 @@ A cleanup failure reports the remaining bounded artifacts. Locks are never
1923
2397
  auto-broken by age. Clients must inspect after committed-recovery or unknown
1924
2398
  outcomes and must not retry blindly.
1925
2399
 
2400
+ ### Response loss and once-only dispatch
2401
+
2402
+ A caller can lose the response to a mutation it already dispatched: the
2403
+ transport between the caller and the owning host can drop after the core
2404
+ started, and a dropped transport observes nothing about the ledger. This
2405
+ section is normative for every explicit user-triggered `create`, `transition`,
2406
+ and `patch`.
2407
+
2408
+ Only a complete observed process result establishes an outcome. A result is
2409
+ complete when the process is observed to have exited, both captured streams are
2410
+ complete and within their bounds, and standard output is one valid envelope for
2411
+ the dispatched command. Anything else establishes nothing about the ledger.
2412
+
2413
+ | Observation | Established outcome | Required client action |
2414
+ |---|---|---|
2415
+ | Complete envelope, exit 0, state committed | The mutation applied exactly as returned. | Continue. |
2416
+ | Complete envelope, state unchanged, any nonzero exit | The mutation did not create, remove, rename, or byte-modify an item. | Read the refusal; a revision conflict re-inspects. |
2417
+ | Complete envelope, exit 6 post-commit-recovery-required, state committed | The item is published; a verify or cleanup step still needs recovery. | Inspect the item and clear the reported recovery artifacts. Never repeat the mutation. |
2418
+ | Complete envelope, exit 6 write-outcome-unknown, state unknown | Publication was attempted and the visible bytes are indeterminate. | Validate, inspect, and compare the caller-known revision. Never repeat the mutation. |
2419
+ | Signal, timeout, orphan or containment doubt, or an incomplete or over-limit stream | Nothing. The mutation may or may not have applied. | Treat as unresolved. |
2420
+ | No envelope, a partial envelope, or no response at all because the transport was lost | Nothing. | Treat as unresolved. |
2421
+
2422
+ No row reports success that was not observed, and no row licenses a repeat of
2423
+ the mutation.
2424
+
2425
+ An unresolved outcome is answered by one sequence, never by a retry:
2426
+
2427
+ 1. Dispatch once, never replay, invalidate the inspected revision, reconnect,
2428
+ then re-read the ledger.
2429
+ 2. Re-read means `validate`, then `inspect` the caller-known ID. For `create`
2430
+ the caller already knows the ID it minted; for `transition` and `patch` the
2431
+ caller already knows the expected revision it sent.
2432
+ 3. Compare the observed current state with the caller's own pre-dispatch
2433
+ observation, and report the comparison as current state. A later item state
2434
+ describes only current ledger state; it never proves that the lost dispatch
2435
+ caused it.
2436
+ 4. Dispatch a fresh mutation only after a person reviews that comparison, and
2437
+ only as a new explicit decision built on the current revision.
2438
+
2439
+ Exit 4 is not response loss. A `revision-conflict` is a complete observed
2440
+ refusal with state unchanged: the item changed after it was inspected, and the
2441
+ core proves the write did not run. It invalidates the inspected revision and
2442
+ requires re-inspection; it never becomes an unresolved outcome and never
2443
+ triggers a retry of the same request bytes.
2444
+
2445
+ This contract carries no operation identity. There is no operation ID, no
2446
+ durable outcome store, and no replay endpoint, because the caller never
2447
+ replays: once-only dispatch plus honest unresolved outcomes need no
2448
+ correlation. Introducing replay-safe correlation requires a new contract
2449
+ decision that defines request binding, retention, and collision behavior before
2450
+ implementation.
2451
+
1926
2452
  ## 11. Normative design vectors
1927
2453
 
1928
2454
  The synthetic vectors under
@@ -1944,6 +2470,21 @@ frontmatter, the body append with its exclusivity refusal, and the declared
1944
2470
  extension-member correction with its untouched anchored extension nodes and
1945
2471
  its undeclared-member refusal.
1946
2472
 
2473
+ The `list` cases cover the seven categories section 5.1 is accepted against: an
2474
+ empty ledger's witnessed empty page (`list-empty`), a mixed-status filtered
2475
+ first page with its continuation cursor (`list-page`), a cursor whose bound
2476
+ snapshot moved (`list-stale-cursor`), an invalid ledger refused with no rows
2477
+ beside the error (`list-invalid-ledger`), and a maximum page whose exact rows
2478
+ exceed the advertised response bound (`list-response-too-large`).
2479
+
2480
+ The `workbench` cases cover section 5.2 the same way: a task's four allowed
2481
+ edges with their generated actions and derived minimum date
2482
+ (`workbench-task-options`), an epic carrying both observed precondition issues
2483
+ and both observed disposition blockers (`workbench-epic-blocked`), a terminal
2484
+ item with no target at all (`workbench-terminal`), and an invalid ledger refused
2485
+ with no projection beside the error (`workbench-invalid-ledger`). Every one
2486
+ declares identical before and after ledger digests: the projection is a read.
2487
+
1947
2488
  The runtime executes every vector as a black-box CLI test, including exact
1948
2489
  response bytes and the complete before/after ledger snapshot.
1949
2490