borgmcp-shared 0.10.1 → 0.12.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 (43) hide show
  1. package/CONTRIBUTING.md +2 -2
  2. package/README.md +22 -15
  3. package/RELEASES.md +14 -0
  4. package/dist/conformance/adapter.d.ts +3 -0
  5. package/dist/conformance/adapter.d.ts.map +1 -1
  6. package/dist/conformance/adapter.js +91 -10
  7. package/dist/conformance/adapter.js.map +1 -1
  8. package/dist/conformance/index.d.ts +13 -0
  9. package/dist/conformance/index.d.ts.map +1 -1
  10. package/dist/conformance/index.js +83 -14
  11. package/dist/conformance/index.js.map +1 -1
  12. package/dist/protocol/contract.d.ts +2 -1
  13. package/dist/protocol/contract.d.ts.map +1 -1
  14. package/dist/protocol/contract.js +9 -4
  15. package/dist/protocol/contract.js.map +1 -1
  16. package/dist/protocol/coordination.d.ts.map +1 -1
  17. package/dist/protocol/coordination.js +8 -2
  18. package/dist/protocol/coordination.js.map +1 -1
  19. package/dist/protocol/errors.d.ts +1 -0
  20. package/dist/protocol/errors.d.ts.map +1 -1
  21. package/dist/protocol/errors.js +1 -0
  22. package/dist/protocol/errors.js.map +1 -1
  23. package/dist/protocol/types.d.ts +2 -0
  24. package/dist/protocol/types.d.ts.map +1 -1
  25. package/dist/protocol/version.d.ts +1 -1
  26. package/dist/protocol/version.js +1 -1
  27. package/dist/templates.d.ts +1 -1
  28. package/dist/templates.d.ts.map +1 -1
  29. package/dist/templates.js +178 -53
  30. package/dist/templates.js.map +1 -1
  31. package/docs/release-records.json +103 -0
  32. package/docs/releases/0.12.0.md +7 -0
  33. package/docs/releasing.md +104 -23
  34. package/docs/template-lifecycle.md +66 -0
  35. package/package.json +2 -2
  36. package/src/conformance/adapter.ts +110 -9
  37. package/src/conformance/index.ts +101 -15
  38. package/src/protocol/contract.ts +11 -5
  39. package/src/protocol/coordination.ts +8 -2
  40. package/src/protocol/errors.ts +1 -0
  41. package/src/protocol/types.ts +2 -0
  42. package/src/protocol/version.ts +2 -2
  43. package/src/templates.ts +185 -53
