forgedock 1.22.0 → 1.23.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.
@@ -44,6 +44,13 @@ export const OUTCOME_CODES = Object.freeze({
44
44
  DEPENDENCY_UNREADABLE: "dependency.unreadable",
45
45
  INVALID: "invalid",
46
46
  DECOMPOSED: "decomposed",
47
+ // Lean core work-on (bin/lean) needs-human causes.
48
+ BUILDER_BLOCKED: "builder.blocked",
49
+ BUILDER_NO_EVIDENCE: "builder.no_evidence",
50
+ BUILDER_NO_COMMIT: "builder.no_commit",
51
+ REVIEW_FAILED: "review.failed",
52
+ REVIEW_NO_VERDICT: "review.no_verdict",
53
+ MERGE_HEAD_MISMATCH: "merge.head_mismatch",
47
54
  // Non-failure terminals, so a RUN_TERMINAL for a healthy run is not counted as a gap.
48
55
  COMPLETED: "completed",
49
56
  AWAITING_MERGE: "awaiting_merge",
@@ -115,7 +122,9 @@ export function codeForTerminalReason(reason) {
115
122
  case "phase-complete":
116
123
  case "investigation":
117
124
  return OUTCOME_CODES.COMPLETED;
118
- case "awaiting-merge": return OUTCOME_CODES.AWAITING_MERGE;
125
+ case "awaiting-merge":
126
+ case "ci-pending":
127
+ return OUTCOME_CODES.AWAITING_MERGE;
119
128
  case "invalid": return OUTCOME_CODES.INVALID;
120
129
  case "decomposed": return OUTCOME_CODES.DECOMPOSED;
121
130
  case "engine-error": return OUTCOME_CODES.ENGINE_ERROR;
package/bin/lean/lean.mjs CHANGED
@@ -76,9 +76,9 @@ export async function runCore(o) {
76
76
  }
77
77
 
78
78
  let done = null, wtCreated = false;
79
- async function finish(reason, detail = "", extra = {}) {
79
+ async function finish(reason, detail = "", extra = {}, codeOverride = null) {
80
80
  if (done) return done; // idempotent: exactly one RUN_TERMINAL and one telemetry emit
81
- const code = codeForTerminalReason(reason);
81
+ const code = codeOverride ?? codeForTerminalReason(reason);
82
82
  ev({ event: "RUN_TERMINAL", reason, code });
83
83
  await emit({ event: "RUN_TERMINAL", reason, code });
84
84
  if (["needs-human", "engine-error"].includes(reason)) {
@@ -93,12 +93,12 @@ export async function runCore(o) {
93
93
  done = { terminalReason: reason, detail, branch, worktree: wt, cost_usd: usage.cost_usd, tokens: { input: usage.input, output: usage.output, cache: usage.cache }, wall_ms: now() - t0, ...extra };
94
94
  return done;
95
95
  }
96
- async function needsHuman(why) {
96
+ async function needsHuman(why, code = OUTCOME_CODES.ENGINE_ERROR) {
97
97
  try {
98
98
  await post("FORGE:NEEDS_HUMAN", `## Needs human\n${why}\n\n_${usageLine()}_`);
99
99
  await ghc(["issue", "edit", String(issue), "-R", repo, "--add-label", "needs-human"]);
100
100
  } catch { /* best effort */ }
101
- return finish("needs-human", why);
101
+ return finish("needs-human", why, { code }, code);
102
102
  }
103
103
 
104
104
  async function builder(n, prompt, resume) {
@@ -178,7 +178,7 @@ export async function runCore(o) {
178
178
  const evidence = (Array.isArray(result?.evidence) ? result.evidence : []).filter((x) => typeof x === "string" && x.trim());
179
179
  const verdict = result ? (result.status === "fixed" ? "valid" : result.status) : null;
180
180
  const closing = ["invalid", "already-fixed"].includes(result?.status);
181
- if (closing && !evidence.length) return await needsHuman(`Builder reported "${result.status}" without evidence, so the issue is not closed automatically.\n\n${result.summary ?? "(no summary)"}`);
181
+ if (closing && !evidence.length) return await needsHuman(`Builder reported "${result.status}" without evidence, so the issue is not closed automatically.\n\n${result.summary ?? "(no summary)"}`, OUTCOME_CODES.BUILDER_NO_EVIDENCE);
182
182
  // The validation comment (no model call): verdict, evidence, one-line summary. For invalid/already-fixed it IS the evidence comment.
183
183
  if (verdict) validateUrl = await post("FORGE:VALIDATE", renderValidate(result, verdict));
184
184
  if (closing) {
@@ -186,8 +186,8 @@ export async function runCore(o) {
186
186
  await label("invalid");
187
187
  return await finish("invalid", result.summary ?? result.status);
188
188
  }
189
- if (result?.status === "blocked") return await needsHuman(`Builder is blocked: ${result.summary ?? "(no summary)"}`);
190
- if (!files0.length) return await needsHuman(`Builder made no commit${result ? ` (status "${result.status}")` : " and wrote no result.json"}.\n\n${result?.summary ?? ""}`);
189
+ if (result?.status === "blocked") return await needsHuman(`Builder is blocked: ${result.summary ?? "(no summary)"}`, OUTCOME_CODES.BUILDER_BLOCKED);
190
+ if (!files0.length) return await needsHuman(`Builder made no commit${result ? ` (status "${result.status}")` : " and wrote no result.json"}.\n\n${result?.summary ?? ""}`, OUTCOME_CODES.BUILDER_NO_COMMIT);
191
191
  planUrl = await post("FORGE:PLAN", renderPlan(result, files0));
192
192
  }
193
193
 
@@ -208,14 +208,14 @@ export async function runCore(o) {
208
208
  return null;
209
209
  };
210
210
  review = await doReview();
211
- if (!review) return await needsHuman("Reviewer returned no parseable verdict twice.");
211
+ if (!review) return await needsHuman("Reviewer returned no parseable verdict twice.", OUTCOME_CODES.REVIEW_NO_VERDICT);
212
212
  if (review.fail) {
213
- if (!sessionId) return await needsHuman(`Review FAIL and the builder session id is unknown, cannot resume: ${review.reason}`);
213
+ if (!sessionId) return await needsHuman(`Review FAIL and the builder session id is unknown, cannot resume: ${review.reason}`, OUTCOME_CODES.REVIEW_FAILED);
214
214
  await builder(++attemptN, resumeBuilderPrompt(review.reason), sessionId);
215
215
  result = (await readResult()) ?? result;
216
216
  review = await doReview();
217
- if (!review) return await needsHuman("Reviewer returned no parseable verdict twice (re-review).");
218
- if (review.fail) return await needsHuman(`Review FAIL twice. Last reason: ${review.reason}`);
217
+ if (!review) return await needsHuman("Reviewer returned no parseable verdict twice (re-review).", OUTCOME_CODES.REVIEW_NO_VERDICT);
218
+ if (review.fail) return await needsHuman(`Review FAIL twice. Last reason: ${review.reason}`, OUTCOME_CODES.REVIEW_FAILED);
219
219
  }
220
220
  reviewUrl = await post("FORGE:REVIEW", renderReview(review));
221
221
  ev({ event: "PHASE_COMMIT", phase: "review" });
@@ -251,13 +251,13 @@ export async function runCore(o) {
251
251
  if (policy === "all") {
252
252
  const w = await io.waitCiGreen(pr, repo, 0).catch(() => ({ rc: 2, text: "" }));
253
253
  if (w.rc === 0) { gateHead = /CI_GATE_HEAD:\s*([0-9a-f]{7,40})/.exec(w.text ?? "")?.[1] ?? null; break; }
254
- if (w.rc === 1) return await needsHuman(`Remote CI failed on PR #${pr}:\n${String(w.text).slice(-2000)}`);
254
+ if (w.rc === 1) return await needsHuman(`Remote CI failed on PR #${pr}:\n${String(w.text).slice(-2000)}`, OUTCOME_CODES.CI_RED);
255
255
  } else {
256
256
  let states = [];
257
257
  try { states = (await io.checkStates(pr)) ?? []; } catch { states = []; }
258
258
  const st = (n) => states.find((x) => x.name === n)?.state ?? "pending";
259
259
  const failed = required.filter((n) => st(n) === "failure");
260
- if (failed.length) return await needsHuman(`Required check(s) failed on PR #${pr}: ${failed.join(", ")}`);
260
+ if (failed.length) return await needsHuman(`Required check(s) failed on PR #${pr}: ${failed.join(", ")}`, OUTCOME_CODES.CI_RED);
261
261
  if (required.every((n) => st(n) === "success")) break;
262
262
  }
263
263
  if (now() - startedAt >= ciMaxMinutes * 60000) {
@@ -269,7 +269,7 @@ export async function runCore(o) {
269
269
  await sleep(ciPollDelayMs(pollIndex++, random));
270
270
  }
271
271
  if (gateHead && !(localHead.startsWith(gateHead) || gateHead.startsWith(localHead))) {
272
- return await needsHuman(`CI passed for ${gateHead.slice(0, 12)} but the reviewed head is ${localHead.slice(0, 12)}; refusing to merge an unreviewed head.`);
272
+ return await needsHuman(`CI passed for ${gateHead.slice(0, 12)} but the reviewed head is ${localHead.slice(0, 12)}; refusing to merge an unreviewed head.`, OUTCOME_CODES.MERGE_HEAD_MISMATCH);
273
273
  }
274
274
  }
275
275
  await ghc(["pr", "merge", String(pr), "-R", repo, "--merge", "--match-head-commit", localHead]);
@@ -194,6 +194,20 @@ describe("core work-on", () => {
194
194
  assert.equal((await run(world({ changed: [] }))).terminalReason, "needs-human");
195
195
  });
196
196
 
197
+ it("needs-human terminals carry a specific outcome code, never unclassified", async () => {
198
+ const code = async (w, extra) => {
199
+ const dir = tmpDir("lean-code-");
200
+ const r = await run(w, { dir, ...extra });
201
+ const term = readLog(dir, 7).filter((e) => e.event === "RUN_TERMINAL");
202
+ assert.equal(term.length, 1);
203
+ assert.equal(term[0].code, r.code);
204
+ return term[0].code;
205
+ };
206
+ assert.equal(await code(world({ result: { status: "blocked", summary: "need creds" } })), "builder.blocked");
207
+ assert.equal(await code(world({ changed: [] })), "builder.no_commit");
208
+ assert.equal(await code(world({ waits: [{ rc: 1, text: "lint failed" }] }), { mergeWait: "all" }), "ci.red");
209
+ });
210
+
197
211
  it("red remote CI is needs-human; slow CI polls with backoff and ends ci-pending, never needs-human", async () => {
198
212
  const w = world({ waits: [{ rc: 1, text: "lint failed" }] });
199
213
  assert.equal((await run(w, { mergeWait: "all" })).terminalReason, "needs-human");
@@ -0,0 +1,88 @@
1
+ ---
2
+ description: Legacy full orchestrator (triage, investigation wave, synthesis, DAG) — reached via /orchestrate --path full
3
+ argument-hint: "[milestone <slug> | #1 #2 #3 | next <N> | fast-lane | priority:P0] [--auto|--confirm]"
4
+ user-invocable: false
5
+ install: internal
6
+ ---
7
+ <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
8
+ <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
9
+
10
+ # /orchestrate — Multi-Issue Parallel Orchestrator
11
+
12
+ **Input**: $ARGUMENTS
13
+
14
+ `--auto` and `--confirm` authorize the dispatch checkpoint; they are control
15
+ flags, not part of the issue-set query. `--deep-plan` requests the full analysis
16
+ path, and `--max-concurrent N` only tunes the dispatch cap.
17
+
18
+ This file is the slim dispatcher. Detailed phase content lives in `commands/orchestrate/`.
19
+
20
+ ## OpenCode Preflight
21
+
22
+ When `FORGE_RUNTIME=opencode` (or an OpenCode runtime marker is present), run the
23
+ deterministic preflight at `$FORGE_HOME/bin/orchestrate-preflight.mjs` immediately,
24
+ before reading `orchestrate/config.md`, this workflow, or any phase spec. The helper
25
+ resolves the repository from `forge.yaml`, including a parent config when the target
26
+ is a nested Git worktree:
27
+
28
+ ```bash
29
+ node "$FORGE_HOME/bin/orchestrate-preflight.mjs" \
30
+ --repo "$GH_REPO" \
31
+ --args "$ARGUMENTS"
32
+ ```
33
+
34
+ The preflight is a compact mechanical adapter for issue resolution, eligibility,
35
+ explicit dependencies, scoped issue-body file overlap, database serialization, and
36
+ the initial ready queue. If it returns a supported plan with `requiresDeepPlan: false`
37
+ and `confirmed: true`, launch `dispatchNow` with native background `task` calls
38
+ immediately. Without an explicit `--auto` or `--confirm` argument, present the
39
+ compact plan and ask for one confirmation; after the user confirms, launch the
40
+ plan's ready queue without re-reading the large phase files. Do not load the full
41
+ Phase 3 or Phase 4 prose just to ask that question.
42
+
43
+ Continue through the phase files when the plan says `requiresDeepPlan`, the input is
44
+ unsupported, preflight fails, or a task-result event requires recovery. This adapter
45
+ never closes, deduplicates, or edits issues; the full shared workflow remains the
46
+ authority for investigations, review-finding cascade handling, recovery, cleanup,
47
+ and reporting.
48
+
49
+ For a supported compact OpenCode plan, stop after the fast-path dispatch. The phase
50
+ execution order below is the fallback path for deep plans, unsupported inputs,
51
+ preflight failures, and task-result recovery; do not read it merely to confirm or
52
+ dispatch a compact plan.
53
+
54
+ ## Execution Order
55
+
56
+ Read and execute phases in sequence. Each phase file is self-contained. Resolve every path from the spec root defined in the Quick Reference below.
57
+
58
+ | Step | File | Description |
59
+ |------|------|-------------|
60
+ | 0 | `orchestrate/config.md` | Hard rules, config resolution, multi-repo support — READ FIRST |
61
+ | 1 | `orchestrate/phase-1-resolve.md` | Resolve the issue set from input |
62
+ | 2 | `orchestrate/phase-2-triage.md` | Investigation-first triage, Wave 0 |
63
+ | 2.5 | `orchestrate/phase-2.5-synthesis.md` | Investigation synthesis and deconfliction |
64
+ | 3 | `orchestrate/phase-3-dependency.md` | Dependency analysis, DAG construction, execution plan |
65
+ | 4 | `orchestrate/phase-4-execution.md` | Streaming DAG execution, agent dispatch, stall detection |
66
+ | 5 | `orchestrate/phase-5-cleanup.md` | Post-batch cleanup sweep and agent audit |
67
+ | 6 | `orchestrate/phase-6-report.md` | Consolidated report and pipeline summary |
68
+ | — | `orchestrate/safety.md` | Safety rules and examples (reference) |
69
+
70
+ ## Quick Reference
71
+
72
+ **Spec root (MANDATORY)**: read every file below from `${CLAUDE_PLUGIN_ROOT}` — the install root of the ForgeDock plugin that is running this command (Claude Code fills it in when it loads the spec). Only if that path does not start with `/` (not a Claude Code plugin session: install.sh, Codex, OpenCode) use `$FORGE_HOME` instead. Never read sub-files from `$FORGE_HOME` when the plugin root resolved: an exported `FORGE_HOME` can point at a different, older ForgeDock checkout, and mixing roots runs stale phase specs.
73
+
74
+ **Raw reads are not substituted (MANDATORY)**: Claude Code fills in the plugin root only in the spec it loads (this file), never in files you open with Read. The phase files contain the placeholder written as `$` immediately followed by `{CLAUDE_PLUGIN_ROOT}`. Before running any bash block taken from a phase file, replace every occurrence of that placeholder with `${CLAUDE_PLUGIN_ROOT}` (the resolved root shown here), exactly as Claude Code would have. Left unreplaced it is rejected as a non-absolute path and resolution falls back to `$FORGE_HOME`, which may be stale.
75
+
76
+ ```
77
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/config.md # ALWAYS READ FIRST
78
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/phase-1-resolve.md
79
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/phase-2-triage.md
80
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/phase-2.5-synthesis.md
81
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/phase-3-dependency.md
82
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/phase-4-execution.md
83
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/phase-5-cleanup.md
84
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/phase-6-report.md
85
+ ```
86
+
87
+ The orchestrator reads only the phase file(s) relevant to the current step rather than
88
+ loading the full 2300-line monolith upfront.
@@ -1,86 +1,85 @@
1
1
  ---
2
- description: Orchestrate parallel work on multiple issues or an entire milestone — spawns sub-agents that each run the full /work-on pipeline
3
- argument-hint: "[milestone <slug> | #1 #2 #3 | next <N> | fast-lane | priority:P0] [--auto|--confirm]"
2
+ description: Orchestrate several issues in parallel — launches one lean work-on lane per issue (background shell or agent), GitHub is the register
3
+ argument-hint: "<#1 #2 #3 | milestone <slug> | label <name>> [--parallel K] [--lane L] [--path full]"
4
4
  ---
5
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
6
6
  <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
7
7
 
8
- # /orchestrate — Multi-Issue Parallel Orchestrator
8
+ # /orchestrate — parallel lean work-on lanes
9
9
 
10
10
  **Input**: $ARGUMENTS
11
11
 
12
- `--auto` and `--confirm` authorize the dispatch checkpoint; they are control
13
- flags, not part of the issue-set query. `--deep-plan` requests the full analysis
14
- path, and `--max-concurrent N` only tunes the dispatch cap.
15
-
16
- This file is the slim dispatcher. Detailed phase content lives in `commands/orchestrate/`.
17
-
18
- ## OpenCode Preflight
19
-
20
- When `FORGE_RUNTIME=opencode` (or an OpenCode runtime marker is present), run the
21
- deterministic preflight at `$FORGE_HOME/bin/orchestrate-preflight.mjs` immediately,
22
- before reading `orchestrate/config.md`, this workflow, or any phase spec. The helper
23
- resolves the repository from `forge.yaml`, including a parent config when the target
24
- is a nested Git worktree:
25
-
26
- ```bash
27
- node "$FORGE_HOME/bin/orchestrate-preflight.mjs" \
28
- --repo "$GH_REPO" \
29
- --args "$ARGUMENTS"
30
- ```
31
-
32
- The preflight is a compact mechanical adapter for issue resolution, eligibility,
33
- explicit dependencies, scoped issue-body file overlap, database serialization, and
34
- the initial ready queue. If it returns a supported plan with `requiresDeepPlan: false`
35
- and `confirmed: true`, launch `dispatchNow` with native background `task` calls
36
- immediately. Without an explicit `--auto` or `--confirm` argument, present the
37
- compact plan and ask for one confirmation; after the user confirms, launch the
38
- plan's ready queue without re-reading the large phase files. Do not load the full
39
- Phase 3 or Phase 4 prose just to ask that question.
40
-
41
- Continue through the phase files when the plan says `requiresDeepPlan`, the input is
42
- unsupported, preflight fails, or a task-result event requires recovery. This adapter
43
- never closes, deduplicates, or edits issues; the full shared workflow remains the
44
- authority for investigations, review-finding cascade handling, recovery, cleanup,
45
- and reporting.
46
-
47
- For a supported compact OpenCode plan, stop after the fast-path dispatch. The phase
48
- execution order below is the fallback path for deep plans, unsupported inputs,
49
- preflight failures, and task-result recovery; do not read it merely to confirm or
50
- dispatch a compact plan.
51
-
52
- ## Execution Order
53
-
54
- Read and execute phases in sequence. Each phase file is self-contained. Resolve every path from the spec root defined in the Quick Reference below.
55
-
56
- | Step | File | Description |
57
- |------|------|-------------|
58
- | 0 | `orchestrate/config.md` | Hard rules, config resolution, multi-repo support — READ FIRST |
59
- | 1 | `orchestrate/phase-1-resolve.md` | Resolve the issue set from input |
60
- | 2 | `orchestrate/phase-2-triage.md` | Investigation-first triage, Wave 0 |
61
- | 2.5 | `orchestrate/phase-2.5-synthesis.md` | Investigation synthesis and deconfliction |
62
- | 3 | `orchestrate/phase-3-dependency.md` | Dependency analysis, DAG construction, execution plan |
63
- | 4 | `orchestrate/phase-4-execution.md` | Streaming DAG execution, agent dispatch, stall detection |
64
- | 5 | `orchestrate/phase-5-cleanup.md` | Post-batch cleanup sweep and agent audit |
65
- | 6 | `orchestrate/phase-6-report.md` | Consolidated report and pipeline summary |
66
- | — | `orchestrate/safety.md` | Safety rules and examples (reference) |
67
-
68
- ## Quick Reference
69
-
70
- **Spec root (MANDATORY)**: read every file below from `${CLAUDE_PLUGIN_ROOT}` — the install root of the ForgeDock plugin that is running this command (Claude Code fills it in when it loads the spec). Only if that path does not start with `/` (not a Claude Code plugin session: install.sh, Codex, OpenCode) use `$FORGE_HOME` instead. Never read sub-files from `$FORGE_HOME` when the plugin root resolved: an exported `FORGE_HOME` can point at a different, older ForgeDock checkout, and mixing roots runs stale phase specs.
71
-
72
- **Raw reads are not substituted (MANDATORY)**: Claude Code fills in the plugin root only in the spec it loads (this file), never in files you open with Read. The phase files contain the placeholder written as `$` immediately followed by `{CLAUDE_PLUGIN_ROOT}`. Before running any bash block taken from a phase file, replace every occurrence of that placeholder with `${CLAUDE_PLUGIN_ROOT}` (the resolved root shown here), exactly as Claude Code would have. Left unreplaced it is rejected as a non-absolute path and resolution falls back to `$FORGE_HOME`, which may be stale.
73
-
74
- ```
75
- Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/config.md # ALWAYS READ FIRST
76
- Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/phase-1-resolve.md
77
- Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/phase-2-triage.md
78
- Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/phase-2.5-synthesis.md
79
- Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/phase-3-dependency.md
80
- Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/phase-4-execution.md
81
- Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/phase-5-cleanup.md
82
- Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/phase-6-report.md
83
- ```
84
-
85
- The orchestrator reads only the phase file(s) relevant to the current step rather than
86
- loading the full 2300-line monolith upfront.
12
+ You are the orchestrator. Resolve the issues, order them, and run each one in its own lane. Each lane
13
+ runs the lean work-on (validate → plan → implement → fix/regression review → PR → merge), which records
14
+ every step on the issue. You triage and order the set yourself (no sub-agent investigations), but never implement an
15
+ issue yourself.
16
+
17
+ ## 1. Legacy path
18
+
19
+ If the input contains `--path full`, stop: read `${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/legacy.md`
20
+ and follow it instead (the previous multi-phase orchestrator).
21
+
22
+ ## 2. Resolve, triage and build the DAG
23
+
24
+ Do this yourself from issue text and the code. Spawn no sub-agents here.
25
+
26
+ 1. **Resolve.**
27
+ - Explicit numbers (`#12 13 14`) are used as given.
28
+ - `milestone <slug>` uses `gh issue list --milestone "<slug>" --state open --json number --jq '.[].number'`.
29
+ - `label <name>` uses the same with `--label`.
30
+
31
+ Keep only plain integers and drop closed issues.
32
+ 2. **Triage.** For each issue, read it once (`gh issue view N --json title,body,labels`) and record:
33
+ - **Ready / skip.** Skip when it is labelled `needs-human` or `workflow:merged`, or an open PR already
34
+ closes it (`gh pr list --search "Closes #N" --state open`). Say why.
35
+ - **Files.** The files or areas it will likely touch: paths named in the issue, or a quick
36
+ `git grep` of the symbols it names. Keep this to a few greps per issue, not an investigation.
37
+ - **Size.** S, M or L, from the scope the issue describes.
38
+ 3. **Build the DAG.** Edge X → N means N waits until X merges:
39
+ - **Explicit:** `Depends on #X` / `Blocked by #X` where X is in the set. An open X outside the set
40
+ means N is skipped as blocked.
41
+ - **Conflict:** two issues that will edit the same file or the same function run one after the
42
+ other (smaller first), so their PRs don't conflict. Different files run in parallel.
43
+ - **Logical:** one issue's fix is needed by another (for example, it adds the helper the other uses).
44
+
45
+ If you find a cycle, break it at the weakest (conflict) edge and say so.
46
+ 4. **Show the plan** as one table: issue, title, size, likely files, waits-on. There are no waves. Then proceed
47
+ without asking.
48
+
49
+ `K` = `--parallel` (default 3). `L` = `--lane` if given, otherwise the repo's base lane from forge.yaml.
50
+
51
+ ## 3. Launch lanes
52
+
53
+ Schedule from the DAG, not in waves. An issue is **ready** when every issue it waits on has merged (or it
54
+ waits on nothing). Keep at most K lanes running. Whenever a lane is free, start the highest-priority ready
55
+ issue right away (priority label first, then the smaller size). Never hold a ready issue back until other
56
+ lanes finish.
57
+
58
+ - **Shell lane (default, when `forgedock` is on PATH).** One `Bash` call per issue with
59
+ `run_in_background: true`:
60
+
61
+ ```bash
62
+ forgedock run-issue <N> --lane <L>
63
+ ```
64
+
65
+ The harness notifies you when the lane exits. Its last line is `issue #N → <outcome>`.
66
+ - **Agent lane (only when the `forgedock` CLI is unavailable).** Spawn one background agent per issue.
67
+ The prompt is:
68
+
69
+ > Resolve issue #N (gh issue view N --comments). If already fixed or invalid, comment the file:line
70
+ > evidence and stop. Otherwise fix it with a regression test, run the relevant tests, commit on a
71
+ > branch, push, open a PR with Closes #N, and comment a summary (root cause, changes, tests, PR) on
72
+ > the issue.
73
+
74
+ The agent does not merge.
75
+
76
+ When a lane finishes:
77
+ - `merged` or `invalid`: start the next ready issue.
78
+ - `needs-human`: read the issue's last comment for the reason, and mark its dependents skipped.
79
+ - `engine-error`: relaunch it once.
80
+ - `ci-pending` or `deferred`: leave it. A later `/orchestrate` resumes it.
81
+
82
+ ## 4. Report
83
+
84
+ When all lanes have finished, print one table: issue, outcome, PR, minutes. Then list each
85
+ `needs-human` or skipped issue with its one-line reason. Nothing else.
@@ -43,6 +43,12 @@ An absent or unknown code is recorded as `unclassified`, with the raw reason kep
43
43
  | `dependency.unreadable` | The issue body or a declared dependency could not be read, so the run was refused (fail closed) | engine | Failed or unparsable `gh api` read |
44
44
  | `invalid` | Issue is invalid or in a divergent closed state | investigate, engine | `INVESTIGATION:INVALID` marker, or live snapshot |
45
45
  | `decomposed` | Issue was decomposed into sub-issues | investigate, decompose | `FORGE:DECOMPOSED` or the decomposition assessment |
46
+ | `builder.blocked` | Core work-on: the builder reported it cannot proceed without a human decision (design, credentials, infra) | lean | `result.json` status `blocked` |
47
+ | `builder.no_evidence` | Core work-on: the builder reported invalid/already-fixed without file:line evidence, so the issue was not closed | lean | `result.json` with empty `evidence` |
48
+ | `builder.no_commit` | Core work-on: the builder made no commit for a fix | lean | No changed files after the builder session |
49
+ | `review.failed` | Core work-on: the fix/regression review failed twice (or could not resume the builder) | lean | Reviewer verdict FAIL |
50
+ | `review.no_verdict` | Core work-on: the reviewer returned no parseable verdict twice | lean | Unparsable reviewer output |
51
+ | `merge.head_mismatch` | Core work-on: CI passed for a head other than the reviewed head; merge refused | lean | Gate head differs from the reviewed head |
46
52
  | `completed` | Non-failure terminal (merged, phase-complete, investigation) | engine | Terminal reason |
47
53
  | `awaiting_merge` | Clean result held for a merge decision | remediate | Terminal reason `awaiting-merge` |
48
54
  | `unclassified` | Fallback for an absent or unknown code | engine | The raw reason is kept alongside |
@@ -155,20 +155,17 @@ Deterministically verifies a staging→main bundle's acceptance criteria against
155
155
 
156
156
  ### `/orchestrate`
157
157
 
158
- **Parallel multi-issue pipeline.**
158
+ **Parallel lean work-on lanes.**
159
159
 
160
- Spawns sub-agents that each run the full `/work-on` pipeline in parallel.
160
+ Resolves the issues, triages them and builds a dependency DAG (explicit `Depends on`, same-file conflicts, logical order), then launches one lane per issue (`forgedock run-issue N` as a background shell, or an agent when the CLI is unavailable), at most K at a time. Each lane runs the lean work-on and records every step on its issue.
161
161
 
162
162
  ```bash
163
- /orchestrate milestone launch-campaign # All issues in a milestone
164
163
  /orchestrate #42 #43 #44 # Specific issues
165
- /orchestrate next 5 # Top 5 priority issues
166
- /orchestrate fast-lane # All staging-lane issues
167
- /orchestrate P0 # All P0 issues
164
+ /orchestrate milestone launch-campaign # All open issues in a milestone
165
+ /orchestrate label bug --parallel 4 # All open issues with a label, 4 lanes
166
+ /orchestrate #42 #43 --path full # Legacy multi-phase orchestrator
168
167
  ```
169
168
 
170
- Each sub-agent gets its own git worktree and GitHub issue thread. No interference between parallel agents.
171
-
172
169
  ---
173
170
 
174
171
  #### `/milestone`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgedock",
3
- "version": "1.22.0",
3
+ "version": "1.23.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"