@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 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
@@ -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
- /** The `<version>` of a release tagged exactly `<package>@<version>` (numeric-dotted), or undefined
531
- * when the tag belongs to another package or is not a version tag. Per-package scoping is enforced
532
- * here: a sibling package's provenance can never leak into this package's resolution. */
533
- function versionForPackage(tag: string, pkg: string): string | undefined {
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 (!tag.startsWith(prefix)) return undefined;
536
- const v = tag.slice(prefix.length).trim();
537
- return /^\d+(\.\d+)*$/.test(v) ? v : undefined;
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 tagged `<match.package>@<version>` (newest first) and,
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 observed = summariseCapabilityCandidates(match, releases);
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.0",
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",