@@ -1,4 +1,49 @@
1
1
  [
2
+ {
3
+ "outcome": "published",
4
+ "version": "0.6.3",
5
+ "tag": "v0.6.3",
6
+ "tag_object": "fd6aa8858e6565ad49a69144736e8fe12a8295bb",
7
+ "commit": "f9d79866e1f7fe0f9ea31099bce1b36af9388ecc",
8
+ "tree": "2fd38cc7b1a0332e987cb95881c2ca8ffe2a28e3",
9
+ "workflow_run_id": 30096375533,
10
+ "workflow_run_attempt": 1,
11
+ "workflow_conclusion": "success",
12
+ "verify_job_id": null,
13
+ "publish_job_id": null,
14
+ "artifact_integrity": "sha512-9osP68zH3a2IbyZr4BHiiz9SF+F4kqqxkwUQiun0GXUzu8Vw/+JorCG8OnyRFm8KpXNZ2Yh3KQxqcRD583l1NA==",
15
+ "reconstructed": true
16
+ },
17
+ {
18
+ "outcome": "published",
19
+ "version": "0.6.4",
20
+ "tag": "v0.6.4",
21
+ "tag_object": "f79b0683686d3c359023a17f6e8a92efd888104a",
22
+ "commit": "fa8a2dc072d4ffe2a16d5f02576fead822a2f72e",
23
+ "tree": "2efa6d9553ca7f80c5c477ca39cf35bef55ad01c",
24
+ "workflow_run_id": 30169732628,
25
+ "workflow_run_attempt": 1,
26
+ "workflow_conclusion": "success",
27
+ "verify_job_id": null,
28
+ "publish_job_id": null,
29
+ "artifact_integrity": "sha512-Wm4b0uoOAw9JCz5OTHD0Q2uXKkeWYdkVksdeZvRG8l62XGMY+G8GkNEsZT9L533LbVbQ29GhgF0htjDenQThDg==",
30
+ "reconstructed": true
31
+ },
32
+ {
33
+ "outcome": "published",
34
+ "version": "0.7.0",
35
+ "tag": "v0.7.0",
36
+ "tag_object": "5ddaf5821c49ac0893fdffbac6115b48d0795281",
37
+ "commit": "30af2629052014feba3f3ebf4d9eb29e977e38a7",
38
+ "tree": "da7b7d67f2d00403d3536923897bd52b73a479db",
39
+ "workflow_run_id": 30337670996,
40
+ "workflow_run_attempt": 1,
41
+ "workflow_conclusion": "success",
42
+ "verify_job_id": null,
43
+ "publish_job_id": null,
44
+ "artifact_integrity": "sha512-CLpQo3P/fE9GzC/R7Xw61LWxxsgcVLwmpGb5Ot5PDRe1Bvnf+EB31kiBdU5rvyvvPhiK4oUQtJAZE4ULFRAA2A==",
45
+ "reconstructed": true
46
+ },
2
47
  {
3
48
  "outcome": "published",
4
49
  "version": "0.7.1",
@@ -13,6 +58,36 @@
13
58
  "publish_job_id": null,
14
59
  "artifact_integrity": "sha512-d4GE7ezGyoFN0uwA62gvRPUMfMdJ1kxhTunqU8dW3Qhy1n2SlNXZWr1Nthm6Goo0bAgvax8KEywyReodA6FyQA=="
15
60
  },
61
+ {
62
+ "outcome": "published",
63
+ "version": "0.8.0",
64
+ "tag": "v0.8.0",
65
+ "tag_object": "b43710281df47f9a6f3d1dafd8ef6b2b527e718d",
66
+ "commit": "24d260cd882b306b48c5c9397b6c3eb21b8b4990",
67
+ "tree": "3067a71b57ec96041269282ce3d71f38ec624cc5",
68
+ "workflow_run_id": 30720286941,
69
+ "workflow_run_attempt": 1,
70
+ "workflow_conclusion": "success",
71
+ "verify_job_id": null,
72
+ "publish_job_id": null,
73
+ "artifact_integrity": "sha512-0p/ZC03Y6G4N6bfBjMD5b+WcQXcLalhU/3QqGjSWYCFKa3q2/183O53w6Tu6y8h2DkhTw5prnamsOozmVpXZew==",
74
+ "reconstructed": true
75
+ },
76
+ {
77
+ "outcome": "published",
78
+ "version": "0.8.1",
79
+ "tag": "v0.8.1",
80
+ "tag_object": "cc690f6d41aa34b3f2884db500bc9979ab2de1db",
81
+ "commit": "d29c5cf9285dc62b01258b640102f8cf21a9ec21",
82
+ "tree": "b2134c0e54041554e87bf4b4d044e3f476cb1f2b",
83
+ "workflow_run_id": 30853413999,
84
+ "workflow_run_attempt": 1,
85
+ "workflow_conclusion": "success",
86
+ "verify_job_id": null,
87
+ "publish_job_id": null,
88
+ "artifact_integrity": "sha512-0+UfSDnAO0mWYIqxzzppekatya1gCF9ntZmuk7VVr2Hnf9QeM1S7uPLP/TWwDSTLtczB4EUoKA71E1WZJH2mbg==",
89
+ "reconstructed": true
90
+ },
16
91
  {
17
92
  "outcome": "published",
18
93
  "version": "0.9.0",
@@ -40,5 +115,33 @@
40
115
  "verify_job_id": null,
41
116
  "publish_job_id": null,
42
117
  "artifact_integrity": "sha512-NYZJi6z0g/Txb6ge+5NgRPNRszVEi0eNmICxkoZq5bGkJWm5qEvNSt3ws90Xz8IOKuWcyHC9i6sAK6cbZxwYDw=="
118
+ },
119
+ {
120
+ "outcome": "published",
121
+ "version": "0.10.1",
122
+ "tag": "v0.10.1",
123
+ "tag_object": "7fbf92b057988c6893ad4ff2af13dfa7bc50c131",
124
+ "commit": "691c9498c6248363a6f7d056178321443d12cf43",
125
+ "tree": "331d100770eb2d23f6e06d56a595141a6a9c1749",
126
+ "workflow_run_id": 31265832036,
127
+ "workflow_run_attempt": 1,
128
+ "workflow_conclusion": "success",
129
+ "verify_job_id": null,
130
+ "publish_job_id": null,
131
+ "artifact_integrity": "sha512-IAESc6+VpUGECNabFkO60agvga0B8Fx0hk2RMUkSDCnlIjMvDodMFKlqw+TOzdXtjf0Uk9LJi9ZCU4HfRYRRhw=="
132
+ },
133
+ {
134
+ "outcome": "published",
135
+ "version": "0.11.0",
136
+ "tag": "v0.11.0",
137
+ "tag_object": "9086deca3f3d05b4702a415c6d50b19c54bcc7f2",
138
+ "commit": "c1df96a858ff7c587d42508d388aa517a0b8545c",
139
+ "tree": "84a5599afba05d9630e0e41beb401f700d83cd68",
140
+ "workflow_run_id": 31531549256,
141
+ "workflow_run_attempt": 1,
142
+ "workflow_conclusion": "success",
143
+ "verify_job_id": null,
144
+ "publish_job_id": null,
145
+ "artifact_integrity": "sha512-I8mixCbSrLKyOAAyqEI/HZJ8cML2rz3r812Up8pr547OdAk9LxZevdCo7ojG42ZwrUmS5u7iKQPg7Vk1XvtX1g=="
43
146
  }
44
147
  ]
