wowbagger 0.1.0-alpha.13 → 0.1.0-alpha.14

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.
@@ -14,7 +14,7 @@ publish something.
14
14
  Install the core separately before using this skill:
15
15
 
16
16
  ```sh
17
- npm install -g wowbagger@0.1.0-alpha.13
17
+ npm install -g wowbagger@0.1.0-alpha.14
18
18
  ```
19
19
 
20
20
  The core requires Node.js 20 or later. This plugin ships only agent
@@ -34,9 +34,9 @@ wowbagger capabilities --json
34
34
 
35
35
  Read the plain distribution version from the first command and the top-level
36
36
  `contract_version` from the second. **This skill requires distribution version
37
- `0.1.0-alpha.13` and core `contract_version: 5`.**
37
+ `0.1.0-alpha.14` and core `contract_version: 5`.**
38
38
 
39
- The distribution pin names the published `0.1.0-alpha.13` release; the cut that
39
+ The distribution pin names the published `0.1.0-alpha.14` release; the cut that
40
40
  publishes core `contract_version: 5` moves it. Earlier cores report
41
41
  `contract_version: 3` or lower and lack behavior this skill requires, including
42
42
  the bounded item source, so the version check refuses them. Do not soften
@@ -184,6 +184,32 @@ work read as ready.
184
184
  redaction and no access control. Say that plainly if a user asks for a report
185
185
  that hides work from a reader.
186
186
 
187
+ ## Every writer must be on the same core before the first create
188
+
189
+ On a provisioned ledger, `create` now records its allocation in the shared
190
+ claim journal before it publishes anything. That grammar is new, so the upgrade
191
+ is a hard cutover with no automatic migration and no mixed-version grace
192
+ period: **upgrade every writer in one Git coordination domain to the current
193
+ core before the first alpha.14 create.** A worktree left on the old core does
194
+ not write a duplicate — it stops making claim-protected mutations, which is the
195
+ safe outcome, not a usable one.
196
+
197
+ An old core cannot read the new create entry and says so badly. It answers
198
+ exit 6, `error.code` `claim-store-unavailable`, message
199
+ `The durable claim store is unavailable.`, and `error.details.reason`
200
+ `claim-store-unreadable`, leaves state unchanged, and writes no item. Read that
201
+ exact combination as **this repository was written by a newer Wowbagger;
202
+ upgrade this worktree to continue**, and say so to the user: the old binary is
203
+ immutable and can never print better guidance. (Item #185 is open for general
204
+ version-drift detection.)
205
+
206
+ Say what the fix does and does not cover. It closes the reported
207
+ PropertyCompass2 collision: cooperating alpha.14 worktrees of one clone that
208
+ share one Git common directory can no longer commit two items carrying the same
209
+ number. Separate clones, separate machines, alpha.13 writers before the hard
210
+ cutover, and noncooperating writes stay outside that fence and still rely on
211
+ branch integration plus `validate`.
212
+
187
213
  ## Writing
188
214
 
189
215
  Every write is an explicit, reviewable Git change. Show the user the command
@@ -434,6 +460,11 @@ authorized predecessor/successor window produces no finding, so another
434
460
  mutation can run before the first is committed. Do not mistake acceptance for
435
461
  durability. Commit each mutation anyway, then run `claim-verify`.
436
462
 
463
+ `create` never gets that window. A new item has no earlier authorized revision,
464
+ so Git `HEAD` is the only place its authorized bytes can live, and an
465
+ uncommitted create blocks every later mutation — including the next create —
466
+ with `git-finalization-required`.
467
+
437
468
  **`claim-verify` is the reconciliation procedure for that refusal.** Do not go
438
469
  looking for another verb; there is none. Read `details.findings`, do exactly
439
470
  what each finding's `remediation` string says (it names the path), run
@@ -491,7 +522,10 @@ commit. With `--auto-commit`, `changed_paths` matches `commit_paths`, and
491
522
  `git_commit` proves the commit.
492
523
 
493
524
  Batch work is where this bites: filing ten items means ten commits, not one
494
- commit at the end. Tell the user that before starting a batch.
525
+ commit at the end. Tell the user that before starting a batch. There is no
526
+ batch mutation to reach for — the create-then-commit loop is the supported bulk
527
+ pattern, and item #186 is open to design a safe batch create. `--auto-commit`
528
+ on each `create` is the shortest form of that loop.
495
529
 
496
530
  ### Or use --auto-commit and let one invocation do it
497
531
 
