@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 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
@@ -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 `path:line` shown in
157
- * block reasons. */
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 (first non-empty line after the header), used for the stable fingerprint. */
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 `path:line` label for block-reason messages. */
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. Copilot renders them
254
- * under a `<summary>Suppressed comments (N)</summary>` block, each as a bold `**path:line**` header
255
- * followed by the advisory prose. Returns de-duplicated advisories (empty when there is no block). */
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.1",
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. Example:
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