@nanobpm/nano-workforce 0.185.0 → 0.185.1
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/CHANGELOG.md +6 -0
- package/app/readiness.test.ts +51 -0
- package/app/readiness.ts +55 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [0.185.1](https://github.com/nanobpm/nano-workforce/compare/v0.185.0...v0.185.1) (2026-09-08)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* **agentic:** capability probe recognises single-package v-prefixed release tags ([#765](https://github.com/nanobpm/nano-workforce/issues/765)) ([79faa67](https://github.com/nanobpm/nano-workforce/commit/79faa676f46882d4e10f3ce0bb70080f40e3e757)), closes [#764](https://github.com/nanobpm/nano-workforce/issues/764)
|
|
6
|
+
|
|
1
7
|
## [0.185.0](https://github.com/nanobpm/nano-workforce/compare/v0.184.1...v0.185.0) (2026-09-08)
|
|
2
8
|
|
|
3
9
|
### Features
|
package/app/readiness.test.ts
CHANGED
|
@@ -220,6 +220,57 @@ test("matchCapability: the bare '#274' ref form resolves identically to 'nano-id
|
|
|
220
220
|
assertEquals(res.bind?.resolvedArtifact, "@nanobpm/urban@1.2.3");
|
|
221
221
|
});
|
|
222
222
|
|
|
223
|
+
// ── #764: single-package repos tag releases `v<version>` / bare `<version>` (semantic-release default) ──
|
|
224
|
+
|
|
225
|
+
const c8Match = { capabilityRef: "c8ctl-plugin-nano#206", package: "c8ctl-plugin-nano" };
|
|
226
|
+
|
|
227
|
+
test("#764 matchCapability: a `v<version>` release (body refs the cap) resolves in a single-package repo", () => {
|
|
228
|
+
// Red before the fix: `v1.58.0` never matched versionForPackage(tag, 'c8ctl-plugin-nano'),
|
|
229
|
+
// so the gate reported 'not published yet' though the npm package WAS published.
|
|
230
|
+
const res = matchCapability(c8Match, [rel("v1.58.0", [206])]);
|
|
231
|
+
assert(res.ready);
|
|
232
|
+
assertEquals(res.bind?.resolvedArtifact, "c8ctl-plugin-nano@1.58.0");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("#764 matchCapability: a bare `<version>` release resolves the same way in a single-package repo", () => {
|
|
236
|
+
const res = matchCapability(c8Match, [rel("1.58.0", [206])]);
|
|
237
|
+
assert(res.ready);
|
|
238
|
+
assertEquals(res.bind?.resolvedArtifact, "c8ctl-plugin-nano@1.58.0");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("#764 monorepo guard: a stray `v2.0.0` alongside package-scoped tags is NOT attributed to another package", () => {
|
|
242
|
+
// The repo emits package-scoped tags → it is a monorepo → bare `v` tags are ignored, so the stray
|
|
243
|
+
// v2.0.0 can never leak into @nanobpm/other's resolution.
|
|
244
|
+
const releases = [rel("@nanobpm/other@1.0.0", [200]), rel("v2.0.0", [274])];
|
|
245
|
+
const res = matchCapability({ capabilityRef: "nano-ide#274", package: "@nanobpm/other" }, releases);
|
|
246
|
+
assert(!res.ready, "the stray v2.0.0 must not resolve #274 for @nanobpm/other");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("#764 summariseCapabilityCandidates: shows the extended `v`-tag candidate in a single-package repo", () => {
|
|
250
|
+
const summary = summariseCapabilityCandidates(c8Match, [rel("v1.58.0", [206]), rel("v1.57.0", [200])]);
|
|
251
|
+
assertStringIncludes(summary, "2 c8ctl-plugin-nano release(s) observed");
|
|
252
|
+
assertStringIncludes(summary, "c8ctl-plugin-nano@1.58.0 refs #206");
|
|
253
|
+
assertStringIncludes(summary, "c8ctl-plugin-nano@1.57.0 no #206");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("#764 summariseCapabilityCandidates: in a monorepo a stray `v` tag is not summarised as another package's candidate", () => {
|
|
257
|
+
const summary = summariseCapabilityCandidates({ capabilityRef: "nano-ide#274", package: "@nanobpm/other" }, [
|
|
258
|
+
rel("@nanobpm/other@1.0.0", [200]),
|
|
259
|
+
rel("v2.0.0", [274]),
|
|
260
|
+
]);
|
|
261
|
+
assertStringIncludes(summary, "1 @nanobpm/other release(s) observed");
|
|
262
|
+
assert(!summary.includes("2.0.0"), "the stray monorepo v2.0.0 is not a candidate for @nanobpm/other");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("#764 newestPublishedVersion: resolves from `v`-tags under the single-package condition", () => {
|
|
266
|
+
assertEquals(newestPublishedVersion("c8ctl-plugin-nano", [rel("v1.57.0", []), rel("v1.58.0", [])]), "1.58.0");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("#764 newestPublishedVersion: a stray `v` tag in a monorepo is ignored for a scoped package", () => {
|
|
270
|
+
const releases = [rel("@nanobpm/urban@0.9.0", []), rel("v9.9.9", [])];
|
|
271
|
+
assertEquals(newestPublishedVersion("@nanobpm/urban", releases), "0.9.0");
|
|
272
|
+
});
|
|
273
|
+
|
|
223
274
|
// ── #514 Defect A: the capability gate is self-diagnosing on escalation ──────────────────────────
|
|
224
275
|
|
|
225
276
|
test("#514 Defect A summariseCapabilityCandidates: lists candidate releases (newest first) and whether each references the ref", () => {
|
package/app/readiness.ts
CHANGED
|
@@ -527,18 +527,58 @@ function bodyReferences(body: string, num: string): boolean {
|
|
|
527
527
|
return new RegExp(`#${num}(?!\\d)`).test(body);
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
-
/**
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
530
|
+
/** A numeric-dotted SemVer-ish core, e.g. `1.58.0`. */
|
|
531
|
+
const VERSION_CORE = /^\d+(\.\d+)*$/;
|
|
532
|
+
|
|
533
|
+
/** Is `tag` a **package-scoped** version tag — `<anything>@<version>` (numeric-dotted), the monorepo
|
|
534
|
+
* convention (e.g. `@nanobpm/urban@0.54.0`, `c8ctl-plugin-nano@1.58.0`)? Used to decide whether a
|
|
535
|
+
* repo follows the single-package (`v1.58.0` / bare) convention: a repo is "single-package" only when
|
|
536
|
+
* it emits NO package-scoped tags at all. Anchored on the LAST `@` so scoped npm names (`@org/name@1.0.0`)
|
|
537
|
+
* are recognised. */
|
|
538
|
+
function isPackageScopedVersionTag(tag: string): boolean {
|
|
539
|
+
const at = tag.lastIndexOf("@");
|
|
540
|
+
if (at <= 0) return false; // no `@`, or a leading `@` (scoped-name start) with nothing before it
|
|
541
|
+
return VERSION_CORE.test(tag.slice(at + 1).trim());
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** True when the repo's observed release set contains NO `<anything>@<version>`-prefixed package-scoped
|
|
545
|
+
* tags at all — i.e. it follows the single-package `v<version>` / bare `<version>` convention
|
|
546
|
+
* (semantic-release default), so a bare `v1.58.0` tag may be attributed to the sole package (#764).
|
|
547
|
+
* A repo using package-scoped tags is a monorepo: bare `v` tags are ignored there, keeping
|
|
548
|
+
* per-package provenance scoping airtight (a sibling package's provenance can never leak). */
|
|
549
|
+
function repoIsSinglePackage(releases: readonly GithubRelease[]): boolean {
|
|
550
|
+
for (const rel of releases) {
|
|
551
|
+
if (rel && typeof rel.tag === "string" && isPackageScopedVersionTag(rel.tag)) return false;
|
|
552
|
+
}
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** The `<version>` a release tag carries for `pkg`, or undefined when it belongs to another package /
|
|
557
|
+
* is not a version tag. A tag is a candidate for `pkg` when **either**:
|
|
558
|
+
*
|
|
559
|
+
* 1. it is `<pkg>@<version>` (numeric-dotted) — the monorepo convention, per-package scoped so a
|
|
560
|
+
* sibling package's provenance can never leak; **or**
|
|
561
|
+
* 2. `singlePackage` is true (the repo emits no package-scoped tags — see {@link repoIsSinglePackage})
|
|
562
|
+
* AND the tag is `v<version>` or a bare `<version>` — the single-package / semantic-release-default
|
|
563
|
+
* convention (#764). In a monorepo (`singlePackage` false) bare `v` tags are ignored, so scoping
|
|
564
|
+
* stays airtight. */
|
|
565
|
+
function versionForPackage(tag: string, pkg: string, singlePackage: boolean): string | undefined {
|
|
534
566
|
const prefix = `${pkg}@`;
|
|
535
|
-
if (
|
|
536
|
-
|
|
537
|
-
|
|
567
|
+
if (tag.startsWith(prefix)) {
|
|
568
|
+
const v = tag.slice(prefix.length).trim();
|
|
569
|
+
return VERSION_CORE.test(v) ? v : undefined;
|
|
570
|
+
}
|
|
571
|
+
if (singlePackage) {
|
|
572
|
+
const v = (tag.startsWith("v") ? tag.slice(1) : tag).trim();
|
|
573
|
+
if (VERSION_CORE.test(v)) return v;
|
|
574
|
+
}
|
|
575
|
+
return undefined;
|
|
538
576
|
}
|
|
539
577
|
|
|
540
578
|
/** A compact, deterministic, BOUNDED summary of the capability candidate releases a probe observed
|
|
541
|
-
* (issue #514 Defect A). Lists the releases
|
|
579
|
+
* (issue #514 Defect A). Lists the releases carrying a resolvable `<match.package>` version — tagged
|
|
580
|
+
* `<match.package>@<version>`, or (for a single-package repo) a `v<version>`/bare `<version>` tag —
|
|
581
|
+
* newest first, and,
|
|
542
582
|
* for each, whether its body referenced `match.capabilityRef` — so an escalated capability gate can
|
|
543
583
|
* show a human/agent EXACTLY what the probe saw (a genuine "not published yet" vs. a transient
|
|
544
584
|
* false-negative where a matching release was live but its provenance body was momentarily empty).
|
|
@@ -548,6 +588,7 @@ function versionForPackage(tag: string, pkg: string): string | undefined {
|
|
|
548
588
|
export function summariseCapabilityCandidates(
|
|
549
589
|
match: ProbeMatch | undefined,
|
|
550
590
|
releases: readonly GithubRelease[],
|
|
591
|
+
singlePackage: boolean = repoIsSinglePackage(releases),
|
|
551
592
|
): string {
|
|
552
593
|
const pkg = match?.package;
|
|
553
594
|
const ref = match?.capabilityRef;
|
|
@@ -556,7 +597,7 @@ export function summariseCapabilityCandidates(
|
|
|
556
597
|
const candidates: { version: string; refs: boolean }[] = [];
|
|
557
598
|
for (const rel of releases) {
|
|
558
599
|
if (!rel || typeof rel.tag !== "string" || typeof rel.body !== "string") continue;
|
|
559
|
-
const version = versionForPackage(rel.tag, pkg);
|
|
600
|
+
const version = versionForPackage(rel.tag, pkg, singlePackage);
|
|
560
601
|
if (!version) continue;
|
|
561
602
|
candidates.push({ version, refs: num !== undefined && bodyReferences(rel.body, num) });
|
|
562
603
|
}
|
|
@@ -591,7 +632,8 @@ const CAPABILITY_SUMMARY_LIMIT = 8;
|
|
|
591
632
|
export function matchCapability(match: ProbeMatch | undefined, releases: readonly GithubRelease[]): ProbeResult {
|
|
592
633
|
const pkg = match?.package;
|
|
593
634
|
const ref = match?.capabilityRef;
|
|
594
|
-
const
|
|
635
|
+
const singlePackage = repoIsSinglePackage(releases);
|
|
636
|
+
const observed = summariseCapabilityCandidates(match, releases, singlePackage);
|
|
595
637
|
if (!pkg || !ref) return { ready: false, detail: "capability: missing package/capabilityRef", observed };
|
|
596
638
|
const num = capabilityNumber(ref);
|
|
597
639
|
if (!num) return { ready: false, detail: "capability: unparseable capabilityRef (no #NNN)", observed };
|
|
@@ -599,7 +641,7 @@ export function matchCapability(match: ProbeMatch | undefined, releases: readonl
|
|
|
599
641
|
let firstVersion: string | undefined;
|
|
600
642
|
for (const rel of releases) {
|
|
601
643
|
if (!rel || typeof rel.tag !== "string" || typeof rel.body !== "string") continue;
|
|
602
|
-
const version = versionForPackage(rel.tag, pkg);
|
|
644
|
+
const version = versionForPackage(rel.tag, pkg, singlePackage);
|
|
603
645
|
if (!version) continue;
|
|
604
646
|
if (!bodyReferences(rel.body, num)) continue;
|
|
605
647
|
if (firstVersion === undefined || cmpVersion(version, firstVersion) < 0) firstVersion = version;
|
|
@@ -615,9 +657,10 @@ export function matchCapability(match: ProbeMatch | undefined, releases: readonl
|
|
|
615
657
|
export function newestPublishedVersion(pkg: string | undefined, releases: readonly GithubRelease[]): string | undefined {
|
|
616
658
|
if (!pkg) return undefined;
|
|
617
659
|
let newest: string | undefined;
|
|
660
|
+
const singlePackage = repoIsSinglePackage(releases);
|
|
618
661
|
for (const rel of releases) {
|
|
619
662
|
if (!rel || typeof rel.tag !== "string") continue;
|
|
620
|
-
const version = versionForPackage(rel.tag, pkg);
|
|
663
|
+
const version = versionForPackage(rel.tag, pkg, singlePackage);
|
|
621
664
|
if (!version) continue;
|
|
622
665
|
if (newest === undefined || cmpVersion(version, newest) > 0) newest = version;
|
|
623
666
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.185.
|
|
3
|
+
"version": "0.185.1",
|
|
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",
|