borgmcp 3.11.1 → 3.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.
- package/THIRD_PARTY_NOTICES.md +5 -0
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +11 -0
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/codex-app-wake.d.ts +3 -2
- package/dist/codex-app-wake.d.ts.map +1 -1
- package/dist/codex-app-wake.js +42 -9
- package/dist/codex-app-wake.js.map +1 -1
- package/dist/cube-activity-wake-copy.d.ts +1 -1
- package/dist/cube-activity-wake-copy.d.ts.map +1 -1
- package/dist/cube-activity-wake-copy.js +1 -1
- package/dist/cube-activity-wake-copy.js.map +1 -1
- package/dist/log-stream.d.ts +3 -3
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +8 -13
- package/dist/log-stream.js.map +1 -1
- package/dist/opencode-drone.d.ts +1 -1
- package/dist/opencode-drone.d.ts.map +1 -1
- package/dist/opencode-drone.js +16 -2
- package/dist/opencode-drone.js.map +1 -1
- package/dist/remote-client.d.ts +2 -21
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +8 -6
- package/dist/remote-client.js.map +1 -1
- package/dist/server-handshake.d.ts +6 -1
- package/dist/server-handshake.d.ts.map +1 -1
- package/dist/server-handshake.js +1 -0
- package/dist/server-handshake.js.map +1 -1
- package/docs/LOCAL_SERVER.md +4 -8
- package/docs/RELEASING.md +32 -308
- package/package.json +3 -2
- package/src/assimilate-deps.ts +11 -0
- package/src/codex-app-wake.ts +42 -8
- package/src/cube-activity-wake-copy.ts +1 -1
- package/src/log-stream.ts +24 -15
- package/src/opencode-drone.ts +14 -1
- package/src/remote-client.ts +11 -25
- package/src/server-handshake.ts +4 -1
package/docs/RELEASING.md
CHANGED
|
@@ -8,8 +8,9 @@ npm Trusted Publishing; no long-lived npm token is stored or exposed.
|
|
|
8
8
|
## Release Integrity
|
|
9
9
|
|
|
10
10
|
Release provenance is established by protected annotated tags, GitHub build
|
|
11
|
-
provenance, and npm Trusted Publishing.
|
|
12
|
-
|
|
11
|
+
provenance, and npm Trusted Publishing. Never move, replace, or reuse a tag.
|
|
12
|
+
A failed workflow may be rerun until npm accepts a stage; stage acceptance
|
|
13
|
+
consumes the version.
|
|
13
14
|
|
|
14
15
|
## Release Prerequisites
|
|
15
16
|
|
|
@@ -20,19 +21,15 @@ Before creating the release tag, independently verify all of these conditions:
|
|
|
20
21
|
- the extraction review confirms no private backend secrets, deployment
|
|
21
22
|
configuration, customer data, local state, or duplicated shared contracts
|
|
22
23
|
entered the public package;
|
|
23
|
-
- the exact audited registry dependency `borgmcp-shared@0.
|
|
24
|
+
- the exact audited registry dependency `borgmcp-shared@0.12.2` remains locked to
|
|
24
25
|
its canonical tarball and integrity
|
|
25
|
-
`sha512-
|
|
26
|
-
- the coupled shared/server/client candidates
|
|
27
|
-
|
|
28
|
-
before any stage is approved;
|
|
26
|
+
`sha512-l459XEeqk0cSz1+Z8yk8cCVWik4/CX4OBTRZqj6n1SZYvDpzWJksUz82FA9k4taf//rs43Tfl1tpWXnRHAqxOQ==`;
|
|
27
|
+
- the coupled shared/server/client candidates use the same exact
|
|
28
|
+
`borgmcp-shared` version;
|
|
29
29
|
- the selected stable client version is unused and the exact release commit is
|
|
30
30
|
on protected `main`;
|
|
31
31
|
- the repository and protected npm environment settings pass an operator audit;
|
|
32
|
-
- the exact release source passes
|
|
33
|
-
extraction, and package gates; and
|
|
34
|
-
- the immutable annotated tag and publication receive explicit release
|
|
35
|
-
authorization.
|
|
32
|
+
- the exact release source passes exact-SHA CI and one Code Review.
|
|
36
33
|
|
|
37
34
|
`scripts/verify-release-readiness.mjs` makes the source-side blockers
|
|
38
35
|
machine-checkable. A release tag created before they are resolved fails before
|
|
@@ -41,18 +38,19 @@ dependency installation or publication.
|
|
|
41
38
|
### Release branches
|
|
42
39
|
|
|
43
40
|
Release branches use the `release/` prefix and enter protected `main` through a
|
|
44
|
-
pull request.
|
|
45
|
-
|
|
41
|
+
pull request. After exact-SHA CI and one Code Review, merge the preparation and
|
|
42
|
+
create its annotated version tag. The tag automatically verifies and stages the
|
|
43
|
+
package.
|
|
46
44
|
|
|
47
45
|
## Repository Controls
|
|
48
46
|
|
|
49
47
|
Repository settings are operator-owned and are not changed by this workflow.
|
|
50
48
|
Before preparing a candidate, independently verify:
|
|
51
49
|
|
|
52
|
-
1. The `npm-publish` environment disables administrator bypass,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
1. The `npm-publish` environment disables administrator bypass, has no required
|
|
51
|
+
reviewer, and allows only `v*.*.*` tags. Its `NPM_EXPECTED_OWNER` variable
|
|
52
|
+
must match the sole reviewed maintainer of the existing `borgmcp` package.
|
|
53
|
+
It must contain no npm token.
|
|
56
54
|
2. npm Trusted Publishing is configured for organization `Byte-Ventures`,
|
|
57
55
|
repository `borg-mcp-client`, workflow `publish.yml`, and environment
|
|
58
56
|
`npm-publish`. Its allowed actions enable `npm stage publish` and disable
|
|
@@ -69,212 +67,8 @@ Before preparing a candidate, independently verify:
|
|
|
69
67
|
|
|
70
68
|
## Release Workflow
|
|
71
69
|
|
|
72
|
-
### Clean-environment rig lifecycle
|
|
73
|
-
|
|
74
|
-
Every clean-environment verification rig has an explicit identity and an
|
|
75
|
-
explicit end-of-life. Run it from a Borg-launched session and keep filesystem
|
|
76
|
-
workspaces under the exact disposable scratch root Borg exports. Give each
|
|
77
|
-
workspace a name beginning with `borg-rig-` followed by its owner, purpose, and
|
|
78
|
-
a unique suffix. Before the first command that can create files, anchor the rig
|
|
79
|
-
as its own npm project:
|
|
80
|
-
|
|
81
|
-
```sh
|
|
82
|
-
BORG_SCRATCH_ROOT="${BORG_LAUNCH_SCRATCH:?run from a Borg-launched session}"
|
|
83
|
-
RIG_OWNER="$(basename "$BORG_SCRATCH_ROOT")"
|
|
84
|
-
RIG_NONCE="${RIG_NONCE:-$(date +%Y%m%d%H%M%S)-$$}"
|
|
85
|
-
RIG_ID="borg-rig-${RIG_OWNER}-release-${RIG_NONCE}"
|
|
86
|
-
RIG_ROOT="$BORG_SCRATCH_ROOT/$RIG_ID"
|
|
87
|
-
mkdir -p "$RIG_ROOT"
|
|
88
|
-
printf '%s\n' '{"private":true}' > "$RIG_ROOT/package.json"
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
The manifest anchor is required before any `npm install`, `npm update`, or
|
|
92
|
-
other npm command. Without it, npm can walk up from an empty scratch directory
|
|
93
|
-
and write the operator's `package.json` instead of creating project-local
|
|
94
|
-
state. In a source checkout, the committed `release:exercise` follows this rule
|
|
95
|
-
for its temporary consumer; other QA scripts and manual rigs must do the same.
|
|
96
|
-
|
|
97
|
-
The system temporary root is shared with the whole machine, so an unbounded
|
|
98
|
-
listing there is not an inspectable cleanup check. Bound that leg by the
|
|
99
|
-
invoking user and the run-window markers, which select directories whose
|
|
100
|
-
timestamps fall within this run window without requiring a prefix inventory.
|
|
101
|
-
New `mkdtemp` prefixes therefore require no documentation change. Keep both
|
|
102
|
-
time predicates and the ownership predicate: removing the run-window bound
|
|
103
|
-
turns a populated shared temp root back into an uninspectable listing.
|
|
104
|
-
|
|
105
|
-
On a machine where several runs share one user account, this listing can
|
|
106
|
-
include another run's live workspace. Removal is scoped to paths created by
|
|
107
|
-
this run; anything else in the output is reported, not deleted. Unrelated
|
|
108
|
-
same-user temporary directories created during the window can therefore
|
|
109
|
-
appear in the result, so never remove a path merely because this listing found
|
|
110
|
-
it.
|
|
111
|
-
|
|
112
|
-
```sh
|
|
113
|
-
TEMP_ROOT="${TMPDIR:-/tmp}"
|
|
114
|
-
TEMP_OWNER="$(id -un)"
|
|
115
|
-
TEMP_SCAN_START="$TEMP_ROOT/.${RIG_ID}-temp-scan-start"
|
|
116
|
-
TEMP_SCAN_END="$TEMP_ROOT/.${RIG_ID}-temp-scan-end"
|
|
117
|
-
|
|
118
|
-
list_recent_owned_temp_rigs() {
|
|
119
|
-
find "$TEMP_ROOT" -mindepth 1 -prune \
|
|
120
|
-
-type d \
|
|
121
|
-
-user "$TEMP_OWNER" \
|
|
122
|
-
-newer "$TEMP_SCAN_START" \
|
|
123
|
-
! -newer "$TEMP_SCAN_END" \
|
|
124
|
-
-print
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
touch "$TEMP_SCAN_START"
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
Container-backed rigs use the same `RIG_ID` as the container name and carry
|
|
131
|
-
both labels below. Register exact-target cleanup before launching the rig:
|
|
132
|
-
|
|
133
|
-
```sh
|
|
134
|
-
list_owned_rig_containers() {
|
|
135
|
-
docker container ls --all \
|
|
136
|
-
--filter label=borg-rig=1 \
|
|
137
|
-
--filter "label=borg-rig-owner=${RIG_OWNER:?}" \
|
|
138
|
-
--format '{{.ID}}\t{{.Names}}\t{{.Status}}'
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
cleanup_done=0
|
|
142
|
-
cleanup() {
|
|
143
|
-
[ "$cleanup_done" -eq 0 ] || return
|
|
144
|
-
cleanup_done=1
|
|
145
|
-
trap - EXIT HUP INT TERM
|
|
146
|
-
docker container rm --force "$RIG_ID" >/dev/null 2>&1 || true
|
|
147
|
-
rm -rf -- "$RIG_ROOT"
|
|
148
|
-
list_owned_rig_containers
|
|
149
|
-
find "$BORG_SCRATCH_ROOT" -mindepth 1 -print
|
|
150
|
-
find "$BORG_SCRATCH_ROOT" -name 'borg-rig-*' -print
|
|
151
|
-
if [ -e "$TEMP_SCAN_START" ]; then
|
|
152
|
-
touch "$TEMP_SCAN_END"
|
|
153
|
-
list_recent_owned_temp_rigs
|
|
154
|
-
fi
|
|
155
|
-
rm -f -- "$TEMP_SCAN_START" "$TEMP_SCAN_END"
|
|
156
|
-
}
|
|
157
|
-
trap cleanup EXIT
|
|
158
|
-
trap 'exit 129' HUP
|
|
159
|
-
trap 'exit 130' INT
|
|
160
|
-
trap 'exit 143' TERM
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
List containers and scratch paths before starting. The filters distinguish this
|
|
164
|
-
session's rigs from unrelated containers on a shared host:
|
|
165
|
-
|
|
166
|
-
```sh
|
|
167
|
-
list_owned_rig_containers
|
|
168
|
-
find "$BORG_SCRATCH_ROOT" -mindepth 1 -print
|
|
169
|
-
find "$BORG_SCRATCH_ROOT" -name 'borg-rig-*' -print
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
Only after the cleanup traps and pre-launch listings are in place, launch a
|
|
173
|
-
container-backed rig. `--rm` is preferred; a runtime without automatic removal
|
|
174
|
-
must remove the exact `RIG_ID` in its cleanup path and retain the labels for a
|
|
175
|
-
bounded sweep:
|
|
176
|
-
|
|
177
|
-
```sh
|
|
178
|
-
RIG_IMAGE="${RIG_IMAGE:?set the rig image}"
|
|
179
|
-
docker run --rm \
|
|
180
|
-
--name "$RIG_ID" \
|
|
181
|
-
--label borg-rig=1 \
|
|
182
|
-
--label "borg-rig-owner=$RIG_OWNER" \
|
|
183
|
-
"$RIG_IMAGE"
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
The unfiltered filesystem listings expose legacy names as well as conforming
|
|
187
|
-
rigs. The bounded temporary-directory function reports same-user directories
|
|
188
|
-
created during the run window, including unrelated work; it does not establish
|
|
189
|
-
which process owns them. The cleanup path runs the container and scratch
|
|
190
|
-
listings after removing this rig, then runs the bounded temporary-directory
|
|
191
|
-
listing between the start and end markers. Inspect and report its results. If a
|
|
192
|
-
rig creates an exact system-temporary path, record that path when it is created
|
|
193
|
-
and remove that exact path during cleanup; never remove a path solely because
|
|
194
|
-
the bounded listing found it.
|
|
195
|
-
|
|
196
|
-
Register cleanup before launching any process, run it on success and failure,
|
|
197
|
-
and do not deliver a verdict until the container listing and both scratch
|
|
198
|
-
listings show no rig owned by this run, every exact system-temporary path
|
|
199
|
-
recorded by the rig has been removed, and the bounded temporary-directory
|
|
200
|
-
listing has been inspected and reported. For a non-`--rm` container runtime,
|
|
201
|
-
remove only the named rig or the same owner label; never prune unrelated
|
|
202
|
-
containers. A completed verification
|
|
203
|
-
therefore implies zero running or stopped rig containers and no rig workspace
|
|
204
|
-
created by this run left in either the session scratch root or any exact
|
|
205
|
-
system-temporary path the rig recorded.
|
|
206
|
-
|
|
207
|
-
### Pre-tag composed exercise
|
|
208
|
-
|
|
209
|
-
Before creating a release tag, exercise the packed client against the selected
|
|
210
|
-
server artifact:
|
|
211
|
-
|
|
212
|
-
```sh
|
|
213
|
-
npm run release:exercise -- \
|
|
214
|
-
--server /absolute/path/to/borgmcp-server.tgz \
|
|
215
|
-
--server-integrity 'sha512-...'
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
The server integrity must come from the reviewed producer of that artifact. Run
|
|
219
|
-
the command once with the published counterpart. When the client and server are
|
|
220
|
-
being released together, run it again with the co-releasing server candidate.
|
|
221
|
-
The default path builds and packs the current client; `--client-tarball` exists
|
|
222
|
-
only for reproducing a reviewed packed client or running a negative control.
|
|
223
|
-
|
|
224
|
-
This harness exercises reviewed release candidates and their published
|
|
225
|
-
counterparts. It is not a containment sandbox for hostile or otherwise untrusted
|
|
226
|
-
packages and must not be used as one. The temporary install and data directories
|
|
227
|
-
isolate the reviewed exercise from normal product state; they are not an
|
|
228
|
-
operating-system security boundary.
|
|
229
|
-
|
|
230
|
-
The harness requires Node.js 22, npm 11.18.0, Python 3, and macOS or Linux. It
|
|
231
|
-
installs both tarballs into a private temporary project, bootstraps isolated
|
|
232
|
-
server data, and starts the installed server directly. It never uses
|
|
233
|
-
`borg server setup`, because registry-backed activation would select the
|
|
234
|
-
previously published server instead of a candidate.
|
|
235
|
-
|
|
236
|
-
Both composed terminal journeys run under a real PTY:
|
|
237
|
-
|
|
238
|
-
- `borg server dashboard` must render the attached-viewer footer, exit cleanly
|
|
239
|
-
on Ctrl-C, restore the cursor and alternate screen, and leave the server's
|
|
240
|
-
pinned-TLS process healthy at `GET /healthz` (204, empty body).
|
|
241
|
-
- `borg server start` must render the foreground-server footer, exit cleanly on
|
|
242
|
-
Ctrl-C, restore the terminal, stop the server, and release its health endpoint.
|
|
243
|
-
|
|
244
|
-
The client facade resolves `borg-mcp-server` by bare name. The harness therefore
|
|
245
|
-
uses a controlled shim only to arrange resolution, then verifies the outcome:
|
|
246
|
-
before trusting a frame, it reads the live process command and requires the
|
|
247
|
-
absolute installed server entry. Every client or server executable must resolve
|
|
248
|
-
inside the installed package root covered by the SRI cited for that role. Its
|
|
249
|
-
JSON report records the absolute path, version, and independently supplied
|
|
250
|
-
integrity for the client, dashboard listener, dashboard viewer, and foreground
|
|
251
|
-
listener/viewer roles. PATH ordering alone is never accepted as identity evidence.
|
|
252
|
-
|
|
253
|
-
The harness is a fail-closed release gate. A missing frame, non-PTY execution,
|
|
254
|
-
wrong journey footer, substituted artifact, nonzero exit, absent terminal
|
|
255
|
-
restore sequence, failed post-exit health assertion, timeout, or oversized
|
|
256
|
-
transcript fails the command. Its controls must remain demonstrably bidirectional:
|
|
257
|
-
the current client/server pairing passes; and a deliberately wrong server
|
|
258
|
-
integrity fails before either journey starts.
|
|
259
|
-
|
|
260
|
-
The terminal-restore negative control is specifically the packed pre-#146 client
|
|
261
|
-
from commit `81da7b970ffb4e76a35c7bc551c419fec702a3b6` composed with published
|
|
262
|
-
`borgmcp-server@0.2.0`, registry integrity
|
|
263
|
-
`sha512-squb0+vdy0q7l/4FeV7OTvSm7OiFWGsAjGhcVEXYrQc9K/8jJYduqPS90VBwaJRT6z221Gxf3xQ5SeZe/Qoncw==`.
|
|
264
|
-
That composition must fail on the missing cursor-restore sequence. A newer server
|
|
265
|
-
candidate can mask the old client's defect and is not a valid counterpart for
|
|
266
|
-
this negative control.
|
|
267
|
-
|
|
268
|
-
```sh
|
|
269
|
-
npm run release:exercise -- \
|
|
270
|
-
--server borgmcp-server@0.2.0 \
|
|
271
|
-
--server-integrity 'sha512-squb0+vdy0q7l/4FeV7OTvSm7OiFWGsAjGhcVEXYrQc9K/8jJYduqPS90VBwaJRT6z221Gxf3xQ5SeZe/Qoncw==' \
|
|
272
|
-
--client-tarball /absolute/path/to/pre-146-borgmcp-2.1.1.tgz
|
|
273
|
-
```
|
|
274
|
-
|
|
275
70
|
The only trigger is a protected annotated `v<package version>` tag. Manual
|
|
276
|
-
dispatch is intentionally absent
|
|
277
|
-
existing tag. The workflow rejects reruns, root `.npmrc`
|
|
71
|
+
dispatch is intentionally absent. The workflow rejects root `.npmrc`
|
|
278
72
|
configuration, non-tag events, lightweight or malformed tags, version mismatch,
|
|
279
73
|
source/tag mismatch, and tags whose commits are not on protected `main`.
|
|
280
74
|
|
|
@@ -283,7 +77,8 @@ The unprivileged `verify` job performs one sequence:
|
|
|
283
77
|
1. Verify the public-source boundary, extraction readiness, exact shared-package
|
|
284
78
|
pin, and canonical registry lock metadata.
|
|
285
79
|
2. Install the lockfile once with lifecycle scripts disabled and audit it.
|
|
286
|
-
3. Run
|
|
80
|
+
3. Run one readable build and reject generated `dist` drift. Tests already ran
|
|
81
|
+
in CI on the exact source.
|
|
287
82
|
4. Produce one npm tarball.
|
|
288
83
|
5. Verify that tarball once for package identity, license/notice, source and map
|
|
289
84
|
completeness, executable bins, archive safety, dependency integrity, and
|
|
@@ -294,14 +89,6 @@ The unprivileged `verify` job performs one sequence:
|
|
|
294
89
|
7. Upload only the tarball and its verifier-generated report as the same-run
|
|
295
90
|
release artifact.
|
|
296
91
|
|
|
297
|
-
After `verify` succeeds, the designated Queen operator alone approves the
|
|
298
|
-
`npm-publish` environment. There is no separate pre-stage exact-artifact
|
|
299
|
-
Security gate: the verify job is the mechanical authority for the exact bytes
|
|
300
|
-
that the stage job consumes. Environment approval authorizes submitting those
|
|
301
|
-
bytes to npm's private staged-publishing service; it does not make the version
|
|
302
|
-
public, permit a rerun or rebuilt artifact, or authorize approval by another
|
|
303
|
-
actor.
|
|
304
|
-
|
|
305
92
|
The protected `publish` job alone receives `id-token: write`. It downloads the
|
|
306
93
|
same-run artifact and rejects a report whose package name or version differs
|
|
307
94
|
from the release, a version that already exists, an unclaimed package, or an
|
|
@@ -313,48 +100,15 @@ package.
|
|
|
313
100
|
|
|
314
101
|
Successful completion of the workflow means npm accepted the immutable staged
|
|
315
102
|
tarball. It does not mean the version is public. Stage acceptance consumes the
|
|
316
|
-
|
|
103
|
+
version, but must not trigger
|
|
317
104
|
release announcements, issue closure, consumer pins, site synchronization, or
|
|
318
105
|
claims that the version was published. There is no workflow registry readback:
|
|
319
106
|
stage inspection and approval require an interactive npm identity and cannot use
|
|
320
107
|
the workflow's OIDC credential.
|
|
321
108
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
`borgmcp-shared`, `borgmcp-server`, and `borgmcp`. Use authenticated
|
|
326
|
-
`npm stage list` and `npm stage view <UUID>` to record and inspect all three stage
|
|
327
|
-
UUIDs and verify each package, version, and eventual `latest` tag. For every
|
|
328
|
-
package, `npm stage download <UUID>` is mandatory; compute its SHA-512 SRI and
|
|
329
|
-
require an exact match with that workflow run's same-run artifact report. Bind
|
|
330
|
-
the source annotated tag object, commit, and workflow run separately from GitHub
|
|
331
|
-
evidence, because npm stage inspection does not expose them. Exercise the exact
|
|
332
|
-
downloaded coupled set before approval. Confirm that public `latest`, public
|
|
333
|
-
`versions`, and client update resolution still expose the prior coherent release
|
|
334
|
-
set. If a pending stage appears on any public surface, approve nothing and halt
|
|
335
|
-
this mechanism before consumers can observe it.
|
|
336
|
-
|
|
337
|
-
Approve the verified UUIDs in one operator session, using interactive 2FA, in
|
|
338
|
-
this exact order:
|
|
339
|
-
|
|
340
|
-
1. `npm stage approve <shared-stage-uuid>`
|
|
341
|
-
2. `npm stage approve <server-stage-uuid>`
|
|
342
|
-
3. `npm stage approve <client-stage-uuid>`
|
|
343
|
-
|
|
344
|
-
The approvals are not atomic. Shared approval leaves existing client and server
|
|
345
|
-
pins coherent. Server approval opens a bounded mismatch window between public
|
|
346
|
-
server `latest` and client `latest`; client approval closes it. After an approval
|
|
347
|
-
returns successfully, continue only while the next stage remains valid. For an
|
|
348
|
-
ambiguous result, inspect authenticated stage state and canonical public package
|
|
349
|
-
version/integrity before acting; never repeat an approval blindly.
|
|
350
|
-
|
|
351
|
-
The terminal release boundary is successful interactive stage approval followed
|
|
352
|
-
by canonical registry visibility, integrity verification, and registry
|
|
353
|
-
provenance-attestation inspection. Stage inspection cannot establish provenance
|
|
354
|
-
before approval; Trusted Publishing carries it to the live package, where the
|
|
355
|
-
registry attestation is authoritative. Only then may the release be announced,
|
|
356
|
-
issues closed, consumer pins or site data synchronized, or the version described
|
|
357
|
-
as published.
|
|
109
|
+
Inspect the npm stage, then approve it interactively. This stage approval is the
|
|
110
|
+
sole human publication boundary. Confirm canonical live package visibility and
|
|
111
|
+
integrity before announcing the release or updating dependent packages.
|
|
358
112
|
|
|
359
113
|
After all three packages cross that boundary, create their GitHub Releases in
|
|
360
114
|
the same shared → server → client operator session. In each package repository,
|
|
@@ -364,46 +118,16 @@ run:
|
|
|
364
118
|
GITHUB_TOKEN="$(gh auth token)" node scripts/create-github-release.mjs <version>
|
|
365
119
|
```
|
|
366
120
|
|
|
367
|
-
The script binds the annotated tag,
|
|
368
|
-
|
|
369
|
-
the
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
it into an isolated prefix and exercise the real user update path end to end.
|
|
373
|
-
This is product verification, not candidate validation: failure routes a new
|
|
374
|
-
reviewed fix and never invalidates, rebuilds, retags, or reruns the immutable
|
|
375
|
-
release. Do not repeat byte comparisons, packed-version checks, source-tree
|
|
376
|
-
verification, dist-tag readback, or provenance reconstruction that the
|
|
377
|
-
exact-artifact `verify` and stage jobs already completed. The canonical
|
|
378
|
-
live-version integrity check is required because it establishes the approval
|
|
379
|
-
boundary; it is not a duplicate candidate verification.
|
|
380
|
-
|
|
381
|
-
No separate checksum file is needed: the tarball verifier records canonical
|
|
382
|
-
SHA-512 SRI in the artifact report. GitHub's same-run artifact transport and the
|
|
383
|
-
report bind the reviewed candidate without repeated SHA512 choreography.
|
|
384
|
-
|
|
385
|
-
Rely on npm Trusted Publishing. Perform the one authoritative registry
|
|
386
|
-
provenance-attestation inspection at the approved-live boundary; do not
|
|
387
|
-
reconstruct DSSE, in-toto, SLSA, workflow-ref, or builder statements locally or
|
|
388
|
-
add redundant provenance readback.
|
|
389
|
-
Do not add cross-run tuple variables, cross-run artifact selection, duplicate
|
|
390
|
-
builds, duplicate package verification, checksum bundles, or SBOM ceremony.
|
|
121
|
+
The script binds the annotated tag, live npm package and integrity, and Release
|
|
122
|
+
absence before creating the Release. It reads
|
|
123
|
+
the curated `docs/releases/<version>.md` notes from the exact tagged commit,
|
|
124
|
+
renders them under `News and fixes`, and refuses missing or blank notes and an
|
|
125
|
+
existing Release.
|
|
391
126
|
|
|
392
127
|
## Stop And Recovery Conditions
|
|
393
128
|
|
|
394
|
-
Stop when source, settings, ownership, tag, artifact, test, audit, review
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
Before any approval, missing or inconsistent stage evidence means approve none.
|
|
401
|
-
Reject all three stages only when abandoning the coupled candidate; Borg treats
|
|
402
|
-
every rejected or unusable tagged stage as burned, even though npm permits some
|
|
403
|
-
re-staging. Replacement versions require fresh review, tags, and stage runs.
|
|
404
|
-
After shared approval but before server approval, stopping is compatibility-safe
|
|
405
|
-
for the existing client/server pair, but shared is immutable and any repair uses
|
|
406
|
-
fresh versions. After server approval, prioritize the already-verified client
|
|
407
|
-
approval because the public pair is mismatched. If that client stage cannot be
|
|
408
|
-
approved, do not reject it or rerun the tag; prepare newly reviewed matching
|
|
409
|
-
server/client recovery versions and state the live mismatch window explicitly.
|
|
129
|
+
Stop when source, settings, ownership, tag, artifact, test, audit, or review
|
|
130
|
+
evidence is missing or inconsistent. Never move or reuse a tag,
|
|
131
|
+
overwrite an npm version, unpublish to hide a failure, or substitute a local
|
|
132
|
+
rebuild. A pre-stage workflow failure may be rerun against the same immutable
|
|
133
|
+
tag. After npm accepts a stage, recovery uses a fresh reviewed version.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "borgmcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.12.0",
|
|
4
4
|
"description": "Coordinate AI coding agents in shared cubes. Works with Claude Code, Codex, and OpenCode.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"README.md",
|
|
49
49
|
"LICENSE",
|
|
50
50
|
"NOTICE",
|
|
51
|
+
"THIRD_PARTY_NOTICES.md",
|
|
51
52
|
"SECURITY.md",
|
|
52
53
|
"CONTRIBUTING.md"
|
|
53
54
|
],
|
|
@@ -71,7 +72,7 @@
|
|
|
71
72
|
},
|
|
72
73
|
"dependencies": {
|
|
73
74
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
74
|
-
"borgmcp-shared": "0.
|
|
75
|
+
"borgmcp-shared": "0.12.2",
|
|
75
76
|
"chalk": "^5.3.0",
|
|
76
77
|
"prompts": "^2.4.2",
|
|
77
78
|
"which": "^4.0.0"
|
package/src/assimilate-deps.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
resumeLocalBorgServerEnrollment,
|
|
41
41
|
sendBorgServerAttach,
|
|
42
42
|
} from './server-handshake.js';
|
|
43
|
+
import { advanceLocalServerCursor } from './local-server-cursor.js';
|
|
43
44
|
import {
|
|
44
45
|
findIncompleteSiblingAttempt,
|
|
45
46
|
observeSeat,
|
|
@@ -483,6 +484,16 @@ export function buildDefaultAssimilateDeps(
|
|
|
483
484
|
);
|
|
484
485
|
}
|
|
485
486
|
}
|
|
487
|
+
if (prepared.initialLogCursor) {
|
|
488
|
+
const binding = {
|
|
489
|
+
origin: apiUrl,
|
|
490
|
+
trustIdentity: serverTrustIdentity,
|
|
491
|
+
cubeId: prepared.cube.id,
|
|
492
|
+
droneId: prepared.drone.id,
|
|
493
|
+
};
|
|
494
|
+
await advanceLocalServerCursor(binding, prepared.initialLogCursor);
|
|
495
|
+
await advanceLocalServerCursor({ ...binding, purpose: 'stream' }, prepared.initialLogCursor);
|
|
496
|
+
}
|
|
486
497
|
return {
|
|
487
498
|
cube_id: prepared.cube.id,
|
|
488
499
|
drone_id: prepared.drone.id,
|
package/src/codex-app-wake.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from './cubes.js';
|
|
7
7
|
import { CodexAppServerClient } from './codex-app-server.js';
|
|
8
8
|
import { checkCodexBridgeHealthy } from './codex-remote.js';
|
|
9
|
-
import { hasPendingWakeActivity } from './remote-client.js';
|
|
9
|
+
import { hasPendingWakeActivity, hasPendingWakeEntry } from './remote-client.js';
|
|
10
10
|
import {
|
|
11
11
|
BORG_CODEX_REMOTE_WAKE_ENV,
|
|
12
12
|
resolveSessionAgentKind,
|
|
@@ -98,6 +98,7 @@ let wakeInFlight = false;
|
|
|
98
98
|
const pendingWakeRequests: Array<{
|
|
99
99
|
reason: string;
|
|
100
100
|
deliveryIdentity?: string;
|
|
101
|
+
sourceEntryId?: string;
|
|
101
102
|
deps: CodexWakeDeps;
|
|
102
103
|
}> = [];
|
|
103
104
|
const deliveredWakeKeys = new Set<string>();
|
|
@@ -108,6 +109,8 @@ const DELIVERED_WAKE_KEY_CAP = 100;
|
|
|
108
109
|
// (mid-turn thread) or missed (transient error) into ONE retried-until-delivered
|
|
109
110
|
// drain. The coalesce gate means a burst collapses to one poller, not N.
|
|
110
111
|
let retryDrainInFlight = false;
|
|
112
|
+
const retryDrainSourceEntryIds = new Set<string>();
|
|
113
|
+
let retryDrainHasUnscopedWork = false;
|
|
111
114
|
|
|
112
115
|
// gh#857 WI-2: timestamp of the last SUCCESSFUL wake delivery (per-entry OR
|
|
113
116
|
// retry-drain OR heartbeat). The heartbeat reads this (shouldFireHeartbeat) to
|
|
@@ -192,6 +195,7 @@ export interface CodexWakeDeps {
|
|
|
192
195
|
// Per-entry and retry-drain paths do not use this: their pending obligation is
|
|
193
196
|
// already established by a concrete delivered/deferred event.
|
|
194
197
|
hasPendingWork?: (active: ActiveCube) => Promise<boolean>;
|
|
198
|
+
hasPendingEntry?: (active: ActiveCube, entryId: string) => Promise<boolean>;
|
|
195
199
|
// gh#861 finding 2: lease-ownership gate for the heartbeat tick — a lease-LOSING
|
|
196
200
|
// duplicate child must NOT tick/inject (symmetry with the per-entry path, which
|
|
197
201
|
// only fires inside an SSE session that holds the stream lease). Heartbeat-only;
|
|
@@ -280,10 +284,11 @@ export function wakeCodexViaAppServer(
|
|
|
280
284
|
env: NodeJS.ProcessEnv = process.env,
|
|
281
285
|
deps: CodexWakeDeps = {},
|
|
282
286
|
deliveryIdentity?: string,
|
|
287
|
+
sourceEntryId?: string,
|
|
283
288
|
): void {
|
|
284
289
|
const target = resolveCodexWakeTarget(env);
|
|
285
290
|
if (!target.enabled) return;
|
|
286
|
-
pendingWakeRequests.push({ reason, deliveryIdentity, deps });
|
|
291
|
+
pendingWakeRequests.push({ reason, deliveryIdentity, sourceEntryId, deps });
|
|
287
292
|
if (wakeInFlight) return;
|
|
288
293
|
|
|
289
294
|
wakeInFlight = true;
|
|
@@ -295,21 +300,30 @@ export function wakeCodexViaAppServer(
|
|
|
295
300
|
async function drainCodexWakeQueue(): Promise<void> {
|
|
296
301
|
while (pendingWakeRequests.length > 0) {
|
|
297
302
|
const request = pendingWakeRequests.shift()!;
|
|
298
|
-
await wakeCodexTargeted(
|
|
303
|
+
await wakeCodexTargeted(
|
|
304
|
+
request.reason, request.deliveryIdentity, request.sourceEntryId, request.deps,
|
|
305
|
+
);
|
|
299
306
|
}
|
|
300
307
|
}
|
|
301
308
|
|
|
302
|
-
async function wakeCodexTargeted(
|
|
309
|
+
async function wakeCodexTargeted(
|
|
310
|
+
reason: string,
|
|
311
|
+
deliveryIdentity: string | undefined,
|
|
312
|
+
sourceEntryId: string | undefined,
|
|
313
|
+
deps: CodexWakeDeps,
|
|
314
|
+
): Promise<void> {
|
|
303
315
|
// gh#861 finding 1: another path (heartbeat/retry-drain) is mid-inject into the
|
|
304
316
|
// same thread — defer to the retry-drain so this entry isn't double-injected nor
|
|
305
317
|
// lost (the drain re-syncs the whole burst via the server read-cursor).
|
|
306
318
|
if (!tryAcquireInjectLock()) {
|
|
307
|
-
scheduleRetryDrain(deps);
|
|
319
|
+
scheduleRetryDrain(deps, sourceEntryId);
|
|
308
320
|
return;
|
|
309
321
|
}
|
|
310
322
|
try {
|
|
311
323
|
const active = await (deps.getActiveCube ?? getActiveCube)();
|
|
312
324
|
if (!active) return;
|
|
325
|
+
const pendingEntry = deps.hasPendingEntry ?? hasPendingWakeEntry;
|
|
326
|
+
if (sourceEntryId && !(await pendingEntry(active, sourceEntryId))) return;
|
|
313
327
|
// gh#855: resolve FRESH (live env socket + re-resolved thread), falling back
|
|
314
328
|
// to the launch-recorded file only when the env socket is absent.
|
|
315
329
|
const resolved = await resolveFreshCodexWakeTarget(active, deps);
|
|
@@ -326,9 +340,10 @@ async function wakeCodexTargeted(reason: string, deliveryIdentity: string | unde
|
|
|
326
340
|
// now. Schedule the retry-drain (coalesced, retried-until-delivered) so
|
|
327
341
|
// the burst's entries are drained once the thread goes idle; codex has no
|
|
328
342
|
// on-disk tail fallback like Claude's borg-inbox-monitor.
|
|
329
|
-
scheduleRetryDrain(deps);
|
|
343
|
+
scheduleRetryDrain(deps, sourceEntryId);
|
|
330
344
|
return;
|
|
331
345
|
}
|
|
346
|
+
if (sourceEntryId && !(await pendingEntry(active, sourceEntryId))) return;
|
|
332
347
|
await client.startTurn(threadId, reason);
|
|
333
348
|
rememberDeliveredWake(wakeKey);
|
|
334
349
|
markDelivered(deps);
|
|
@@ -340,7 +355,7 @@ async function wakeCodexTargeted(reason: string, deliveryIdentity: string | unde
|
|
|
340
355
|
// swallowed (the old best-effort drop let a single blip lose an entry).
|
|
341
356
|
// Schedule the retry-drain so the wake is retried-until-delivered; the SSE
|
|
342
357
|
// stream is never broken (this is fire-and-forget).
|
|
343
|
-
scheduleRetryDrain(deps);
|
|
358
|
+
scheduleRetryDrain(deps, sourceEntryId);
|
|
344
359
|
} finally {
|
|
345
360
|
releaseInjectLock();
|
|
346
361
|
}
|
|
@@ -357,7 +372,9 @@ async function wakeCodexTargeted(reason: string, deliveryIdentity: string | unde
|
|
|
357
372
|
* (wakeRetryExpired); the gh#857 WI-2 heartbeat is the backstop beyond that.
|
|
358
373
|
* Never throws into the SSE path (fire-and-forget).
|
|
359
374
|
*/
|
|
360
|
-
function scheduleRetryDrain(deps: CodexWakeDeps): void {
|
|
375
|
+
function scheduleRetryDrain(deps: CodexWakeDeps, sourceEntryId?: string): void {
|
|
376
|
+
if (sourceEntryId) retryDrainSourceEntryIds.add(sourceEntryId);
|
|
377
|
+
else retryDrainHasUnscopedWork = true;
|
|
361
378
|
if (retryDrainInFlight) return; // coalesce: one loop covers all deferred/missed wakes
|
|
362
379
|
retryDrainInFlight = true;
|
|
363
380
|
void runRetryDrainLoop(deps).finally(() => {
|
|
@@ -383,6 +400,15 @@ async function runRetryDrainLoop(deps: CodexWakeDeps): Promise<void> {
|
|
|
383
400
|
try {
|
|
384
401
|
const active = await (deps.getActiveCube ?? getActiveCube)();
|
|
385
402
|
if (!active) continue; // no active cube yet → keep retrying (until age cap)
|
|
403
|
+
const pendingEntry = deps.hasPendingEntry ?? hasPendingWakeEntry;
|
|
404
|
+
for (const entryId of retryDrainSourceEntryIds) {
|
|
405
|
+
try {
|
|
406
|
+
if (!(await pendingEntry(active, entryId))) retryDrainSourceEntryIds.delete(entryId);
|
|
407
|
+
} catch {
|
|
408
|
+
// Retain the obligation until unread state can be checked.
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (!retryDrainHasUnscopedWork && retryDrainSourceEntryIds.size === 0) return;
|
|
386
412
|
// gh#855: same FRESH resolution as the per-entry wake, so a stale launch
|
|
387
413
|
// probe can't defeat the retry-drain either.
|
|
388
414
|
const resolved = await resolveFreshCodexWakeTarget(active, deps);
|
|
@@ -395,7 +421,13 @@ async function runRetryDrainLoop(deps: CodexWakeDeps): Promise<void> {
|
|
|
395
421
|
if (thread?.status?.type === 'active') {
|
|
396
422
|
continue; // re-defer: still mid-turn (backoff before next poll)
|
|
397
423
|
}
|
|
424
|
+
for (const entryId of retryDrainSourceEntryIds) {
|
|
425
|
+
if (!(await pendingEntry(active, entryId))) retryDrainSourceEntryIds.delete(entryId);
|
|
426
|
+
}
|
|
427
|
+
if (!retryDrainHasUnscopedWork && retryDrainSourceEntryIds.size === 0) return;
|
|
398
428
|
await client.startTurn(threadId, CODEX_CATCHUP_PROMPT);
|
|
429
|
+
retryDrainSourceEntryIds.clear();
|
|
430
|
+
retryDrainHasUnscopedWork = false;
|
|
399
431
|
markDelivered(deps);
|
|
400
432
|
return; // drain delivered → server read-cursor drains all unread → done
|
|
401
433
|
} finally {
|
|
@@ -515,6 +547,8 @@ export function resetCodexWakeForTests(): void {
|
|
|
515
547
|
deliveredWakeKeys.clear();
|
|
516
548
|
deliveredWakeKeyOrder.length = 0;
|
|
517
549
|
retryDrainInFlight = false;
|
|
550
|
+
retryDrainSourceEntryIds.clear();
|
|
551
|
+
retryDrainHasUnscopedWork = false;
|
|
518
552
|
lastDeliveredAt = null;
|
|
519
553
|
heartbeatInFlight = false;
|
|
520
554
|
injectInFlight = false;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const CUBE_ACTIVITY_RESUME_WAKE_MESSAGE =
|
|
2
|
-
'Borg cube activity arrived while you were busy. Reading cube messages does not end your current task. Drain `borg_read-log unread_only=true` until caught up, handle actionable entries, then RESUME the interrupted work.';
|
|
2
|
+
'Borg cube activity arrived while you were busy. Reading cube messages does not end your current task. Drain `borg_read-log unread_only=true` until caught up, handle actionable entries, then RESUME the interrupted work. If the unread drain is empty, resume silently without a liveness post or full regen.';
|
|
3
3
|
|
|
4
4
|
export function formatCubeActivityWakeMessage(detail: string): string {
|
|
5
5
|
return `${CUBE_ACTIVITY_RESUME_WAKE_MESSAGE}\n${detail}`;
|