@@ -508,9 +542,11 @@ One flagged invocation refuses if anything is staged anywhere or any foreign
508
542
  path under the ledger is dirty, reconciles, runs the mutation unchanged,
509
543
  commits exactly the changed item plus at most one
510
544
  `.wowbagger/reconcile-<namespace>.md` with a fixed subject, verifies that commit,
511
- and runs `claim-verify` before it answers. A command that owns the claim journal
512
- may rebuild only its derived reconciliation log during preflight. `create`
513
- remains strict, and every other dirty ledger path still refuses.
545
+ and runs `claim-verify` before it answers. A successful `create --auto-commit`
546
+ commits exactly two paths: the created item and that reconciliation log. Every
547
+ command rebuilds its own derived reconciliation log during preflight, but
548
+ `create` refuses a log that was already dirty when you invoked it, and every
549
+ other dirty ledger path still refuses.
514
550
 
515
551
  Preflight and post-commit reconciliation block findings for the requested item.
516
552
  An unrelated `worktree-synchronization-required` finding remains visible to
@@ -610,29 +646,85 @@ envelope's `limits.cross_worktree_coordination: false` as permission to write
610
646
  with hostile or noncooperating tools — it only says the core never synchronizes
611
647
  checkouts.
612
648
 
613
- A recorded `transition`, `patch`, or claimed publication blocks mutations
614
- targeting that same item with exit 6 `claim-store-unavailable`, reason
615
- `publication-reconciliation-required`. An unrelated item mutation may proceed
616
- when the only finding is `worktree-synchronization-required`. `create` records
617
- nothing, so it never creates a publication block.
649
+ A recorded `create`, `transition`, `patch`, or claimed publication blocks
650
+ mutations targeting that same item with exit 6 `claim-store-unavailable`,
651
+ reason `publication-reconciliation-required`. An unrelated item mutation may
652
+ proceed when the only finding is `worktree-synchronization-required`.
653
+
654
+ `create` is the one exception to that scoping, because it allocates the next
655
+ number from the items this checkout can see. It also refuses when the journal
656
+ records a committed item this worktree does not hold at all: that item carries
657
+ a number nobody here can read, so the next number allocated here might already
658
+ be taken. A stale revision of an item this worktree does hold is not a blocker
659
+ for `create` — a number is immutable, so the local maximum is still right. The
660
+ refusal is the same exit 6 `claim-store-unavailable` with reason
661
+ `publication-reconciliation-required`, state `unchanged`, and no item file
662
+ written; integrate the missing item, run `claim-verify` until it exits 0, and
663
+ resend the same request, which then takes the next number.
664
+
665
+ That fence stops new collisions; it does not repair old ones. A ledger that
666
+ already carries duplicate numbers is item #182's recovery work: it fails
667
+ `validate` and refuses every mutation until #182 ships. Never hand-edit a
668
+ `number` to clear it — the edit skips the collision and reference checks every
669
+ mutation runs and can leave dangling `depends_on`, `related`, and parent
670
+ references that nothing reports.
618
671
 
619
672
  Read `error.details.findings[0].reason` and act on the named item:
620
673
 
621
674
  - `git-finalization-required` — you wrote the item here and have not committed.
622
675
  Commit, then `claim-verify`.
623
- - `worktree-synchronization-required` — another worktree wrote the item. If the
676
+ - `worktree-synchronization-required` — another worktree wrote the item.
677
+ `owner_ref` names an **active named worktree** and nothing else: it is always
678
+ the branch of a live worktree that carries the expected revision. If the
624
679
  finding names `owner_ref` and `owner_commit`, WAIT for that owner to publish,
625
- then synchronize this checkout and run `claim-verify`. If it carries
626
- `owner_unavailable: true`, follow its `remediation`: a revision that is not
627
- yet reachable means WAIT for the owning worktree to commit, then synchronize;
628
- only ownership that cannot be established from reachable refs calls for
629
- inspecting reachable or dangling commits with explicit restore or
630
- `claim-adopt`. Never merge unrelated live work.
680
+ then synchronize this checkout and run `claim-verify`. `owner_unavailable:
681
+ true` means no such worktree exists, and it covers three cases: the expected
682
+ revision is not reachable at all, a live sibling holds it on a detached
683
+ `HEAD`, or it is reachable only from a tag, a remote-tracking ref, or a
684
+ branch no worktree has checked out. Reachability is not ownership; a ref you
685
+ can see is not a worktree that can publish. Follow the `remediation`, which
686
+ separates those cases. A revision that is **not yet reachable** means WAIT for
687
+ the owning worktree to commit, then synchronize. A revision that is
688
+ **reachable in Git while no active named worktree owner is established** —
689
+ a tag, a remote-tracking ref, an unchecked-out branch, or a detached sibling
690
+ carries it — is not a wait at all: inspect that reachable history, then
691
+ restore the authorized bytes or use explicit `claim-adopt` after review.
692
+ Ownership that **cannot be established from reachable refs** — the item has
693
+ never existed in this checkout — calls for inspecting reachable or dangling
694
+ commits with the same explicit restore or `claim-adopt`. Never merge unrelated
695
+ live work.
631
696
  - `unauthorized-revision` — the item changed outside the protocol. Two remedies