@@ -0,0 +1,7 @@
1
+ This release introduces the protocol v9 reliable-coordination contract used by
2
+ the matching Borg MCP server and client releases.
3
+
4
+ - Log posts now carry a canonical UUID so an interrupted write can be retried safely.
5
+ - Exact same-author retries return the original entry and identify the result as deduplicated.
6
+ - Reusing that UUID with changed message or routing returns a typed conflict, while another author may use the same UUID independently.
7
+ - Fresh attach responses include the current log cursor so clients can resume without missing activity between attachment and stream startup.
package/docs/releasing.md CHANGED
@@ -13,16 +13,18 @@ The release lane has one build, test, package, and publication authority:
13
13
  1. verifies the public repository context, exact package version, annotated tag,
14
14
  tag commit, and ancestry on protected `main`;
15
15
  2. installs locked dependencies without lifecycle scripts and runs dependency
16
- audit, type checks, the full test suite, one clean build, and generated-output
17
- drift detection;
16
+ audit, one clean build, type checks, and the full test suite;
18
17
  3. creates one local tarball, enforces the packed-artifact policy, and installs
19
18
  that tarball into a clean consumer that imports every public export;
20
19
  4. rejects an existing immutable version or a package not owned solely by the
21
20
  configured npm owner before any registry mutation;
22
- 5. publishes that exact local tarball through npm Trusted Publishing with
23
- provenance. Successful completion of `npm publish` is the workflow's terminal
24
- release boundary; no post-publication registry readback can fail the immutable
25
- release after npm has accepted it.
21
+ 5. stages that exact local tarball through npm Trusted Publishing with
22
+ provenance. Successful completion of `npm stage publish` means npm accepted
23
+ the immutable stage; it does not mean the version is publicly available.
24
+
25
+ The release becomes live only after the authorized operator approves the stage
26
+ with 2FA and the canonical registry exposes the expected version and integrity.
27
+ No workflow post-publication readback can fail a release after that boundary.
26
28
 
27
29
  npm and GitHub produce the registry signature and publish attestation as part of
28
30
  Trusted Publishing. The repository does not reconstruct or immediately read back
@@ -37,6 +39,7 @@ Keep these controls in place:
37
39
 
38
40
  1. `publish.yml` is the only npm Trusted Publisher workflow for organization
39
41
  `Byte-Ventures`, repository `borg-mcp-shared`, and environment `npm-publish`.
42
+ Its allowed actions enable `npm stage publish` and disable `npm publish`.
40
43
  2. The `npm-publish` environment requires the authorized sole operator
41
44
  `TheodorStorm`, permits that operator's self-review, prevents administrator
42
45
  bypass, and allows only protected `v*.*.*` tags.
@@ -64,9 +67,12 @@ repository, workflow output, artifact, issue, or shell history.
64
67
 
65
68
  ## Release Procedure
66
69
 
67
- 1. Merge the exact version, lockfile, generated output, and release documentation
70
+ 1. Merge the exact version, lockfile, source, and release documentation
68
71
  to protected `main` after the required exact-SHA Code Review, Security Review,
69
72
  and Release Quality gates.
73
+ Release documentation includes curated `docs/releases/<version>.md` notes.
74
+ The GitHub Release operator reads those exact bytes from the tagged commit
75
+ under `News and fixes`; it links the merged release PR but never renders its body.
70
76
  2. Verify repository controls with the configuration guard above. Confirm the
71
77
  target version does not already exist and `borgmcp-shared` is owned solely by
72
78
  `byteventures`.
@@ -78,16 +84,70 @@ repository, workflow output, artifact, issue, or shell history.
78
84
  5. Obtain the separately required environment approval, then approve that exact
79
85
  pending job. Approval does not permit a local rebuild or alternate artifact.
80
86
  6. Require the protected publish job to complete successfully before announcing
81
- the version or updating consumers. Registry propagation and later consumer
82
- availability are operational observations, not release-workflow gates.
87
+ that the stage is ready. Record its stage UUID; do not announce the version,
88
+ update consumers, synchronize the site, create a GitHub Release, or record a
89
+ `published` outcome while it remains staged.
90
+
91
+ Before release preparation or trusted identity verification can pass, the
92
+ greatest stable npm version below the current package version must already have a
93
+ `published` ledger entry. This prevents a later release from silently skipping
94
+ the immediately previous publication. A final `"reconstructed": true` marker
95
+ means the entry was recovered later from the same annotated-tag, Actions-run, and
96
+ npm-integrity authorities; it does not weaken verification or change the outcome.
97
+
98
+ ## Coupled Stage Approval
99
+
100
+ For a coordinated shared, server, and client release, require all three tag
101
+ workflows to succeed before approving any stage. Use authenticated `npm stage
102
+ list` and `npm stage view` to record and verify each UUID, package, version, and
103
+ `latest` tag. Bind each source run, annotated tag, and commit separately from
104
+ GitHub workflow and tag evidence. Before any approval, run `npm stage download
105
+ <UUID>` for every package. For server and client, verify the downloaded SHA-512
106
+ against the same-run artifact report. For shared, independently compute and
107
+ record the stage-download SHA-512 SRI, then carry that exact value into the
108
+ required post-approval `--integrity` argument; shared has no same-run artifact
109
+ report. Exercise the exact downloaded coupled set before approving any stage.
110
+ Confirm public `latest` and the public version lists still describe the prior
111
+ coherent set.
112
+
113
+ Approve the verified stages in one operator session with 2FA:
114
+
115
+ 1. `npm stage approve <shared-stage-uuid>`
116
+ 2. `npm stage approve <server-stage-uuid>`
117
+ 3. `npm stage approve <client-stage-uuid>`
118
+
119
+ The three approvals are not atomic. Shared-first does not change the exact
120
+ shared pins of the live client and server. Server-second opens a bounded window
121
+ in which `latest` client and server may not match; client-third closes it. Staging
122
+ reduces that window to the approval sequence but does not eliminate it.
123
+
124
+ After each approval, continue only while the remaining stage is still the exact
125
+ verified candidate. Resolve an ambiguous approval through authenticated stage
126
+ state and canonical public version/integrity state; never repeat it blindly.
127
+ After all approvals:
128
+
129
+ 1. Verify the three live versions, integrities, and registry provenance
130
+ attestations, and exercise a fresh coupled install. Only then announce,
131
+ record, close, or synchronize the release.
132
+ 2. Create the three GitHub Releases in the same operator session, in shared,
133
+ server, then client order. For shared, pass the SHA-512 SRI computed from the
134
+ mandatory stage download during promotion:
135
+
136
+ ```sh
137
+ GITHUB_TOKEN="$(gh auth token)" node scripts/create-github-release.mjs <version> --integrity <sha512-SRI>
138
+ ```
139
+
140
+ This required value binds npm-live to the independently verified staged bytes.
141
+ Unlike server and client, shared uploads no same-run artifact report; do not
142
+ derive the expected integrity from npm or alter `publish.yml` to create one.
83
143
 
