@nanobpm/nano-workforce 0.195.1 → 0.195.2
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/convergeGate.test.ts +73 -0
- package/app/github.ts +78 -7
- package/package.json +1 -1
- package/resources/prompts/review-round.md +10 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [0.195.2](https://github.com/nanobpm/nano-workforce/compare/v0.195.1...v0.195.2) (2026-09-30)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* **converge-gate:** gate Copilot ccr-overview-v2 table findings ([#835](https://github.com/nanobpm/nano-workforce/issues/835)) ([#836](https://github.com/nanobpm/nano-workforce/issues/836)) ([8e3c0b6](https://github.com/nanobpm/nano-workforce/commit/8e3c0b690b7a212b0fcd4904b9e50ab7589e5bda))
|
|
6
|
+
|
|
1
7
|
## [0.195.1](https://github.com/nanobpm/nano-workforce/compare/v0.195.0...v0.195.1) (2026-09-30)
|
|
2
8
|
|
|
3
9
|
### Bug Fixes
|
package/app/convergeGate.test.ts
CHANGED
|
@@ -1123,3 +1123,76 @@ test("the blocked-comments escalation routes through gw-escalated toward an answ
|
|
|
1123
1123
|
// The human sees the gate's own reason (the unresolved threads / unacknowledged advisories).
|
|
1124
1124
|
assertStringIncludes(task[0], "convergeBlockReason");
|
|
1125
1125
|
});
|
|
1126
|
+
|
|
1127
|
+
// ── Copilot ccr-overview-v2 review format (issue #835) ──────────────────────
|
|
1128
|
+
// The v2 overview has no "Suppressed comments" block; findings without an inline thread live only in
|
|
1129
|
+
// the "What changed" file table as `… (<severity>, N vote[s])` clauses. Paths carry zero-width spaces.
|
|
1130
|
+
const V2_REVIEW_BODY = [
|
|
1131
|
+
"<!-- ccr-overview-v2 -->",
|
|
1132
|
+
"",
|
|
1133
|
+
"## Copilot review overview",
|
|
1134
|
+
"",
|
|
1135
|
+
"<details open>",
|
|
1136
|
+
"<summary><strong>Open (1)</strong></summary>",
|
|
1137
|
+
"",
|
|
1138
|
+
"- [Replace HEAD with a valid bootstrap commit SHA](#discussion_r1) · New",
|
|
1139
|
+
"</details>",
|
|
1140
|
+
"",
|
|
1141
|
+
"<details>",
|
|
1142
|
+
"<summary><strong>What changed in this PR</strong></summary>",
|
|
1143
|
+
"",
|
|
1144
|
+
"| File | Summary and final review notes |",
|
|
1145
|
+
"|---|---|",
|
|
1146
|
+
"| `skills/\u200bcamunda-forms/\u200bportability.json` | Adds dependency requirements. |",
|
|
1147
|
+
"| `skills/\u200bcamunda-job-workers/\u200bportability.json` | Adds dependency requirements; Camunda version conflicts with the documented Node fallback (moderate, 1 vote), and Java/TypeScript runtime paths are missing (moderate, 1 vote). |",
|
|
1148
|
+
"| `release-please-config.json` | Configures automated releases; `bootstrap-sha` must be removed or replaced with a full base commit SHA (moderate, 3 votes). |",
|
|
1149
|
+
"</details>",
|
|
1150
|
+
].join("\n");
|
|
1151
|
+
|
|
1152
|
+
test("parseSuppressedAdvisories: ccr-overview-v2 table findings are gated advisories (#835)", () => {
|
|
1153
|
+
const advisories = parseSuppressedAdvisories(V2_REVIEW_BODY);
|
|
1154
|
+
assertEquals(
|
|
1155
|
+
advisories.map((a) => [a.path, a.text]),
|
|
1156
|
+
[
|
|
1157
|
+
[
|
|
1158
|
+
"skills/camunda-job-workers/portability.json",
|
|
1159
|
+
"Adds dependency requirements; Camunda version conflicts with the documented Node fallback",
|
|
1160
|
+
],
|
|
1161
|
+
["skills/camunda-job-workers/portability.json", "Java/TypeScript runtime paths are missing"],
|
|
1162
|
+
[
|
|
1163
|
+
"release-please-config.json",
|
|
1164
|
+
"Configures automated releases; `bootstrap-sha` must be removed or replaced with a full base commit SHA",
|
|
1165
|
+
],
|
|
1166
|
+
],
|
|
1167
|
+
);
|
|
1168
|
+
// Keyed on path + prose (vote count excluded, so a re-vote next round keeps the same key).
|
|
1169
|
+
assertEquals(
|
|
1170
|
+
advisories[1].key,
|
|
1171
|
+
advisoryStableKey("skills/camunda-job-workers/portability.json", "Java/TypeScript runtime paths are missing"),
|
|
1172
|
+
);
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
test("parseSuppressedAdvisories: a v2 table finding is acked by the canonical nano-ack marker and unblocks the gate (#835)", () => {
|
|
1176
|
+
const advisories = parseSuppressedAdvisories(V2_REVIEW_BODY);
|
|
1177
|
+
const threads: ReviewThread[] = advisories.map((a) => ({
|
|
1178
|
+
isResolved: true,
|
|
1179
|
+
path: a.path,
|
|
1180
|
+
bodies: [`Declined. nano-ack: ${a.path} :: ${a.text}`],
|
|
1181
|
+
}));
|
|
1182
|
+
const blocked = evaluateConvergeGate({ unresolvedThreadCount: 0, suppressedAdvisories: advisories, acknowledgedKeys: [] });
|
|
1183
|
+
assertEquals(blocked.convergeBlocked, true);
|
|
1184
|
+
const r = evaluateConvergeGate({
|
|
1185
|
+
unresolvedThreadCount: 0,
|
|
1186
|
+
suppressedAdvisories: advisories,
|
|
1187
|
+
acknowledgedKeys: parseAckedAdvisories(threads),
|
|
1188
|
+
});
|
|
1189
|
+
assertEquals(r.convergeBlocked, false);
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
test("parseSuppressedAdvisories: v2 table rows without findings yield nothing; non-v2 tables are ignored (#835)", () => {
|
|
1193
|
+
assertEquals(
|
|
1194
|
+
parseSuppressedAdvisories("<!-- ccr-overview-v2 -->\n| `a.ts` | Adds a thing. |"),
|
|
1195
|
+
[],
|
|
1196
|
+
);
|
|
1197
|
+
assertEquals(parseSuppressedAdvisories("| `a.ts` | Broken thing (moderate, 1 vote). |"), []);
|
|
1198
|
+
});
|
package/app/github.ts
CHANGED
|
@@ -153,16 +153,31 @@ export interface ReviewThread {
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
/** A suppressed / low-confidence Copilot advisory parsed out of a review body. Its `key` is the
|
|
156
|
-
* line-stable identity (survives a line drift); `label` is the human-facing
|
|
157
|
-
*
|
|
156
|
+
* line-stable identity (survives a line drift); `label` is the human-facing locator shown in block
|
|
157
|
+
* reasons.
|
|
158
|
+
*
|
|
159
|
+
* Two source formats populate this shape, so the per-field shapes below vary by variant:
|
|
160
|
+
* - LEGACY "Suppressed comments" block (`parseLegacySuppressedBlock`): a `**path:line**` header
|
|
161
|
+
* with real `line` (> 0), `text` is the first non-empty prose line after the header, and `label`
|
|
162
|
+
* is `path:line`.
|
|
163
|
+
* - `ccr-overview-v2` file-table findings (`parseOverviewV2TableFindings`, issue #835): `text` is a
|
|
164
|
+
* finding clause from the table's notes column (no header, no source line), so `line` is the
|
|
165
|
+
* sentinel `0` and `label` is `<path> (review overview)` rather than `path:line`.
|
|
166
|
+
*
|
|
167
|
+
* Consumers must therefore treat `line === 0` as "not line-addressable" (an overview finding), NOT
|
|
168
|
+
* as a real line, and must not assume `label` is always `path:line`. */
|
|
158
169
|
export interface SuppressedAdvisory {
|
|
159
170
|
path: string;
|
|
171
|
+
/** Source line of the advisory. Real (> 0) for a legacy `**path:line**` header; the sentinel `0`
|
|
172
|
+
* for a `ccr-overview-v2` table finding, which carries no source line and is not line-addressable. */
|
|
160
173
|
line: number;
|
|
161
|
-
/** The advisory prose
|
|
174
|
+
/** The advisory prose: the first non-empty line after the header (legacy block) or a finding
|
|
175
|
+
* clause from the overview table's notes column (`ccr-overview-v2`). Used for the stable fingerprint. */
|
|
162
176
|
text: string;
|
|
163
177
|
/** Line-stable identity: `<path>#<fingerprint>` of the normalized prose. Survives line drift. */
|
|
164
178
|
key: string;
|
|
165
|
-
/** Human-facing
|
|
179
|
+
/** Human-facing label for block-reason messages: `path:line` for a legacy advisory, or
|
|
180
|
+
* `<path> (review overview)` for a line-less `ccr-overview-v2` table finding. */
|
|
166
181
|
label: string;
|
|
167
182
|
}
|
|
168
183
|
|
|
@@ -250,11 +265,67 @@ export function questionFingerprint(text: string): string {
|
|
|
250
265
|
return fingerprint(normalizeAdvisoryText(text));
|
|
251
266
|
}
|
|
252
267
|
|
|
253
|
-
/** Parse Copilot's suppressed / low-confidence advisories out of a review body
|
|
254
|
-
*
|
|
255
|
-
*
|
|
268
|
+
/** Parse Copilot's suppressed / low-confidence advisories out of a review body, supporting BOTH
|
|
269
|
+
* formats Copilot emits:
|
|
270
|
+
*
|
|
271
|
+
* 1. The legacy `<summary>Suppressed comments (N)</summary>` block — each advisory is a bold
|
|
272
|
+
* `**path:line**` header followed by the advisory prose (`parseLegacySuppressedBlock`).
|
|
273
|
+
* 2. The newer `ccr-overview-v2` review body (issue #835), which has NO "Suppressed comments"
|
|
274
|
+
* block — findings appear only as clauses in the "What changed" file table's notes column,
|
|
275
|
+
* each ending `(<severity>, N vote[s])` (`parseOverviewV2TableFindings`).
|
|
276
|
+
*
|
|
277
|
+
* Returns de-duplicated advisories from both formats (empty when neither is present). */
|
|
256
278
|
export function parseSuppressedAdvisories(reviewBody: string | null | undefined): SuppressedAdvisory[] {
|
|
257
279
|
const body = reviewBody ?? "";
|
|
280
|
+
const out: SuppressedAdvisory[] = [];
|
|
281
|
+
const seen = new Set<string>();
|
|
282
|
+
for (const a of [...parseLegacySuppressedBlock(body), ...parseOverviewV2TableFindings(body)]) {
|
|
283
|
+
if (seen.has(a.key)) continue;
|
|
284
|
+
seen.add(a.key);
|
|
285
|
+
out.push(a);
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** A finding clause's trailing `(<severity>, N vote[s])` tag in the ccr-overview-v2 file table. */
|
|
291
|
+
const V2_FINDING_TAG = /\s*\(\s*[a-z]+\s*,\s*\d+\s+votes?\s*\)/gi;
|
|
292
|
+
|
|
293
|
+
/** Copilot's `ccr-overview-v2` review body (issue #835) has NO "Suppressed comments" block. Findings
|
|
294
|
+
* that got an inline comment are threads (gated via `isResolved`), but others appear ONLY as clauses
|
|
295
|
+
* in the "What changed" file table's notes column, each ending `(<severity>, N vote[s])`, e.g.
|
|
296
|
+
* `Adds X; A is wrong (moderate, 1 vote), and B is missing (nit, 3 votes).` Each such clause is an
|
|
297
|
+
* advisory keyed on `<path> :: <clause>` — the vote tag is EXCLUDED so a re-vote next round keeps the
|
|
298
|
+
* key stable. The first clause keeps any leading row summary (`Adds X; A is wrong`): Copilot does not
|
|
299
|
+
* always emit a summary (`Maven is declared…; requirements should model…` is ONE finding), so cutting
|
|
300
|
+
* at `; ` would silently truncate a finding — keeping it is fail-CLOSED. Table findings carry
|
|
301
|
+
* no line, so `line` is 0. Findings that duplicate an inline thread are still returned: fail-CLOSED
|
|
302
|
+
* (an extra ack is cheap; a finding silently dropped is the false-OPEN this gate prevents). */
|
|
303
|
+
function parseOverviewV2TableFindings(body: string): SuppressedAdvisory[] {
|
|
304
|
+
if (!/<!--\s*ccr-overview-v\d+\s*-->/i.test(body)) return [];
|
|
305
|
+
const out: SuppressedAdvisory[] = [];
|
|
306
|
+
const rowRe = /^\|\s*`([^`]+)`\s*\|(.*)\|\s*$/;
|
|
307
|
+
for (const raw of body.split(/\r?\n/)) {
|
|
308
|
+
const row = rowRe.exec(raw);
|
|
309
|
+
if (!row) continue;
|
|
310
|
+
const path = row[1].replace(/\u200b|\u200c|\u200d|\ufeff/g, "").trim();
|
|
311
|
+
const notes = row[2].trim();
|
|
312
|
+
let prev = 0;
|
|
313
|
+
V2_FINDING_TAG.lastIndex = 0;
|
|
314
|
+
let m: RegExpExecArray | null;
|
|
315
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: canonical regex-exec accumulation loop
|
|
316
|
+
while ((m = V2_FINDING_TAG.exec(notes)) !== null) {
|
|
317
|
+
const seg = notes.slice(prev, m.index);
|
|
318
|
+
prev = m.index + m[0].length;
|
|
319
|
+
const text = seg.replace(/^[\s,;.]*(?:and\s+)?/i, "").trim();
|
|
320
|
+
if (!text) continue;
|
|
321
|
+
out.push({ path, line: 0, text, key: advisoryStableKey(path, text), label: `${path} (review overview)` });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** The legacy "Suppressed comments (N)" block: `**path:line**` headers followed by advisory prose. */
|
|
328
|
+
function parseLegacySuppressedBlock(body: string): SuppressedAdvisory[] {
|
|
258
329
|
const idx = body.search(/Suppressed comments\s*\(/i);
|
|
259
330
|
if (idx < 0) return [];
|
|
260
331
|
// Scan only from the "Suppressed comments" marker onward so a `**path:line**` elsewhere in the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.195.
|
|
3
|
+
"version": "0.195.2",
|
|
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",
|
|
@@ -71,6 +71,11 @@ Because several agents may run on the same host at once:
|
|
|
71
71
|
"low confidence" list Copilot folds into the **review body** (`.../reviews`
|
|
72
72
|
`body`). These are NOT in the default inline-comment API set, so a plain
|
|
73
73
|
`.../comments` read misses them; scan the review body for them explicitly.
|
|
74
|
+
Newer Copilot reviews use an **overview format** (body starts `<!-- ccr-overview-v2 -->`)
|
|
75
|
+
with no "Suppressed comments" block. There, findings without an inline thread appear
|
|
76
|
+
**only** in the "What changed in this PR" file table, as clauses in the notes column
|
|
77
|
+
ending `(<severity>, N vote[s])`. The gate treats **each such clause** as an advisory
|
|
78
|
+
(even if it duplicates an inline thread), so triage and ack every one (step 5a).
|
|
74
79
|
If `answer` is present, it is a human's instruction: either the decision on the
|
|
75
80
|
escalation you raised last round, or change requests from merge approval of this
|
|
76
81
|
converged PR. Act on it first, on this same PR branch (never open a new PR).
|
|
@@ -130,7 +135,11 @@ Because several agents may run on the same host at once:
|
|
|
130
135
|
new line — a line-based ack would go stale and the gate would escalate to a human
|
|
131
136
|
every round (issue #787). Because the ack is keyed on the prose, a decline you
|
|
132
137
|
made in an earlier round stays acknowledged across the drift and you need **not**
|
|
133
|
-
re-ack it. The ack thread may sit on any valid diff line.
|
|
138
|
+
re-ack it. The ack thread may sit on any valid diff line. For an **overview-format
|
|
139
|
+
table finding**, `<path>` is the file column (backticks and any invisible zero-width
|
|
140
|
+
characters removed) and `<advisory text>` is the finding clause **verbatim, without**
|
|
141
|
+
its trailing `(<severity>, N vote[s])` tag — for the row's first finding, include any
|
|
142
|
+
leading row summary before it (e.g. `nano-ack: a.json :: Adds X; A is wrong`). Example:
|
|
134
143
|
|
|
135
144
|
```sh
|
|
136
145
|
# Post the ack thread (pick any changed line in the diff for path/line). Use the PR's real HEAD
|