632
697
  are explicit: **restore** the authorized revision and run `claim-verify` to
633
698
  discard the edit, or **adopt** the committed revision and run `claim-verify`
634
699
  to keep it. Ask before discarding reviewed work.
635
700
 
701
+ Two live worktrees answering to one identity, or a worktree roster the
702
+ coordinator could not finish reading, refuse before anything is classified.
703
+ You get exit 6 `claim-store-unavailable`, reason `claim-store-unreadable`, with
704
+ `error.details.identity_diagnostic`: `duplicate-worktree-identity` naming the
705
+ `worktree_id` and `live_worktree_count`, or `worktree-enumeration-failed` with
706
+ no further member. Auto-commit reports the same diagnostic inside
707
+ `auto-commit-preflight-failed` with `retryable: false`. Neither is retryable
708
+ and neither is yours to repair by editing files. Report the diagnostic verbatim,
709
+ including the `worktree_id` and `live_worktree_count`: a duplicate means two
710
+ live worktrees hold the same identity file, usually because a private Git
711
+ directory was copied, and which worktree keeps the UUID is a person's decision.
712
+ An enumeration failure means a registered worktree path could not be read. The
713
+ identity itself is an opaque UUID a worktree writes once into its private Git
714
+ directory. Never create, copy, or edit it.
715
+
716
+ **`claim-verify` is repository-wide, and a clean mutation does not make it
717
+ exit 0.** It names no target, so any blocking finding anywhere in the
718
+ repository keeps it at exit 6 — including a finding on an item belonging to
719
+ work you have nothing to do with. On a repository with live sibling worktrees
720
+ you may commit every one of your own mutations and still never see
721
+ `claim-verify` exit 0. That is current behavior; item #184 is open in triage to
722
+ decide the supported verification surface. When the remaining findings all name
723
+ items you are not working on, say so and stop; do not hand-edit an item, and do
724
+ not run `claim-adopt` on a sibling's item to force exit 0. Adoption moves the
725
+ coordinator's authorized revision and is not a way to silence someone else's
726
+ finding.
727
+
636
728
  Adoption is per item and per revision explicit. Name the item and both
637
729
  revisions, take them from the finding, and commit the edited bytes first:
638
730
 
@@ -692,7 +784,9 @@ Use the claimed write path as one complete loop:
692
784
  9. Run `claim-verify` after the commit or merge. It finalizes the Git outcome,
693
785
  repairs response-loss cases, and reports later revision drift. Require exit
694
786
  0 before the next mutating command; exit 6 means findings remain, so act on
695
- each `remediation` string and run it again.
787
+ each `remediation` string and run it again. If every remaining finding names
788
+ an item you are not working on, that is the repository-wide scope described
789
+ above, not a failure of your work: report it instead of forcing exit 0.
696
790
  10. Release the claim with its current observed state.
697
791
  11. Run `validate` and show the resulting diff.
698
792
 
@@ -717,7 +811,8 @@ response envelopes, refusal precedence, and recovery rules.
717
811
  `git add <dir> && git commit`.
718
812
  8. On a provisioned ledger, run `claim-verify --ledger <dir> --json` and
719
813
  require exit 0 before the next `create`, `transition`, `parent-migrate`,
720
- `snooze`, `patch`, or `publish-claimed`.
814
+ `snooze`, `patch`, or `publish-claimed`. Findings that name only unrelated
815
+ items are repository-wide scope; report them rather than forcing exit 0.
721
816
 
722
817
  Write, commit, `claim-verify`, next write. The unclaimed loop obeys the same
723
818
  rule as the claimed one, because both run through the same coordinator. Steps 7
