forgedock 1.1.1 → 1.1.3

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.
Files changed (174) hide show
  1. package/README.md +53 -8
  2. package/bin/batch-runner.mjs +339 -0
  3. package/bin/demo.mjs +92 -7
  4. package/bin/engine/phases.mjs +24 -2
  5. package/bin/engine/state.mjs +8 -3
  6. package/bin/engine-cli.mjs +177 -5
  7. package/bin/forgedock.mjs +409 -26
  8. package/bin/hooks/interactive-engine.mjs +509 -0
  9. package/bin/hooks/pre-tool-use.mjs +498 -0
  10. package/bin/hooks/subagent-stop-enforce.mjs +197 -0
  11. package/bin/journey.mjs +72 -14
  12. package/bin/registry.mjs +139 -0
  13. package/bin/report.mjs +447 -0
  14. package/bin/runner.mjs +70 -20
  15. package/bin/settings-hook.mjs +235 -1
  16. package/commands/adopt.md +4 -1
  17. package/commands/analytics.md +4 -1
  18. package/commands/audit-agents.md +110 -27
  19. package/commands/audit.md +20 -1
  20. package/commands/autopilot.md +518 -361
  21. package/commands/changelog.md +2 -1
  22. package/commands/ci-audit.md +2 -1
  23. package/commands/cleanup.md +161 -1
  24. package/commands/compat-audit.md +2 -1
  25. package/commands/deploy-info.md +58 -1
  26. package/commands/deploy-pr.md +485 -0
  27. package/commands/design-bench.md +5 -1
  28. package/commands/design-render-critique-loop.md +1 -1
  29. package/commands/diagnose.md +2 -1
  30. package/commands/explain.md +2 -1
  31. package/commands/fix-ci.md +577 -0
  32. package/commands/forge-stats.md +121 -2
  33. package/commands/forgedock-init.md +1 -1
  34. package/commands/geo-audit.md +3 -0
  35. package/commands/incident-response.md +2 -1
  36. package/commands/issue.md +23 -2
  37. package/commands/milestone.md +77 -12
  38. package/commands/optimize.md +2 -1
  39. package/commands/orchestrate/config.md +67 -0
  40. package/commands/orchestrate/phase-1-resolve.md +159 -0
  41. package/commands/orchestrate/phase-2-triage.md +126 -0
  42. package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
  43. package/commands/orchestrate/phase-3-dependency.md +591 -0
  44. package/commands/orchestrate/phase-4-execution.md +962 -0
  45. package/commands/orchestrate/phase-5-cleanup.md +47 -0
  46. package/commands/orchestrate/phase-6-report.md +198 -0
  47. package/commands/orchestrate/safety.md +62 -0
  48. package/commands/orchestrate.md +25 -2045
  49. package/commands/pipeline-health/config.md +34 -0
  50. package/commands/pipeline-health/phase-1-context.md +180 -0
  51. package/commands/pipeline-health/phase-2-metrics.md +1729 -0
  52. package/commands/pipeline-health/phase-3-analyze.md +204 -0
  53. package/commands/pipeline-health/phase-4-proposals.md +117 -0
  54. package/commands/pipeline-health/phase-5-report.md +284 -0
  55. package/commands/pipeline-health/phase-6-summary.md +52 -0
  56. package/commands/pipeline-health.md +25 -2401
  57. package/commands/pipeline-resume.md +24 -1
  58. package/commands/pipeline-status.md +65 -0
  59. package/commands/qa-sweep.md +4 -1
  60. package/commands/quality-gate.md +248 -10
  61. package/commands/recover-orphans.md +576 -0
  62. package/commands/replay.md +2 -1
  63. package/commands/review-pr-agents/api.md +135 -0
  64. package/commands/review-pr-agents/auth.md +137 -0
  65. package/commands/review-pr-agents/billing.md +101 -0
  66. package/commands/review-pr-agents/concurrency.md +145 -0
  67. package/commands/review-pr-agents/database.md +103 -0
  68. package/commands/review-pr-agents/frontend.md +89 -0
  69. package/commands/review-pr-agents/infra.md +385 -0
  70. package/commands/review-pr-agents/protocols.md +185 -0
  71. package/commands/review-pr-agents/scraper.md +185 -0
  72. package/commands/review-pr-agents/security.md +406 -0
  73. package/commands/review-pr-agents.md +45 -1722
  74. package/commands/review-pr-staging.md +196 -40
  75. package/commands/review-pr.md +591 -52
  76. package/commands/rollback.md +2 -1
  77. package/commands/scope.md +2 -1
  78. package/commands/security-audit.md +2 -1
  79. package/commands/signal-planner.md +623 -0
  80. package/commands/sync-ecosystem.md +1 -0
  81. package/commands/test-gate.md +17 -1
  82. package/commands/validate.md +2 -1
  83. package/commands/work-on/build/architect.md +28 -0
  84. package/commands/work-on/build/context.md +2 -0
  85. package/commands/work-on/build/implement.md +3 -1
  86. package/commands/work-on/build/validate.md +26 -7
  87. package/commands/work-on/build.md +163 -24
  88. package/commands/work-on/close.md +130 -3
  89. package/commands/work-on/decompose.md +19 -1
  90. package/commands/work-on/investigate.md +117 -2
  91. package/commands/work-on/review.md +24 -4
  92. package/commands/work-on-monolithic.md +7 -2
  93. package/commands/work-on.md +202 -4
  94. package/docs/BRAND.md +14 -7
  95. package/docs/CI.md +16 -2
  96. package/docs/CODEX.md +36 -1
  97. package/docs/CONFIG.md +5 -8
  98. package/docs/FORGE-PROTOCOL.md +15 -1064
  99. package/docs/anthropic-devrel-outreach.md +2 -2
  100. package/docs/articles/command-decomposition-token-savings.md +214 -0
  101. package/docs/articles/model-release-playbook.md +131 -0
  102. package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
  103. package/docs/claude-breakpoints.json +75 -5
  104. package/docs/design/abc-benchmark.md +1 -1
  105. package/docs/design/design-spec-schema.md +1 -1
  106. package/docs/design/divergent-generation.md +1 -1
  107. package/docs/design/eval-gate-runbook.md +178 -0
  108. package/docs/design/show-hn-draft.md +26 -1
  109. package/docs/design/token-cost-baseline-1298.md +59 -0
  110. package/docs/eval/README.md +120 -0
  111. package/docs/eval/corpus/seed-v1.json +6 -0
  112. package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
  113. package/docs/site/annotations-explained.md +24 -0
  114. package/docs/site/command-learning-path.md +51 -14
  115. package/docs/site/command-reference.md +386 -111
  116. package/docs/site/for-companies.md +95 -0
  117. package/docs/site/forge-annotation-protocol.md +12 -256
  118. package/docs/site/getting-started.md +43 -4
  119. package/docs/site/github-is-the-memory.md +113 -0
  120. package/docs/site/how-it-works.md +3 -13
  121. package/docs/site/index.md +9 -1
  122. package/docs/spec/eval-run-result.md +147 -0
  123. package/docs/spec/forge-protocol-v1.md +441 -18
  124. package/docs/spec/label-state-machine.md +55 -0
  125. package/docs/spec/review-protocol.md +158 -0
  126. package/docs/spec-graph-schema.md +28 -0
  127. package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
  128. package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
  129. package/examples/forgedock-demo/README.md +41 -22
  130. package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
  131. package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
  132. package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
  133. package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
  134. package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
  135. package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
  136. package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
  137. package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
  138. package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
  139. package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
  140. package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
  141. package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
  142. package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
  143. package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
  144. package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
  145. package/examples/forgedock-demo/scripts/smoke.js +18 -1
  146. package/examples/forgedock-demo/src/db.js +36 -11
  147. package/examples/forgedock-demo/src/routes/notes.js +106 -9
  148. package/examples/forgedock-demo/src/server.js +4 -1
  149. package/package.json +3 -1
  150. package/packages/protocol/LICENSE +21 -0
  151. package/packages/protocol/src/cli.js +135 -0
  152. package/packages/protocol/src/emit.js +146 -0
  153. package/packages/protocol/src/index.js +43 -0
  154. package/packages/protocol/src/parse.js +254 -0
  155. package/packages/protocol/src/types.js +171 -0
  156. package/packages/protocol/src/validate.js +122 -0
  157. package/scripts/bench-topology-cost.mjs +372 -0
  158. package/scripts/check-command-docs-drift.sh +198 -0
  159. package/scripts/check-protocol-restatements.sh +176 -0
  160. package/scripts/check-registry/README.md +72 -0
  161. package/scripts/check-registry/manifest.json +6 -0
  162. package/scripts/code-index.sh +454 -0
  163. package/scripts/conformance-check.mjs +168 -0
  164. package/scripts/doctor-pipeline-state.sh +484 -0
  165. package/scripts/eval-gate-baseline.json +17 -0
  166. package/scripts/eval-gate-scorecard.mjs +242 -0
  167. package/scripts/eval-scorecard-aggregator.mjs +252 -0
  168. package/scripts/flaky-quarantine.sh +241 -0
  169. package/scripts/forge-annotation.sh +567 -0
  170. package/scripts/issue-dedup.sh +236 -0
  171. package/scripts/validate-annotation-node.mjs +233 -0
  172. package/scripts/worktree-lifecycle.sh +3 -3
  173. package/templates/devdocs/agent/using-forgedock.md +14 -0
  174. package/docs/site/.vitepress/config.mjs +0 -85