84
144
  ## Coupled Publication Window
85
145
 
86
- The shared, server, and client packages are published independently. The current
87
- release workflows publish directly to npm's default `latest` dist-tag, so the
88
- least-bad order is shared first, server second, and client third. This keeps each
89
- consumer pointed at an immutable shared artifact, but it cannot make the three
90
- `latest` pointers atomic.
146
+ The shared, server, and client packages are approved independently from private
147
+ npm stages. Pending stages are not publicly installable and do not change
148
+ `latest` or the public version list. Approval order is shared first, server
149
+ second, and client third. This keeps each consumer pointed at an immutable shared
150
+ artifact, but it cannot make the three approvals atomic.
91
151
 
92
152
  During that sequence, a user can install the newest server and newest client
93
153
  while they still carry different protocol tags. Their credential-free preflight
@@ -95,33 +155,47 @@ fails closed by design; this is a publication-window mismatch, not a negotiation
95
155
  or fallback case. The user remedy is to install the matching coupled shared,
96
156
  server, and client versions from the coordinated release rather than retrying
97
157
  `latest`. Do not describe this window as eliminated or promise an atomic
98
- multi-package publication until all three release workflows support that shape.
158
+ multi-package publication until npm supports atomic multi-package approval.
99
159
 
100
- The workflow publishes only `./release/<tarball>`. It never publishes from the
160
+ The workflow stages only `./release/<tarball>`. It never stages from the
101
161
  repository directory, a package name, a URL, a prior workflow artifact, or a
102
162
  locally rebuilt replacement.
103
163
 
104
164
  ## Stop And Recovery
105
165
 
106
- Stop before publication when source identity, tag ancestry, repository
166
+ Stop before staging when source identity, tag ancestry, repository
107
167
  visibility, environment protection, expected owner, target-version absence,
108
168
  tests, build output, tarball policy, clean-consumer imports, or Trusted Publishing
109
169
  configuration cannot be verified.
110
170
 
111
- If a first-attempt tag run fails before npm accepts the version, preserve the tag
171
+ Before any stage approval, approve none if any coupled stage or its evidence is
172
+ missing or wrong. If the coupled candidate is abandoned, reject all three stages
173
+ with 2FA. Treat every rejected or unusable tagged version as burned: never reuse,
174
+ move, or rerun its tag; prepare newly reviewed versions.
175
+
176
+ After shared approval but before server approval, stopping preserves live
177
+ client/server compatibility, though the shared version is immutable and any
178
+ replacement needs a new version. After server approval, prioritize the already
179
+ verified client approval because the public mismatch window is open. If that
180
+ client cannot be approved, preserve the failed evidence and prepare newly
181
+ reviewed matching server/client recovery versions; do not reject, rerun, or
182
+ silently substitute the tagged client.
183
+
184
+ If a first-attempt tag run fails before npm accepts the stage, preserve the tag
112
185
  and run as immutable evidence. Fix the source and begin a separately reviewed and
113
186
  authorized version/tag plan. Never move, reuse, rerun, or force-update the failed
114
187
  tag.
115
188
 
116
- Once npm accepts the version, the immutable release has occurred. Never rerun,
117
- republish, overwrite, unpublish, or silently substitute a replacement because a
118
- later registry read is delayed or unavailable.
189
+ Once npm accepts a stage, the tag run and candidate version are consumed under
190
+ this project's attempt-1 rule. Once the operator approves it, the immutable live
191
+ release has occurred. Never rerun, republish, overwrite, unpublish, or silently
192
+ substitute a replacement because a later registry read is delayed or unavailable.
119
193
 
120
194
  ## Failed-Superseded Recovery
121
195
 
122
196
  The tag-triggered workflow is single-job and first-attempt-only. If attempt 1
