forgedock 1.25.0 → 1.25.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.
@@ -825,13 +825,14 @@ function prefilterModel(argv) {
825
825
  }
826
826
 
827
827
  /**
828
- * `forgedock triage <issue...> [--lane L] [--repo owner/repo] [--dry-run] [--prefilter-model M]` (#4123)
828
+ * `forgedock triage <issue...> [--parallel K] [--lane L] [--repo owner/repo] [--dry-run] [--prefilter-model M]` (#4123)
829
+ * Issues are checked through a bounded pool of K (default 8, #4141); each lane is fetched once; rows print in input order.
829
830
  * Each issue is checked against its own lane (#4125); `--lane` is an assertion, a refused lane is reported and dropped.
830
831
  * Runs only the already-fixed pre-filter: closes issues it proves fixed (with evidence) and prints the issues that
831
832
  * still need a lane as the last line (`lanes: #1 #2`). `--dry-run` reports candidates without a model call or close.
832
833
  */
833
834
  export async function triageFromCli(argv, deps = {}) {
834
- const valued = ["--lane", "--repo", "--prefilter-model"];
835
+ const valued = ["--lane", "--repo", "--prefilter-model", "--parallel"];
835
836
  const issues = [];
836
837
  for (let i = 0; i < argv.length; i++) {
837
838
  if (valued.includes(argv[i])) { i++; continue; }
@@ -845,17 +846,36 @@ export async function triageFromCli(argv, deps = {}) {
845
846
  const leanIo = deps.leanIo ?? (await import("./lean/effects.mjs")).makeLeanIo({ repo });
846
847
  const { prefilterIssue, formatPrefilterSummary } = await import("./lean/prefilter.mjs");
847
848
  const branches = readBranches();
848
- const fetched = new Set();
849
- const rows = [];
850
- for (const n of [...new Set(issues)]) {
849
+ const parallel = flag(argv, "--parallel") == null ? 8 : parseInt(flag(argv, "--parallel"), 10);
850
+ if (!Number.isInteger(parallel) || parallel < 1) throw new Error("--parallel must be a positive integer");
851
+ // #4141: bounded pool. Each lane is fetched once (memoized promise); output stays in input order.
852
+ const fetched = new Map();
853
+ const fetchLane = (l) => {
854
+ if (!fetched.has(l)) fetched.set(l, leanIo.git(["fetch", "origin", l], { cwd: process.cwd() }).catch(() => { /* offline: use the local ref */ }));
855
+ return fetched.get(l);
856
+ };
857
+ const uniq = [...new Set(issues)];
858
+ const out = new Array(uniq.length);
859
+ const checkOne = async (n) => {
851
860
  // #4125: each issue's own lane; `--lane` is an assertion. A refused lane never reaches the pre-filter or a lane.
852
861
  const ln = await resolveLane({ issue: n, repo, io: leanIo, cwd: process.cwd(), branches, requested: lane, override: argv.includes("--lane-override") });
853
- if (!ln.ok) { console.log(`#${n} → lane-refused (${ln.code}: ${ln.detail})`); rows.push({ issue: n, outcome: "lane-refused", cost_usd: 0, prefilter: null }); continue; }
854
- if (!fetched.has(ln.lane)) { fetched.add(ln.lane); try { await leanIo.git(["fetch", "origin", ln.lane], { cwd: process.cwd() }); } catch { /* offline: use the local ref */ } }
862
+ if (!ln.ok) return { line: `#${n} → lane-refused (${ln.code}: ${ln.detail})`, row: { issue: n, outcome: "lane-refused", cost_usd: 0, prefilter: null } };
863
+ await fetchLane(ln.lane);
855
864
  const pf = await prefilterIssue({ issue: n, repo, lane: ln.lane, cwd: process.cwd(), io: leanIo, dryRun: argv.includes("--dry-run"), ...prefilterModel(argv) });
856
- console.log(`#${n} → ${pf.closed ? "closed" : pf.candidate ? "candidate" : "lane"} (${pf.detail})`);
857
- rows.push({ issue: n, outcome: pf.closed ? "invalid" : "pending", cost_usd: pf.cost_usd, prefilter: pf });
858
- }
865
+ return { line: `#${n} → ${pf.closed ? "closed" : pf.candidate ? "candidate" : "lane"} (${pf.detail})`, row: { issue: n, outcome: pf.closed ? "invalid" : "pending", cost_usd: pf.cost_usd, prefilter: pf } };
866
+ };
867
+ let next = 0, printed = 0;
868
+ const flush = () => { while (printed < out.length && out[printed]) console.log(out[printed++].line); };
869
+ await Promise.all(Array.from({ length: Math.min(parallel, uniq.length) }, async () => {
870
+ while (next < uniq.length) {
871
+ const i = next++;
872
+ try { out[i] = await checkOne(uniq[i]); } catch (e) {
873
+ out[i] = { line: `#${uniq[i]} → lane (pre-filter error: ${String(e?.message ?? e).slice(0, 120)})`, row: { issue: uniq[i], outcome: "pending", cost_usd: 0, prefilter: { closed: false, candidate: false, detail: "pre-filter error", cost_usd: 0 } } };
874
+ }
875
+ flush();
876
+ }
877
+ }));
878
+ const rows = out.map((r) => r.row);
859
879
  const lanes = rows.filter((r) => r.prefilter && !r.prefilter.closed).map((r) => r.issue);
860
880
  console.log(formatPrefilterSummary(rows));
861
881
  console.log(`lanes: ${lanes.map((n) => `#${n}`).join(" ") || "(none)"}`);
@@ -194,6 +194,24 @@ describe("orchestrate with the pre-filter (#4123)", () => {
194
194
  await assert.rejects(triageFromCli([], {}), /usage/);
195
195
  });
196
196
 
197
+ it("#4141 triage runs a bounded parallel pool, fetches each lane once, and prints rows in input order", async () => {
198
+ const issues = {}; for (let n = 1; n <= 6; n++) issues[n] = { body: `b${n}` };
199
+ const w = world({ issues, prs: [], verdict: { verdict: "VALID", evidence: [] } });
200
+ let live = 0, peak = 0, fetches = 0;
201
+ const gh0 = w.io.gh, git0 = w.io.git;
202
+ w.io.gh = async (a, o) => { live++; peak = Math.max(peak, live); await new Promise((r) => setTimeout(r, 5 + (Number(a.find((x) => /^\d+$/.test(x))) || 0) % 3)); live--; return gh0(a, o); };
203
+ w.io.git = async (a, o) => { if (a[0] === "fetch") fetches++; return git0(a, o); };
204
+ const logs = []; const l = console.log; console.log = (s) => logs.push(String(s));
205
+ let res;
206
+ try { res = await triageFromCli(["1", "2", "3", "4", "5", "6", "--repo", "o/r", "--lane", "staging", "--parallel", "3"], { leanIo: w.io }); } finally { console.log = l; }
207
+ const order = logs.filter((s) => /^#\d+ → /.test(s)).map((s) => Number(s.slice(1, s.indexOf(" "))));
208
+ assert.deepEqual(order, [1, 2, 3, 4, 5, 6]);
209
+ assert.ok(peak > 1 && peak <= 3, `peak ${peak}`);
210
+ assert.equal(fetches, 1);
211
+ assert.deepEqual(res.rows.map((r) => r.issue), [1, 2, 3, 4, 5, 6]);
212
+ await assert.rejects(triageFromCli(["1", "--repo", "o/r", "--parallel", "0"], { leanIo: w.io }), /--parallel/);
213
+ });
214
+
197
215
  it("#4125 triage checks each issue against its own lane: a refused lane (--lane main, missing milestone branch) is reported and dropped", async () => {
198
216
  const w = world({ issues: { 12: { body: "x" }, 13: { body: "y", milestone: { title: "Later" } } } });
199
217
  const logs = []; const l = console.log; console.log = (s) => logs.push(String(s));
@@ -33,12 +33,14 @@ Do this yourself from issue text and the code. Spawn no sub-agents here.
33
33
  for the whole set:
34
34
 
35
35
  ```bash
36
- forgedock triage <N...>
36
+ forgedock triage <N...> --parallel <K>
37
37
  ```
38
38
 
39
39
  It closes, with the evidence on the issue and no lane, every issue that a merged PR already fixed (one
40
40
  short read-only validation on a small model) or an auto-filed CI failure whose workflow is green again
41
- (the green run link). Its last line is `lanes: #A #B ...`: continue with only those issues. Keep its
41
+ (the green run link). It checks K issues at a time. Its last line is `lanes: #A #B ...`: continue with only
42
+ those issues. This is the only pre-filter in this command: lanes run `forgedock run-issue`, which does not
43
+ pre-filter again. Keep its
42
44
  `pre-filter:` line for the report.
43
45
  3. **Triage.** For each issue, read it once (`gh issue view N --json title,body,labels`) and record:
44
46
  - **Ready / skip.** Skip when it is labelled `needs-human` or `workflow:merged`, or an open PR already
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgedock",
3
- "version": "1.25.0",
3
+ "version": "1.25.1",
4
4
  "description": "Autonomous development pipeline for coding agents. GitHub as institutional memory, with native Claude Code and OpenCode entrypoints.",
5
5
  "bin": {
6
6
  "forgedock": "bin/forgedock.mjs"