@@ -37,6 +37,12 @@ export async function withLegacyMutationFence(
37
37
  const capability = resolveWorkClaimCapability({ gitCommonDir, namespace });
38
38
  if (!capability.claim_protected_publication) return write();
39
39
 
40
+ // A create is the one mutation that reads an identity it was not given: it
41
+ // allocates the ledger's next number from the items this checkout holds. Its
42
+ // reconciliation therefore stays target-scoped like every other mutation, and
43
+ // gains one extra barrier below, for the coordinated items this working
44
+ // ledger cannot see at all.
45
+ const create = command === 'create-v1';
40
46
  const storePath = claimStorePath(gitCommonDir, namespace);
41
47
  const journalPath = claimJournalPath(gitCommonDir, namespace);
42
48
  let intent = null;
@@ -61,9 +67,15 @@ export async function withLegacyMutationFence(
61
67
  physicalNow: new Date().toISOString(),
62
68
  targetItemId: itemId,
63
69
  writeLogOnUnsafe: false,
64
- writeLogWhenEmpty: command !== 'create-v1',
70
+ writeLogWhenEmpty: !create,
65
71
  });
66
- if (reconciled.unsafe) {
72
+ // The extra create barrier promised above rides on the same refusal: a
73
+ // coordinated item this checkout does not hold carries a number nobody
74
+ // here can read, so the next number this create would allocate may be one
75
+ // a sibling worktree already published. A stale revision of an item that
76
+ // is present hides no number: an item's number is immutable, so target
77
+ // scoping above still lets that create through.
78
+ if (reconciled.unsafe || (create && reconciled.missingCoordinatedItems.length > 0)) {
67
79
  return claimStoreUnavailable(responseCommand, 'publication-reconciliation-required', {
68
80
  findings: reconciled.findings,
69
81
  });
@@ -75,7 +87,7 @@ export async function withLegacyMutationFence(
75
87
  const projected = [...reconciled.entries];
76
88
  const record = reconciled.state.claims.find((entry) => entry.item_id === itemId)
77
89
  ?? { item_id: itemId, last_epoch: '0', active: null };
78
- const mustRefuse = command === 'create-v1'
90
+ const mustRefuse = create
79
91
  ? record.last_epoch !== '0'
80
92
  : record.active !== null && observedAt < record.active.expires_at;
81
93
  if (mustRefuse) return legacyRefusal(responseCommand, namespace, itemId, observedAt, record);
@@ -110,6 +122,7 @@ export async function withLegacyMutationFence(
110
122
  attempt_id: attemptId,
111
123
  ledger_namespace: namespace,
112
124
  item_id: itemId,
125
+ ...(create ? { command } : {}),
113
126
  observed_revision: expectedRevision,
114
127
  observed_at: observedAt,
115
128
  };
@@ -180,6 +193,7 @@ export async function withLegacyMutationFence(
180
193
  attempt_id: intent.attempt_id,
181
194
  ledger_namespace: namespace,
182
195
  item_id: itemId,
196
+ ...(create ? { command } : {}),
183
197
  observed_revision: intent.expected_revision,
184
198
  observed_at: observedAt,
185
199
  }));
@@ -20,6 +20,8 @@ const JOURNAL_ENTRY_TYPES = new Set([
20
20
  'publish-intent',
21
21
  'revision-adoption',
22
22
  ]);
23
+ // Every command the legacy mutation fence journals.
24
+ const LEGACY_MUTATION_COMMANDS = new Set(['patch-v1', 'transition-v1', 'create-v1']);
23
25
  // The reconciliation log is a tracked derived artifact. Entry types that every
24
26
  // invocation appends, including refusals and read-only verification, stay out
25
27
  // of it so a mutation that changes nothing leaves the working tree unchanged.
@@ -177,6 +179,7 @@ async function readJournalEntries(journalPath, namespace = null) {
177
179
  const lines = source.split('\n').filter(Boolean);
178
180
  if (lines.length > MAX_JOURNAL_ENTRIES) throw journalCapacityExceeded();
179
181
  const entries = lines.map((line) => JSON.parse(line));
182
+ const createAttempts = new Map();
180
183
  for (let index = 0; index < entries.length; index += 1) {
181
184
  if (!Number.isSafeInteger(entries[index]?.seq) || entries[index].seq !== index + 1) {
182
185
  throw journalInvalid('non-contiguous-sequence');
@@ -187,6 +190,9 @@ async function readJournalEntries(journalPath, namespace = null) {
187
190
  if (!validJournalEntry(entries[index], namespace)) {
188
191
  throw journalInvalid('invalid-entry');
189
192
  }
193
+ if (!validCreateResolution(entries[index], createAttempts)) {
194
+ throw journalInvalid('invalid-entry');
195
+ }
190
196
  }
191
197
  return entries;
192
198
  }
@@ -282,8 +288,10 @@ function validJournalEntry(entry, namespace) {
282
288
  && typeof entry.ledger_namespace === 'string'
283
289
  && (namespace === null || entry.ledger_namespace === namespace)
284
290
  && typeof entry.item_id === 'string'
285
- && ['patch-v1', 'transition-v1'].includes(entry.command)
286
- && typeof entry.expected_revision === 'string'
291
+ && LEGACY_MUTATION_COMMANDS.has(entry.command)
292
+ && (entry.command === 'create-v1'
293
+ ? entry.expected_revision === null
294
+ : typeof entry.expected_revision === 'string')
287
295
  && typeof entry.candidate_revision === 'string'
288
296
  && (!Object.hasOwn(entry, 'item_path') || typeof entry.item_path === 'string')
289
297
  && (!Object.hasOwn(entry, 'writer_worktree_id')
@@ -295,7 +303,7 @@ function validJournalEntry(entry, namespace) {
295
303
  && typeof entry.ledger_namespace === 'string'
296
304
  && (namespace === null || entry.ledger_namespace === namespace)
297
305
  && typeof entry.item_id === 'string'
298
- && ['patch-v1', 'transition-v1'].includes(entry.command)
306
+ && LEGACY_MUTATION_COMMANDS.has(entry.command)
299
307
  && typeof entry.committed_revision === 'string'
300
308
  && (!Object.hasOwn(entry, 'item_path') || typeof entry.item_path === 'string')
301
309
  && (!Object.hasOwn(entry, 'writer_worktree_id')
@@ -303,11 +311,19 @@ function validJournalEntry(entry, namespace) {
303
311
  && typeof entry.observed_at === 'string';
304
312
  }
305
313
  if (entry.type === 'legacy-mutation-abort') {
314
+ // A create abort names no predecessor revision, so the absence of one is
315
+ // its evidence. Patch and transition aborts keep their exact legacy shape
316
+ // and never carry a command.
317
+ const validAbortRevision = (
318
+ !Object.hasOwn(entry, 'command') && typeof entry.observed_revision === 'string'
319
+ ) || (
320
+ entry.command === 'create-v1' && entry.observed_revision === null
321
+ );
306
322
  return typeof entry.attempt_id === 'string'
307
323
  && typeof entry.ledger_namespace === 'string'
308
324
  && (namespace === null || entry.ledger_namespace === namespace)
309
325
  && typeof entry.item_id === 'string'
310
- && typeof entry.observed_revision === 'string'
326
+ && validAbortRevision
311
327
  && typeof entry.observed_at === 'string';
312
328
  }
313
329
  if (entry.type === 'revision-adoption') {
@@ -351,6 +367,30 @@ function validJournalEntry(entry, namespace) {
351
367
  && typeof entry.git_commit === 'string';
352
368
  }
353
369
 
370
+ // A journal-fenced create is only meaningful as a pair: the terminal that ends
371
+ // an attempt must name the intent that opened it, so a standalone create
372
+ // terminal names no authorized attempt and fails closed. Patch and transition
373
+ // entries remain valid standing alone, so only create-v1 attempts are tracked.
374
+ function validCreateResolution(entry, attempts) {
375
+ if (entry.command !== 'create-v1') return true;
376
+ if (entry.type === 'legacy-mutation-intent') {
377
+ attempts.set(entry.attempt_id, entry);
378
+ return true;
379
+ }
380
+ if (entry.type !== 'legacy-mutation' && entry.type !== 'legacy-mutation-abort') return true;
381
+ const intent = attempts.get(entry.attempt_id);
382
+ if (intent === undefined || intent.item_id !== entry.item_id) return false;
383
+ // A committed create publishes exactly the revision its intent proposed; an
384
+ // aborted one publishes none.
385
+ if (entry.type === 'legacy-mutation' && intent.candidate_revision !== entry.committed_revision) {
386
+ return false;
387
+ }
388
+ // One attempt resolves once, so the intent is consumed here and a repeated
389
+ // resolution finds no open attempt to end.
390
+ attempts.delete(entry.attempt_id);
391
+ return true;
392
+ }
393
+
354
394
  function isRecord(value) {
355
395
  return value !== null && typeof value === 'object' && !Array.isArray(value);
356
396
  }