123
197
  fails before the tarball is built, the tarball is verified, the clean consumer
124
- is exercised, or `npm publish` runs, preserve the failed tag and run as immutable
198
+ is exercised, or `npm stage publish` runs, preserve the failed tag and run as immutable
125
199
  evidence. The failed version is not an install target and must not be rerun.
126
200
 
127
201
  Record the failure and prepare a newer version only from a clean tree:
@@ -135,7 +209,7 @@ npm run release:prepare -- <next-version> \
135
209
 
136
210
  The release identity verifier binds the record to the annotated tag, the exact
137
211
  workflow run and commit, the completed failed `publish` job, and the skipped
138
- tarball, clean-consumer, and publish steps. It independently checks the npm
212
+ tarball, clean-consumer, and staging steps. It independently checks the npm
139
213
  version list for registry absence. An attempt-2 run, a failure after packaging
140
214
  or publication, an artifact integrity value on a failed record, or a version
141
215
  present in npm is rejected. The generated record is `failed-superseded`; the
@@ -145,6 +219,13 @@ next release uses a new version and a new annotated tag.
145
219
 
146
220
  These records remain evidence, not reusable release inputs:
147
221
 
222
+ Eight published versions from 0.2.2 through 0.6.2, excluding the isolated 0.4.0
223
+ baseline, are the pre-convention boundary. Their workflow runs ended in failure
224
+ despite registry publication, which the canonical record schema cannot represent,
225
+ so the incident prose remains their record rather than force-fitting them into
226
+ `docs/release-records.json`. The 0.4.2 publication additionally came from a
227
+ `workflow_dispatch` run on `main`; 0.4.0 is also excluded from reconstruction.
228
+
148
229
  - `v0.2.0` run `29353763609` and `v0.2.1` run `29355823822` failed before
149
230
  artifact creation or publication.
150
231
  - Protected-main proof runs `29356980492` and `29357632667` diagnosed and then
@@ -0,0 +1,66 @@
1
+ # Built-in template lifecycle
2
+
3
+ Built-in templates are versioned package data. `src/templates.ts` is the source of truth for their names, presentation copy, cube directives, message taxonomies, roles, flags, short descriptions, and detailed playbooks. A running cube is not a writable upstream for that source.
4
+
5
+ ## Supported lifecycle
6
+
7
+ ### Edit one cube locally
8
+
9
+ A client with live manage authority for the cube may use the role and cube mutation operations to create or update roles, patch one plain-label role section, or update the cube directive or message taxonomy. Prefer a one-section role patch when one section changes; it preserves every other byte. Local edits affect only that cube. They neither mutate `src/templates.ts` nor create a built-in variant.
10
+
11
+ Read the canonical role or cube state before editing and read it back after the mutation. The server's canonical state is the result to verify. The package does not provide an automatic local-change-to-template audit ledger or promotion path.
12
+
13
+ ### Promote a trusted local improvement
14
+
15
+ Promotion is a normal reviewed repository change, not a runtime copy operation:
16
+
17
+ 1. Compare the local text with the current built-in source and identify the behavior and failure it addresses.
18
+ 2. Generalize only what applies to new cubes across supported implementations and hosts. Remove repository-host, operator-machine, project, and live-cube details.
19
+ 3. Edit `src/templates.ts` and the focused existing tests that pin the affected invariant. Preserve plain-label sections and the role-section round-trip contract.
20
+ 4. Review the complete source change through the repository's protected change mechanism. The change history and review are the built-in template's durable audit record.
21
+ 5. Publish the reviewed package through the normal immutable shared-package release workflow.
22
+ 6. Update client and server to the same exact `borgmcp-shared` version as a matched pair. The client update path rejects a published pair whose exact shared pins differ.
23
+
24
+ There is no direct promotion from server state, no package write from a running cube, and no unreviewed operator override of a built-in template.
25
+
26
+ ### Apply a released template
27
+
28
+ New cubes created through an updated client/server pair receive the template data shipped by that exact shared package. An existing non-empty cube remains locally owned: template application does not overwrite its directive (`resolveCubeDirectiveForApply` returns `null`). A release does not rewrite existing roles, directives, or taxonomies. An operator who wants a released improvement in an existing cube applies that specific local edit deliberately and verifies canonical readback.
29
+
30
+ ## Authority and audit
31
+
32
+ Cube-local authority and package authority are separate:
33
+
34
+ - A cube-local edit requires the cube's live manage authority and is verified against canonical server readback. It is not evidence that a built-in changed.
35
+ - A built-in edit requires repository change authority, review of the source diff, and the package release gates. Git history, review, immutable tag, and package artifact are its audit chain.
36
+ - A shared release alone is not delivery to users. Client and server must both pin the same exact shared version; neither a range nor one-sided pin movement is supported.
37
+ - Merge, tag, publication, and consumer pin changes remain separate authorized actions. Editing a local cube grants none of them.
38
+
39
+ ## Compatibility, versions, and migration
40
+
41
+ Built-in templates have no independent runtime version or negotiation protocol. Their version is the `borgmcp-shared` package version that contains them.
42
+
43
+ A role-text, directive, or taxonomy change ships in a new shared package version. Client and server consume the same exact version. If a change also alters a wire-visible template name or closed protocol acceptance set, follow the protocol change policy: update the exact protocol tag and implementation-neutral conformance vectors, then coordinate both consumers. Text-only playbook improvements do not invent a protocol migration.
44
+
45
+ Existing cubes are snapshots with local ownership, not replicas. They do not auto-migrate or auto-synchronize. This no-clobber rule avoids erasing operator edits and avoids making package upgrades mutate active coordination policy. Migration, when desired, is an explicit cube-local edit of named fields or sections.
46
+
47
+ ## Validation and conformance
48
+
49
+ Use the existing focused controls; do not add a second template lifecycle mechanism:
50
+
51
+ - `test/templates.test.ts` pins the built-in set, expected software-development roles and flags, host-neutral presentation copy, authority/scope language, role-size budgets, Builder minimum-sufficient-change ordering, taxonomy routing, generated/source equality, and no-clobber resolution.
52
+ - `test/role-section.test.ts` pins plain-label parsing, byte-identical parse/serialize for real template roles, and single-section replacement/insertion/deletion without clobbering unrelated text.
53
+ - `test/packed-artifact.test.ts` installs the packed package and verifies the public named-template creation contract and shipped role text from the consumer surface.
54
+ - `CUBE_TEMPLATE_ACCEPTANCE_CONFORMANCE` pins the closed accepted template-name set and invalid-name rejection. Stateful create vectors pin template identity to retry behavior.
55
+
56
+ Review remains the host-neutrality control: role text must avoid a specific repository host, CI vendor, operating system adapter, local path, or project-only workflow unless the template's documented scope requires it. This is a semantic review question, not a keyword validator. Validate both source and the built/packed consumer artifact because built-ins are delivered through the package.
57
+
58
+ ## Rejected alternatives
59
+
60
+ - Runtime-editable built-ins: rejected because package source and running server state would become competing authorities without one review or release history.
61
+ - Automatic promotion from a local cube: rejected because live text may contain project-specific policy, host assumptions, temporary experiments, or unreviewed mistakes.
62
+ - Automatic synchronization into existing cubes: rejected because it would clobber local policy and role edits. Existing cubes remain unchanged unless an operator applies a named edit.
63
+ - Separate template versions or compatibility negotiation: rejected because package identity and exact matched consumer pins already provide the delivery boundary; another version axis would add ambiguity without preserving any supported behavior.
64
+ - A new lifecycle validator or audit service: rejected because current behavioral tests, role-section round trips, protected review, and artifact gates already cover the enforceable invariants. Host-neutrality and promotion judgment remain review responsibilities.
65
+
66
+ Boundary: this document describes current mechanisms and the supported promotion path; it does not itself authorize a release, a local cube mutation, or a consumer pin change.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp-shared",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "description": "Shared protocol, domain, templates, and conformance contracts for Borg MCP implementations.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -64,7 +64,7 @@
64
64
  "verify:sbom": "node scripts/verify-release-sbom.mjs",
