@nanobpm/nano-workforce 0.178.2 → 0.178.4
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/.github/workflows/release.yml +73 -7
- package/CHANGELOG.md +17 -0
- package/README.md +1 -1
- package/e2e/mcp-session-selfheal.e2e.ts +39 -27
- package/package.json +2 -2
|
@@ -7,6 +7,16 @@ on:
|
|
|
7
7
|
workflow_run:
|
|
8
8
|
workflows: ["CI", "Whole-repo invariants (merge-skew guard)"]
|
|
9
9
|
types: [completed]
|
|
10
|
+
# Manual escape hatch. The automatic path above depends on the post-merge
|
|
11
|
+
# `push:main` event that normally follows a merge-queue landing. That event can
|
|
12
|
+
# go MISSING when an out-of-band bypass push to `main` (a `chore(release)
|
|
13
|
+
# [skip ci]` commit from the release App) races the queue's merge — the merged
|
|
14
|
+
# commit then lands with only its `merge_group` run and no `push` run, so the
|
|
15
|
+
# `workflow_run` trigger never sees `event == 'push'` and the release wedges
|
|
16
|
+
# with no way to retrigger. Dispatching runs the SAME green-gate against
|
|
17
|
+
# `main`'s HEAD (the merge_group checks are attached to that commit), so a
|
|
18
|
+
# manual recovery still never ships from a red `main`.
|
|
19
|
+
workflow_dispatch:
|
|
10
20
|
|
|
11
21
|
# Prevent overlapping releases from racing on the same branch.
|
|
12
22
|
concurrency:
|
|
@@ -26,9 +36,10 @@ jobs:
|
|
|
26
36
|
name: release gate
|
|
27
37
|
runs-on: ubuntu-latest
|
|
28
38
|
if: >-
|
|
29
|
-
github.
|
|
30
|
-
github.event.workflow_run.
|
|
31
|
-
|
|
39
|
+
github.event_name == 'workflow_dispatch' ||
|
|
40
|
+
(github.event.workflow_run.event == 'push' &&
|
|
41
|
+
github.event.workflow_run.head_branch == 'main' &&
|
|
42
|
+
github.event.workflow_run.conclusion == 'success')
|
|
32
43
|
permissions:
|
|
33
44
|
checks: read
|
|
34
45
|
contents: read
|
|
@@ -39,7 +50,7 @@ jobs:
|
|
|
39
50
|
id: gate
|
|
40
51
|
env:
|
|
41
52
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
42
|
-
SHA: ${{ github.event.workflow_run.head_sha }}
|
|
53
|
+
SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
|
|
43
54
|
REPO: ${{ github.repository }}
|
|
44
55
|
run: |
|
|
45
56
|
set -euo pipefail
|
|
@@ -78,7 +89,7 @@ jobs:
|
|
|
78
89
|
# done with the release App token below — it is the ruleset bypass actor. The
|
|
79
90
|
# job's own GITHUB_TOKEN only needs OIDC for npm Trusted Publishing.
|
|
80
91
|
contents: read
|
|
81
|
-
id-token: write # OIDC
|
|
92
|
+
id-token: write # OIDC for npm Trusted Publishing + provenance (the only auth path)
|
|
82
93
|
steps:
|
|
83
94
|
# Mint a short-lived token for the dedicated release GitHub App. This App is
|
|
84
95
|
# the ruleset's bypass actor, so @semantic-release/git can push the version
|
|
@@ -96,7 +107,7 @@ jobs:
|
|
|
96
107
|
- name: Checkout
|
|
97
108
|
uses: actions/checkout@v4
|
|
98
109
|
with:
|
|
99
|
-
ref: ${{ github.event.workflow_run.head_sha }} # release the exact gated commit
|
|
110
|
+
ref: ${{ github.event.workflow_run.head_sha || github.sha }} # release the exact gated commit
|
|
100
111
|
fetch-depth: 0 # semantic-release needs full history + tags
|
|
101
112
|
persist-credentials: true # let @semantic-release/git push the release commit
|
|
102
113
|
token: ${{ steps.app-token.outputs.token }} # push as the bypass-actor App
|
|
@@ -129,8 +140,63 @@ jobs:
|
|
|
129
140
|
run: npm ci
|
|
130
141
|
|
|
131
142
|
- name: Release
|
|
143
|
+
id: release
|
|
144
|
+
# OIDC Trusted Publishing is the ONLY automated auth path: npm has retired
|
|
145
|
+
# automation tokens that bypass 2FA, so there is no CI token fallback — it is
|
|
146
|
+
# OIDC or a human `npm publish` locally. OIDC is intermittently flaky, though:
|
|
147
|
+
# npm has returned `401 … Failed to generate Web Auth URLs … token is invalid`
|
|
148
|
+
# mid-publish. @semantic-release/git pushes the tag + release commit in the
|
|
149
|
+
# `prepare` step BEFORE @semantic-release/npm `publish`, so an OIDC publish
|
|
150
|
+
# failure orphans a git tag with no npm package (the v0.178.3 phantom).
|
|
151
|
+
# Don't fail the job here — let the OIDC publish-retry step below recover it.
|
|
152
|
+
continue-on-error: true
|
|
132
153
|
env:
|
|
133
154
|
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} # bypass-actor App token
|
|
134
|
-
# No NPM_TOKEN / NODE_AUTH_TOKEN: publishing
|
|
155
|
+
# No NPM_TOKEN / NODE_AUTH_TOKEN: publishing is OIDC Trusted Publishing only.
|
|
135
156
|
NPM_CONFIG_PROVENANCE: "true"
|
|
136
157
|
run: npx semantic-release
|
|
158
|
+
|
|
159
|
+
# Recovery: if the OIDC publish 401s, semantic-release has already bumped
|
|
160
|
+
# package.json + pushed the tag/release commit, but npm never got the tarball.
|
|
161
|
+
# The failure is INTERMITTENT (a prior version published fine), so retry the
|
|
162
|
+
# OIDC publish of the just-prepared version a few times with backoff — still no
|
|
163
|
+
# token, still OIDC + provenance via the job's id-token. This step is a NO-OP
|
|
164
|
+
# when the release step succeeded or when the version is already on npm, so it
|
|
165
|
+
# can never double-publish. If OIDC stays down through every retry, fail loudly:
|
|
166
|
+
# a human must `npm publish` the pushed tag locally (npm login with 2FA, then
|
|
167
|
+
# `git checkout vX.Y.Z && npm ci && npm publish --provenance --access public`).
|
|
168
|
+
- name: Retry OIDC publish if the release publish failed
|
|
169
|
+
if: steps.release.outcome == 'failure'
|
|
170
|
+
env:
|
|
171
|
+
NPM_CONFIG_PROVENANCE: "true"
|
|
172
|
+
GH_TOKEN: ${{ steps.app-token.outputs.token }} # to create the skipped GitHub Release
|
|
173
|
+
run: |
|
|
174
|
+
set -uo pipefail
|
|
175
|
+
PKG="$(node -p "require('./package.json').name")"
|
|
176
|
+
VERSION="$(node -p "require('./package.json').version")"
|
|
177
|
+
if npm view "${PKG}@${VERSION}" version >/dev/null 2>&1; then
|
|
178
|
+
echo "${PKG}@${VERSION} is already on npm — nothing to recover."
|
|
179
|
+
exit 0
|
|
180
|
+
fi
|
|
181
|
+
echo "semantic-release prepared + tagged ${PKG}@${VERSION} but the OIDC publish failed; retrying."
|
|
182
|
+
published=false
|
|
183
|
+
for attempt in 1 2 3; do
|
|
184
|
+
echo "OIDC publish attempt ${attempt}/3 for ${PKG}@${VERSION}"
|
|
185
|
+
if npm publish --provenance --access public; then
|
|
186
|
+
echo "Published ${PKG}@${VERSION} on retry ${attempt}."
|
|
187
|
+
published=true
|
|
188
|
+
break
|
|
189
|
+
fi
|
|
190
|
+
sleep $((attempt * 30))
|
|
191
|
+
done
|
|
192
|
+
if [ "$published" != true ]; then
|
|
193
|
+
echo "::error::OIDC publish of ${PKG}@${VERSION} still failing after retries. The git tag v${VERSION} is pushed but npm has no tarball. Publish it locally: 'git fetch --tags && git checkout v${VERSION} && npm ci && npm publish --provenance --access public' (npm login with 2FA first)."
|
|
194
|
+
exit 1
|
|
195
|
+
fi
|
|
196
|
+
# semantic-release aborts before @semantic-release/github when the npm
|
|
197
|
+
# publish throws, so the GitHub Release was never created — create it now.
|
|
198
|
+
if ! gh release view "v${VERSION}" >/dev/null 2>&1; then
|
|
199
|
+
echo "Creating the GitHub Release skipped by the aborted semantic-release run."
|
|
200
|
+
gh release create "v${VERSION}" --verify-tag --generate-notes --title "v${VERSION}" || \
|
|
201
|
+
echo "::warning::Published to npm but failed to create GitHub Release v${VERSION}; create it manually."
|
|
202
|
+
fi
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
## [0.178.4](https://github.com/nanobpm/nano-workforce/compare/v0.178.3...v0.178.4) (2026-09-03)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* **mcp:** align session-selfheal e2e with urban 0.90.1 spec-conformant 404/-32001 ([#724](https://github.com/nanobpm/nano-workforce/issues/724)) ([5f54f3c](https://github.com/nanobpm/nano-workforce/commit/5f54f3ce42bbcbf30715bf17829e3f9ddd9cf793)), closes [#715](https://github.com/nanobpm/nano-workforce/issues/715)
|
|
6
|
+
|
|
7
|
+
## [0.178.3](https://github.com/nanobpm/nano-workforce/compare/v0.178.2...v0.178.3) (2026-09-03)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **ci:** add manual workflow_dispatch escape hatch to the release pipeline ([#723](https://github.com/nanobpm/nano-workforce/issues/723)) ([4d06f25](https://github.com/nanobpm/nano-workforce/commit/4d06f259aee7a5689844ddd0dfe3f68253c9fc37))
|
|
12
|
+
* **ci:** retry OIDC publish when the release publish 401s; reclaim v0.178.3 ([#722](https://github.com/nanobpm/nano-workforce/issues/722)) ([208c02d](https://github.com/nanobpm/nano-workforce/commit/208c02dd1936c70635a5108887f452143a8dbc26))
|
|
13
|
+
|
|
14
|
+
### Documentation
|
|
15
|
+
|
|
16
|
+
* refresh stale --version pin example in the upgrade guide ([#721](https://github.com/nanobpm/nano-workforce/issues/721)) ([75dbcec](https://github.com/nanobpm/nano-workforce/commit/75dbcecc93b4adef3bb4d6a05f6010ed44ea74dd))
|
|
17
|
+
|
|
1
18
|
## [0.178.2](https://github.com/nanobpm/nano-workforce/compare/v0.178.1...v0.178.2) (2026-09-03)
|
|
2
19
|
|
|
3
20
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -185,7 +185,7 @@ and runs the review-ready poller.
|
|
|
185
185
|
```sh
|
|
186
186
|
npm run upgrade # dry-run against @latest
|
|
187
187
|
npm run upgrade -- --apply # apply the overlay, preserving app.db
|
|
188
|
-
npm run upgrade -- --version 0.
|
|
188
|
+
npm run upgrade -- --version 0.178.2 --apply # pin a specific version
|
|
189
189
|
npm run upgrade -- --from ./pkg.tgz --apply # from a local tarball (offline)
|
|
190
190
|
```
|
|
191
191
|
|
|
@@ -3,20 +3,25 @@
|
|
|
3
3
|
// WHAT THIS PINS
|
|
4
4
|
// ==============
|
|
5
5
|
// The runtime-served MCP surface (`/app/mcp`, ADR 0067) is a **stateful streamable-HTTP** transport:
|
|
6
|
-
// every `tools/call` MUST carry a valid `mcp-session-id
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// "
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
6
|
+
// every `tools/call` MUST carry a valid `mcp-session-id`. Per the MCP Streamable-HTTP spec the runtime
|
|
7
|
+
// distinguishes two refusals (urban ≥ 0.90.1): a session id that is SUPPLIED but unknown / stale /
|
|
8
|
+
// evicted / deleted is a terminated session, answered `404` with JSON-RPC `-32001 "Session not found:
|
|
9
|
+
// unknown or expired mcp-session-id."` so the client transparently re-initializes; only a GENUINELY
|
|
10
|
+
// MISSING id (no header at all) is the `400` / `-32000 "Bad Request: no valid session id, and not an
|
|
11
|
+
// initialize request."` bad-client case (mcp.ts). In the field (issue #715) a single hiccup — a
|
|
12
|
+
// heavy-tool timeout, an idle drop, a proxy reset, or LRU eviction (`MAX_SESSIONS`) — loses the
|
|
13
|
+
// session and then bricks the ENTIRE surface for a client that does not re-`initialize`: every
|
|
14
|
+
// subsequent tool reads as "tool does not exist". The stateless/resumable transport that would remove
|
|
15
|
+
// the session dependency lives in the urban runtime and is tracked upstream (nano-ide#488); until it
|
|
16
|
+
// lands, the **workforce-visible requirement** (this issue) is that the surface is RECOVERABLE — a
|
|
17
|
+
// client that re-`initialize`s after a gone-session refusal gets the WHOLE surface back in one round
|
|
18
|
+
// trip, not a degraded one.
|
|
15
19
|
//
|
|
16
20
|
// This is the acceptance regression: "kill the session mid-flight and assert the next call still
|
|
17
21
|
// works." It kills the session two faithful ways — an unknown/stale id, and a server-side `DELETE`
|
|
18
|
-
// (the spec session-termination verb) of a live id — asserts each bricks a call with
|
|
19
|
-
//
|
|
22
|
+
// (the spec session-termination verb) of a live id — asserts each bricks a call with a session-gone
|
|
23
|
+
// signature (a supplied-unknown id → `404` / `-32001`, a DELETEd id → the SDK transport's `Session
|
|
24
|
+
// not found`), then asserts a single client `reinitialize()` fully restores the surface
|
|
20
25
|
// (a working `tools/call` AND the complete `tools/list`). If a future runtime makes the transport
|
|
21
26
|
// stateless/resumable (nano-ide#488), the stale-id call simply stops erroring — this test then
|
|
22
27
|
// tightens to that stronger contract with a one-line change, never silently passing on a regression.
|
|
@@ -27,14 +32,12 @@ import { randomUUID } from "node:crypto";
|
|
|
27
32
|
import { after, before, describe, test } from "node:test";
|
|
28
33
|
import { bootMcpHarness, type McpHarness } from "./support/mcp-harness.ts";
|
|
29
34
|
|
|
30
|
-
/**
|
|
31
|
-
*
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* refused because the session no longer exists — the field failure #715 gap 1 is about. */
|
|
37
|
-
const SESSION_GONE = /no valid session id|session not found/i;
|
|
35
|
+
/** Any "the session is gone" refusal, in either faithful form: the runtime's `-32001` "Session not
|
|
36
|
+
* found: unknown or expired …" for a SUPPLIED but unknown / stale / evicted / DELETEd id (urban
|
|
37
|
+
* ≥ 0.90.1), OR the SDK transport's own "Session not found" for a terminated id. Either proves the
|
|
38
|
+
* call was refused because the session no longer exists — the field failure #715 gap 1 is about. A
|
|
39
|
+
* recovered surface must NOT answer with this after a re-`initialize`. */
|
|
40
|
+
const SESSION_GONE = /session not found|unknown or expired|no valid session id/i;
|
|
38
41
|
|
|
39
42
|
/** A safe, side-effect-free read used as the "does the surface answer?" probe. */
|
|
40
43
|
const PROBE_TOOL = "getVersion";
|
|
@@ -53,14 +56,22 @@ describe("#715 gap 1 — a lost MCP session self-heals on client re-initialize",
|
|
|
53
56
|
assert(!res.isError, `baseline ${PROBE_TOOL} should succeed on a live session: ${res.text}`);
|
|
54
57
|
});
|
|
55
58
|
|
|
56
|
-
test("an unknown/stale session id bricks a call with -
|
|
59
|
+
test("an unknown/stale session id bricks a call with 404 / -32001, and re-initialize restores the surface", async () => {
|
|
57
60
|
// A stale id models an evicted (LRU / idle-dropped) or proxy-reset session the client still holds.
|
|
58
61
|
const staleId = `stale-${randomUUID()}`;
|
|
59
62
|
const bricked = await h.callToolAs(staleId, PROBE_TOOL);
|
|
60
63
|
assert(bricked.isError, "a call carrying a stale session id must be refused, not answered");
|
|
64
|
+
// A SUPPLIED-but-unknown id is a terminated session: per the MCP Streamable-HTTP spec the runtime
|
|
65
|
+
// answers 404 / -32001 "Session not found: unknown or expired …" (urban ≥ 0.90.1) so the client
|
|
66
|
+
// transparently re-initializes, rather than the 400 / -32000 reserved for a genuinely missing id.
|
|
67
|
+
assert.equal(
|
|
68
|
+
bricked.httpStatus,
|
|
69
|
+
404,
|
|
70
|
+
`a stale (supplied-but-unknown) session id must be refused 404 so the client re-initializes, got HTTP ${bricked.httpStatus}: ${bricked.text}`,
|
|
71
|
+
);
|
|
61
72
|
assert(
|
|
62
|
-
bricked.text
|
|
63
|
-
`a stale-session call must fail with
|
|
73
|
+
SESSION_GONE.test(bricked.text),
|
|
74
|
+
`a stale-session call must fail with a session-gone signature, got: ${bricked.text}`,
|
|
64
75
|
);
|
|
65
76
|
|
|
66
77
|
// Client self-heal: re-run the handshake. The pinned runtime is stateful, so this is how a real
|
|
@@ -72,8 +83,8 @@ describe("#715 gap 1 — a lost MCP session self-heals on client re-initialize",
|
|
|
72
83
|
const healed = await h.callTool(PROBE_TOOL);
|
|
73
84
|
assert(!healed.isError, `after reinitialize the surface must answer again: ${healed.text}`);
|
|
74
85
|
assert(
|
|
75
|
-
!healed.text
|
|
76
|
-
"a healed call must not still report a
|
|
86
|
+
!SESSION_GONE.test(healed.text),
|
|
87
|
+
"a healed call must not still report a gone session",
|
|
77
88
|
);
|
|
78
89
|
|
|
79
90
|
// And the FULL projected catalogue is restored — the field failure was "every tool vanished".
|
|
@@ -95,9 +106,10 @@ describe("#715 gap 1 — a lost MCP session self-heals on client re-initialize",
|
|
|
95
106
|
const status = await h.deleteSession(killedId);
|
|
96
107
|
assert(status < 500, `DELETE session should not server-error, got ${status}`);
|
|
97
108
|
|
|
98
|
-
// The now-terminated id bricks a call — the mid-flight hiccup.
|
|
99
|
-
//
|
|
100
|
-
// —
|
|
109
|
+
// The now-terminated id bricks a call — the mid-flight hiccup. Both a DELETEd/terminated id and a
|
|
110
|
+
// supplied-but-unknown id now surface a "Session not found" gone-signal (the SDK transport's own,
|
|
111
|
+
// or the runtime's 404 / -32001 "unknown or expired") — either means the session is gone and the
|
|
112
|
+
// call was refused, not answered.
|
|
101
113
|
const bricked = await h.callToolAs(killedId, PROBE_TOOL);
|
|
102
114
|
assert(bricked.isError, "a call on a DELETEd session id must be refused");
|
|
103
115
|
assert(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.178.
|
|
3
|
+
"version": "0.178.4",
|
|
4
4
|
"description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "main.ts",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"@nanobpm/agentic": "^0.12.0",
|
|
68
|
-
"@nanobpm/urban": "^0.90.
|
|
68
|
+
"@nanobpm/urban": "^0.90.1",
|
|
69
69
|
"bpmn-auto-layout": "^2.0.0-alpha.2"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|