@@ -50,6 +50,12 @@ export const PHASES = [
50
50
  id: "context",
51
51
  command: "work-on/build/context",
52
52
  entryCondition: (s) => s.committed.includes("investigate"),
53
+ async reconcile(state, io) {
54
+ // Idempotent resume: FORGE:CONTEXT:COMPLETE present → skip the LLM re-run.
55
+ // Bare FORGE:CONTEXT matches a partial/interrupted annotation — require :COMPLETE.
56
+ const m = await issueMarkers(state.issue, io);
57
+ return has(m, "FORGE:CONTEXT:COMPLETE") ? { satisfied: true } : { satisfied: false };
58
+ },
53
59
  async detectOutcome(state, io) {
54
60
  const m = await issueMarkers(state.issue, io);
55
61
  // Context is non-critical: a missing marker is a VISIBLE skip, not a hard fail (spec §7).
@@ -61,11 +67,17 @@ export const PHASES = [
61
67
  id: "architect",
62
68
  command: "work-on/build/architect",
63
69
  entryCondition: (s) => s.committed.includes("context"),
70
+ async reconcile(state, io) {
71
+ // Idempotent resume: FORGE:ARCHITECT:COMPLETE present → skip the LLM re-run.
72
+ // Bare FORGE:ARCHITECT matches a partial/interrupted annotation — require :COMPLETE.
73
+ const m = await issueMarkers(state.issue, io);
74
+ return has(m, "FORGE:ARCHITECT:COMPLETE") ? { satisfied: true } : { satisfied: false };
75
+ },
64
76
  async detectOutcome(state, io) {
65
77
  const m = await issueMarkers(state.issue, io);
66
- return has(m, "FORGE:ARCHITECT")
78
+ return has(m, "FORGE:ARCHITECT:COMPLETE")
67
79
  ? { status: "committed", outputs: {} }
68
- : { status: "failed", detail: "no FORGE:ARCHITECT" };
80
+ : { status: "failed", detail: "no FORGE:ARCHITECT:COMPLETE" };
69
81
  },
70
82
  },
71
83
  {
@@ -108,6 +120,16 @@ export const PHASES = [
108
120
  id: "close",
109
121
  command: "work-on/close",
110
122
  entryCondition: (s) => s.committed.includes("review"),
123
+ async reconcile(state, io) {
124
+ // Idempotent resume: issue already closed or workflow:merged label set → skip the LLM re-run.
125
+ const out = await io.gh(["issue", "view", String(state.issue), "--json", "state,labels"]);
126
+ let j;
127
+ try { j = JSON.parse(out || "{}"); } catch { return { satisfied: false }; }
128
+ const labels = (j.labels || []).map((l) => l.name || l);
129
+ return (j.state === "CLOSED" || labels.includes("workflow:merged"))
130
+ ? { satisfied: true }
131
+ : { satisfied: false };
132
+ },
111
133
  async detectOutcome(state, io) {
112
134
  const out = await io.gh(["issue", "view", String(state.issue), "--json", "state,labels"]);
113
135
  let j;
@@ -4,19 +4,24 @@
4
4
  */
5
5
  const OPEN = "<!-- FORGE:STATE";
6
6
  const CLOSE = "-->";
7
- // NOTE: a literal "-->" inside a serialized value would close the block early; terminalReason etc. should not contain it. Tracked as a follow-up.
8
7
  const BLOCK_RE = /<!-- FORGE:STATE\s*([\s\S]*?)-->/;
9
8
 
10
9
  /** @param {import("./phases.mjs").RunState} index */
11
10
  export function serializeState(index) {
12
- return `${OPEN}\n${JSON.stringify(index)}\n${CLOSE}`;
11
+ // Escape both HTML comment-close forms: the standard "-->" and the "--!>"
12
+ // variant that browsers/HTML parsers also honour. JSON.stringify does not
13
+ // escape either, so a payload value containing them could otherwise close
14
+ // the <!-- FORGE:STATE ... --> block early (CodeQL js/bad-tag-filter).
15
+ const payload = JSON.stringify(index).replace(/--(!?)>/g, "--$1&gt;");
16
+ return `${OPEN}\n${payload}\n${CLOSE}`;
13
17
  }
14
18
 
15
19
  /** @returns {import("./phases.mjs").RunState|null} */
16
20
  export function parseState(issueBody) {
17
21
  const m = BLOCK_RE.exec(issueBody || "");
18
22
  if (!m) return null;
19
- try { return JSON.parse(m[1].trim()); } catch { return null; }
23
+ // Inverse of serializeState: restore both "--&gt;" and "--!&gt;".
24
+ try { return JSON.parse(m[1].trim().replace(/--(!?)&gt;/g, "--$1>")); } catch { return null; }
20
25
  }
21
26
 
22
27
  /** Replace the FORGE:STATE block in place, or append one if absent. */
@@ -1,12 +1,15 @@
1
1
  /**
2
2
  * Headless entry point: `forgedock run-issue <issue>` drives one issue through the
3
3
  * durable engine; scanStalls finds dead-lease issues for the orchestrator to resume.
4
+ * `forgedock resume-stalled [--dry-run] [--lane <lane>]` enumerates all in-flight
5
+ * issues, calls scanStalls, and re-dispatches each stalled issue via run-issue.
4
6
  */
5
7
  import { execFile } from "node:child_process";
6
8
  import { promisify } from "node:util";
7
9
  import { homedir } from "node:os";
8
10
  import { join } from "node:path";
9
11
  import { runIssue } from "./engine.mjs";
12
+ import { makeProjector } from "./engine/projector.mjs";
10
13
 
11
14
  const pexec = promisify(execFile);
12
15
 
@@ -21,6 +24,55 @@ export function makeIo() {
21
24
 
22
25
  export function runDir() { return join(homedir(), ".forge", "runs"); }
23
26
 
27
+ /**
28
+ * Resolves the repo `gh` would target by default (i.e. resolved from the cwd
29
+ * git remote), via `gh repo view`. Used to validate an explicit `--repo` flag
30
+ * against the ambient context — see `assertRepoMatchesCwd`.
31
+ * @param {{gh: Function}} io
32
+ * @returns {Promise<string|null>} "owner/repo", or null if it can't be determined
33
+ */
34
+ async function resolveDefaultRepo(io) {
35
+ try {
36
+ const out = await io.gh(["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"]);
37
+ const repo = String(out).trim();
38
+ return repo || null;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Guards against cross-repo state confusion (forge#1593): `--repo` used to be
46
+ * threaded into the `gh issue list` enumeration call but never into the
47
+ * projector's state reads/writes or into re-dispatch, so a mismatched
48
+ * `--repo` silently read/wrote FORGE:STATE in the cwd-resolved repo instead
49
+ * of the requested one. Fully threading `--repo` through every `io.gh`/
50
+ * `io.git` call site the engine makes during a run (phases.mjs, reconcile.mjs,
51
+ * projector.mjs) would be a much larger, riskier change. Instead, this fails
52
+ * closed: if `--repo` is given and doesn't match the repo `gh` would use by
53
+ * default, refuse to run at all rather than silently mixing repos.
54
+ * @param {{gh: Function}} io
55
+ * @param {string|null} repo
56
+ * @returns {Promise<void>}
57
+ */
58
+ async function assertRepoMatchesCwd(io, repo) {
59
+ if (!repo) return;
60
+ const defaultRepo = await resolveDefaultRepo(io);
61
+ if (defaultRepo === null) {
62
+ throw new Error(
63
+ `--repo ${repo} was given, but the current repo could not be determined (\`gh repo view\` failed) to ` +
64
+ `verify it matches. Refusing to run cross-repo without verification.`
65
+ );
66
+ }
67
+ if (defaultRepo !== repo) {
68
+ throw new Error(
69
+ `--repo ${repo} does not match the current repo (${defaultRepo}). Cross-repo dispatch is not supported — ` +
70
+ `state reads/writes (issue view/edit) are cwd-scoped and would silently target ${defaultRepo} instead of ` +
71
+ `${repo}. Run this command with cwd set to a checkout of ${repo}, or omit --repo to operate on ${defaultRepo}.`
72
+ );
73
+ }
74
+ }
75
+
24
76
  /**
25
77
  * @param {number[]} issues
26
78
  * @param {{readState:(i:number)=>Promise<{terminal:boolean,lease:?{until:number}}|null>}} io
@@ -36,15 +88,135 @@ export async function scanStalls(issues, io, now) {
36
88
  return stalled;
37
89
  }
38
90
 
39
- export async function runFromCli(argv) {
91
+ /**
92
+ * @param {string[]} argv
93
+ * @param {{io?: {gh: Function}, runIssue?: Function}} [deps]
94
+ * Injectable for tests — defaults to real `gh`/`git` (makeIo()) and the real
95
+ * `runIssue` engine driver.
96
+ */
97
+ export async function runFromCli(argv, deps = {}) {
40
98
  const issue = parseInt(argv[0], 10);
41
- if (!Number.isInteger(issue)) throw new Error("usage: forgedock run-issue <issue-number>");
42
- const lane = flag(argv, "--lane") || "staging";
43
- const io = makeIo();
99
+ if (!Number.isInteger(issue)) throw new Error("usage: forgedock run-issue <issue-number> --lane <lane>");
100
+ const lane = flag(argv, "--lane");
101
+ if (!lane) throw new Error("--lane is required: e.g. --lane main or --lane staging. No default to prevent accidental production targeting.");
102
+ const repo = flag(argv, "--repo");
103
+ const io = deps.io ?? makeIo();
104
+ await assertRepoMatchesCwd(io, repo);
105
+ const runIssueFn = deps.runIssue ?? runIssue;
44
106
  const agentId = `cli_${process.pid}`;
45
- const res = await runIssue({ issue, dir: runDir(), agentId, lane, io,
107
+ const res = await runIssueFn({ issue, dir: runDir(), agentId, lane, io,
46
108
  runner: (await import("./runner.mjs")).runCommand, now: () => Date.now() });
47
109
  console.log(`issue #${issue} → ${res.terminalReason}`);
48
110
  return res;
49
111
  }
112
+
113
+ /**
114
+ * `forgedock resume-stalled [--dry-run] [--lane <lane>] [--repo <owner/repo>]`
115
+ *
116
+ * Enumerates all open issues carrying non-terminal workflow labels, reads each
117
+ * issue's FORGE:STATE block via the projector, identifies those with an expired
118
+ * lease (stalled), and re-dispatches them through the existing run-issue path.
119
+ *
120
+ * Flags:
121
+ * --dry-run Print the stalled list and exit 0 without dispatching anything.
122
+ * --lane Lane to pass to run-issue (required — no default to prevent accidental production targeting).
123
+ * --repo GitHub repo (owner/repo). Must match the repo `gh` resolves by default
124
+ * (the cwd git remote) — cross-repo dispatch is refused (forge#1593),
125
+ * since state reads/writes are cwd-scoped and would otherwise silently
126
+ * target the wrong repo. Omit --repo to operate on the cwd-resolved repo.
127
+ *
128
+ * Per-issue dispatch failures are caught and isolated — one issue's engine error
129
+ * (e.g. NO_API_KEY/NO_SDK or any other uncaught phase error from runIssue) does
130
+ * not abort dispatch of the remaining stalled issues in the batch. Failures are
131
+ * recorded in the returned `failed` array; the caller decides how to surface them.
132
+ *
133
+ * @param {string[]} argv
134
+ * @param {{io?: {gh: Function}, dispatch?: (argv: string[]) => Promise<any>}} [deps]
135
+ * Injectable for tests — defaults to real `gh`/`git` (makeIo()) and the real
136
+ * `runFromCli` dispatcher.
137
+ */
138
+ export async function resumeStalledFromCli(argv, deps = {}) {
139
+ const dryRun = argv.includes("--dry-run");
140
+ const lane = flag(argv, "--lane");
141
+ if (!lane) throw new Error("--lane is required for resume-stalled: e.g. --lane main or --lane staging.");
142
+ const repo = flag(argv, "--repo");
143
+
144
+ const io = deps.io ?? makeIo();
145
+ await assertRepoMatchesCwd(io, repo);
146
+ const dispatch = deps.dispatch ?? runFromCli;
147
+ const projector = makeProjector(io);
148
+ const now = Date.now();
149
+
150
+ // Collect candidate issue numbers from all non-terminal workflow labels.
151
+ const ACTIVE_LABELS = [
152
+ "workflow:investigating",
153
+ "workflow:ready-to-build",
154
+ "workflow:building",
155
+ "workflow:in-review",
156
+ ];
157
+
158
+ const repoFlag = repo ? ["--repo", repo] : [];
159
+ const issueSet = new Set();
160
+
161
+ for (const label of ACTIVE_LABELS) {
162
+ try {
163
+ const out = await io.gh([
164
+ "issue", "list",
165
+ ...repoFlag,
166
+ "--state", "open",
167
+ "--label", label,
168
+ "--limit", "100",
169
+ "--json", "number",
170
+ ]);
171
+ const items = JSON.parse(out);
172
+ for (const { number } of items) issueSet.add(number);
173
+ } catch {
174
+ // gh may return non-zero when no issues match — treat as empty.
175
+ }
176
+ }
177
+
178
+ if (issueSet.size === 0) {
179
+ console.log("resume-stalled: no in-flight issues found.");
180
+ return { stalled: [], dispatched: [], failed: [] };
181
+ }
182
+
183
+ const candidates = [...issueSet];
184
+ const stalled = await scanStalls(candidates, projector, now);
185
+
186
+ if (stalled.length === 0) {
187
+ console.log("resume-stalled: all in-flight issues have active leases — nothing stalled.");
188
+ return { stalled: [], dispatched: [], failed: [] };
189
+ }
190
+
191
+ console.log(`resume-stalled: ${stalled.length} stalled issue(s) found: ${stalled.map((n) => `#${n}`).join(", ")}`);
192
+
193
+ if (dryRun) {
194
+ console.log("resume-stalled: --dry-run — not dispatching.");
195
+ return { stalled, dispatched: [], failed: [] };
196
+ }
197
+
198
+ const dispatched = [];
199
+ const failed = [];
200
+ for (const issue of stalled) {
201
+ console.log(`resume-stalled: dispatching #${issue} …`);
202
+ try {
203
+ await dispatch([String(issue), "--lane", lane, ...(repo ? ["--repo", repo] : [])]);
204
+ dispatched.push(issue);
205
+ } catch (err) {
206
+ const message = err?.message ?? String(err);
207
+ console.error(`resume-stalled: #${issue} failed: ${message}`);
208
+ failed.push({ issue, error: message });
209
+ }
210
+ }
211
+
212
+ if (failed.length > 0) {
213
+ console.log(
214
+ `resume-stalled: ${dispatched.length} dispatched, ${failed.length} failed: ` +
215
+ `${failed.map((f) => `#${f.issue}`).join(", ")}`,
216
+ );
217
+ }
218
+
219
+ return { stalled, dispatched, failed };
220
+ }
221
+
50
222
  function flag(argv, name) { const i = argv.indexOf(name); return i >= 0 ? argv[i + 1] : null; }