65
65
  "release:prepare": "node scripts/release-identity.mjs prepare",
66
66
  "verify:release-identity": "node scripts/release-identity.mjs verify",
67
- "prepack": "npm run check && npm test && npm run build"
67
+ "prepack": "npm run build && npm run check && npm test"
68
68
  },
69
69
  "keywords": [
70
70
  "borgmcp",
@@ -37,6 +37,7 @@ import {
37
37
  type DroneRuntimeMetadata,
38
38
  } from '../protocol/index.js';
39
39
  import {
40
+ APPEND_LOG_IDEMPOTENCY_CONFORMANCE,
40
41
  CREATE_CUBE_ASSOCIATION_CONFORMANCE,
41
42
  CREATE_CUBE_RETRY_CONFORMANCE,
42
43
  INVITATION_ARTIFACT_CONFORMANCE,
@@ -361,6 +362,7 @@ export const ADAPTER_CONFORMANCE_FIXTURES = [
361
362
  { id: 'security.adapter-boundary-injection', area: 'security' },
362
363
  { id: 'security.oversize-request', area: 'security' },
363
364
  { id: 'security.cross-cube-isolation', area: 'security' },
365
+ { id: 'log.append-idempotency', area: 'log' },
364
366
  { id: 'log.read-cursor-tuple', area: 'cursor' },
365
367
  { id: 'sse.replay-live-transition', area: 'sse' },
366
368
  { id: 'cursor.explicit-expiry', area: 'cursor' },
@@ -1323,7 +1325,10 @@ export async function runAdapterConformance(
1323
1325
  await record('security.adapter-boundary-injection', async () => {
1324
1326
  const injectedMessage = "'); DROP TABLE log_entries; --\r\ndata: forged-sse-frame";
1325
1327
  const injectedBody = JSON.stringify(
1326
- createProtocolEnvelope('inject-b1', { message: injectedMessage }),
1328
+ createProtocolEnvelope('inject-b1', {
1329
+ post_id: '00000000-0000-4000-8000-000000000301',
1330
+ message: injectedMessage,
1331
+ }),
1327
1332
  );
1328
1333
  invariant(
1329
1334
  utf8ByteLength(injectedBody) <= PROTOCOL_LIMIT_CEILINGS.max_request_bytes &&
@@ -1342,7 +1347,10 @@ export async function runAdapterConformance(
1342
1347
  const sentinel = await environment.operations.append(
1343
1348
  credentialB,
1344
1349
  cubeB,
1345
- createProtocolEnvelope('inject-b2', { message: 'post-injection-sentinel' }),
1350
+ createProtocolEnvelope('inject-b2', {
1351
+ post_id: '00000000-0000-4000-8000-000000000302',
1352
+ message: 'post-injection-sentinel',
1353
+ }),
1346
1354
  );
1347
1355
  expectStatus(sentinel, 201, 'Post-injection sentinel append');
1348
1356
  const read = await environment.operations.read(
@@ -1389,7 +1397,10 @@ export async function runAdapterConformance(
1389
1397
 
1390
1398
  await record('security.oversize-request', async () => {
1391
1399
  const baseBody = JSON.stringify(
1392
- createProtocolEnvelope('oversize-a1', { message: 'must-not-persist' }),
1400
+ createProtocolEnvelope('oversize-a1', {
1401
+ post_id: '00000000-0000-4000-8000-000000000303',
1402
+ message: 'must-not-persist',
1403
+ }),
1393
1404
  );
1394
1405
  const oversizedBody = baseBody + ' '.repeat(
1395
1406
  Math.max(0, PROTOCOL_LIMIT_CEILINGS.max_request_bytes - utf8ByteLength(baseBody) + 1),
@@ -1417,7 +1428,10 @@ export async function runAdapterConformance(
1417
1428
  const secretAppend = await environment.operations.append(
1418
1429
  credentialB,
1419
1430
  cubeB,
1420
- createProtocolEnvelope('append-b1', { message: 'principal-b-secret' }),
1431
+ createProtocolEnvelope('append-b1', {
1432
+ post_id: '00000000-0000-4000-8000-000000000304',
1433
+ message: 'principal-b-secret',
1434
+ }),
1421
1435
  );
1422
1436
  expectStatus(secretAppend, 201, 'Principal B append');
1423
1437
  const denied = await environment.operations.read(
@@ -1429,6 +1443,77 @@ export async function runAdapterConformance(
1429
1443
  return { status: 404, code: ErrorCode.NOT_FOUND };
1430
1444
  });
1431
1445
 
1446
+ await record('log.append-idempotency', async () => {
1447
+ const principal = await environment.admin.createPrincipal('append-idempotency');
1448
+ const otherPrincipal = await environment.admin.createPrincipal('append-idempotency-other');
1449
+ const cube = await environment.admin.createCube('append-idempotency');
1450
+ await environment.admin.grantCube(principal, cube);
1451
+ await environment.admin.grantCube(otherPrincipal, cube);
1452
+ const credential = await environment.admin.issueDroneSession(principal);
1453
+ const otherCredential = await environment.admin.issueDroneSession(otherPrincipal);
1454
+ const role = await environment.admin.createRole(cube, {
1455
+ roleClass: 'worker',
1456
+ isHumanSeat: false,
1457
+ });
1458
+ const firstRecipient = await environment.admin.createDrone(principal, cube, role);
1459
+ const secondRecipient = await environment.admin.createDrone(principal, cube, role);
1460
+ const postId = '00000000-0000-4000-8000-000000000313';
1461
+ const initialPayload = {
1462
+ post_id: postId,
1463
+ message: 'once',
1464
+ visibility: 'direct' as const,
1465
+ recipientDroneIds: [firstRecipient.id],
1466
+ class: 'review',
1467
+ to: ['Coordinator'],
1468
+ };
1469
+ const first = await environment.operations.append(
1470
+ credential,
1471
+ cube,
1472
+ createProtocolEnvelope('append-idempotency-first', initialPayload),
1473
+ );
1474
+ expectStatus(first, 201, 'Initial idempotent append');
1475
+ const firstResult = decodeAppendLogResultEnvelope(first.body).payload;
1476
+ invariant(!firstResult.deduplicated, 'Initial append was reported as deduplicated.');
1477
+ for (const [index, vector] of APPEND_LOG_IDEMPOTENCY_CONFORMANCE.entries()) {
1478
+ const payload = {
1479
+ ...initialPayload,
1480
+ ...(vector.mutation === 'message' ? { message: 'changed' } : {}),
1481
+ ...(vector.mutation === 'visibility' ? { visibility: 'broadcast' as const } : {}),
1482
+ ...(vector.mutation === 'recipient_set' ? { recipientDroneIds: [secondRecipient.id] } : {}),
1483
+ ...(vector.mutation === 'resolved_class_routing' ? { class: 'security', to: ['Security Auditor'] } : {}),
1484
+ };
1485
+ const response = await environment.operations.append(
1486
+ vector.actor === 'same' ? credential : otherCredential,
1487
+ cube,
1488
+ createProtocolEnvelope(`append-idempotency-${index}`, payload),
1489
+ );
1490
+ if (vector.expected === 'POST_ID_CONFLICT') {
1491
+ expectError(response, 409, ErrorCode.POST_ID_CONFLICT, vector.name);
1492
+ continue;
1493
+ }
1494
+ expectStatus(response, 201, vector.name);
1495
+ const result = decodeAppendLogResultEnvelope(response.body).payload;
1496
+ invariant(result.deduplicated === (vector.expected === 'deduplicated'), `${vector.name} returned the wrong deduplicated flag.`);
1497
+ invariant(
1498
+ vector.expected === 'deduplicated'
1499
+ ? result.entry.id === firstResult.entry.id
1500
+ : result.entry.id !== firstResult.entry.id,
1501
+ `${vector.name} returned the wrong entry identity.`,
1502
+ );
1503
+ }
1504
+ const read = await environment.operations.read(
1505
+ credential,
1506
+ cube,
1507
+ createProtocolEnvelope('append-idempotency-read', { cursor: null, limit: 10 }),
1508
+ );
1509
+ expectStatus(read, 200, 'Idempotent append read');
1510
+ invariant(
1511
+ decodeReadLogResultEnvelope(read.body).payload.entries.length === 2,
1512
+ 'Append collision controls persisted the wrong number of entries.',
1513
+ );
1514
+ return { persisted_entries: 2, stable_entry: true, deduplicated: true, conflicts: 4, cross_author_created: true };
1515
+ });
1516
+
1432
1517
  const entries: Array<{ id: string; created_at: string; message: string }> = [];
1433
1518
  let readCursor: LogCursor | null = null;
1434
1519
  await record('log.read-cursor-tuple', async () => {
@@ -1436,7 +1521,10 @@ export async function runAdapterConformance(
1436
1521
  const response = await environment.operations.append(
1437
1522
  credentialA,
1438
1523
  cubeA,
1439
- createProtocolEnvelope(`append-a${index + 1}`, { message }),
1524
+ createProtocolEnvelope(`append-a${index + 1}`, {
1525
+ post_id: `00000000-0000-4000-8000-${String(305 + index).padStart(12, '0')}`,
1526
+ message,
1527
+ }),
1440
1528
  );
1441
1529
  expectStatus(response, 201, `Append ${message}`);
1442
1530
  const entry = decodeAppendLogResultEnvelope(response.body).payload.entry;
@@ -1470,7 +1558,10 @@ export async function runAdapterConformance(
1470
1558
  const appendDelta = environment.operations.append(
1471
1559
  credentialA,
1472
1560
  cubeA,
1473
- createProtocolEnvelope('append-a4', { message: 'delta' }),
1561
+ createProtocolEnvelope('append-a4', {
1562
+ post_id: '00000000-0000-4000-8000-000000000308',
1563
+ message: 'delta',
1564
+ }),
1474
1565
  );
1475
1566
  expectStatus(await appendDelta, 201, 'Transition append');
1476
1567
  } finally {
@@ -1494,7 +1585,10 @@ export async function runAdapterConformance(
1494
1585
  const epsilonResponse = await environment.operations.append(
1495
1586
  credentialA,
1496
1587
  cubeA,
1497
- createProtocolEnvelope('append-a5', { message: 'epsilon' }),
1588
+ createProtocolEnvelope('append-a5', {
1589
+ post_id: '00000000-0000-4000-8000-000000000309',
1590
+ message: 'epsilon',
1591
+ }),
1498
1592
  );
1499
1593
  expectStatus(epsilonResponse, 201, 'Live append');
1500
1594
  const epsilon = logEvent(await within(noDuplicate, 'Live epsilon event', streamDeadlineMs), 'Live append');
@@ -2014,6 +2108,7 @@ export async function runAdapterConformance(
2014
2108
  cubeA,
2015
2109
  createProtocolEnvelope('evict-direct-target', {
2016
2110
  message: 'must-not-fan-out',
2111
+ post_id: '00000000-0000-4000-8000-000000000310',
2017
2112
  visibility: 'direct',
2018
2113
  recipientDroneIds: [evictedDrone.id],
2019
2114
  }),
@@ -2203,7 +2298,10 @@ export async function runAdapterConformance(
2203
2298
  const attributed = await environment.operations.append(
2204
2299
  evictedSession,
2205
2300
  roleContractCube,
2206
- createProtocolEnvelope('role-delete-attribution', { message: 'attribution survives' }),
2301
+ createProtocolEnvelope('role-delete-attribution', {
2302
+ post_id: '00000000-0000-4000-8000-000000000311',
2303
+ message: 'attribution survives',
2304
+ }),
2207
2305
  );
2208
2306
  expectStatus(attributed, 201, 'Pre-delete attributed log');
2209
2307
  const attributedEntry = decodeAppendLogResultEnvelope(attributed.body).payload.entry;
@@ -2867,7 +2965,10 @@ export async function runAdapterConformance(
2867
2965
  const append = await environment.operations.append(
2868
2966
  creator.credential,
2869
2967
  cube,
2870
- createProtocolEnvelope('delete-log', { message: 'deleted with cube' }),
2968
+ createProtocolEnvelope('delete-log', {
2969
+ post_id: '00000000-0000-4000-8000-000000000312',
2970
+ message: 'deleted with cube',
2971
+ }),
2871
2972
  );
2872
2973
  expectStatus(append, 201, 'Deletion fixture log');
2873
2974
  const entry = decodeAppendLogResultEnvelope(append.body).payload.entry;