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.
- package/README.md +53 -8
- package/bin/batch-runner.mjs +339 -0
- package/bin/demo.mjs +92 -7
- package/bin/engine/phases.mjs +24 -2
- package/bin/engine/state.mjs +8 -3
- package/bin/engine-cli.mjs +177 -5
- package/bin/forgedock.mjs +409 -26
- package/bin/hooks/interactive-engine.mjs +509 -0
- package/bin/hooks/pre-tool-use.mjs +498 -0
- package/bin/hooks/subagent-stop-enforce.mjs +197 -0
- package/bin/journey.mjs +72 -14
- package/bin/registry.mjs +139 -0
- package/bin/report.mjs +447 -0
- package/bin/runner.mjs +70 -20
- package/bin/settings-hook.mjs +235 -1
- package/commands/adopt.md +4 -1
- package/commands/analytics.md +4 -1
- package/commands/audit-agents.md +110 -27
- package/commands/audit.md +20 -1
- package/commands/autopilot.md +518 -361
- package/commands/changelog.md +2 -1
- package/commands/ci-audit.md +2 -1
- package/commands/cleanup.md +161 -1
- package/commands/compat-audit.md +2 -1
- package/commands/deploy-info.md +58 -1
- package/commands/deploy-pr.md +485 -0
- package/commands/design-bench.md +5 -1
- package/commands/design-render-critique-loop.md +1 -1
- package/commands/diagnose.md +2 -1
- package/commands/explain.md +2 -1
- package/commands/fix-ci.md +577 -0
- package/commands/forge-stats.md +121 -2
- package/commands/forgedock-init.md +1 -1
- package/commands/geo-audit.md +3 -0
- package/commands/incident-response.md +2 -1
- package/commands/issue.md +23 -2
- package/commands/milestone.md +77 -12
- package/commands/optimize.md +2 -1
- package/commands/orchestrate/config.md +67 -0
- package/commands/orchestrate/phase-1-resolve.md +159 -0
- package/commands/orchestrate/phase-2-triage.md +126 -0
- package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
- package/commands/orchestrate/phase-3-dependency.md +591 -0
- package/commands/orchestrate/phase-4-execution.md +962 -0
- package/commands/orchestrate/phase-5-cleanup.md +47 -0
- package/commands/orchestrate/phase-6-report.md +198 -0
- package/commands/orchestrate/safety.md +62 -0
- package/commands/orchestrate.md +25 -2045
- package/commands/pipeline-health/config.md +34 -0
- package/commands/pipeline-health/phase-1-context.md +180 -0
- package/commands/pipeline-health/phase-2-metrics.md +1729 -0
- package/commands/pipeline-health/phase-3-analyze.md +204 -0
- package/commands/pipeline-health/phase-4-proposals.md +117 -0
- package/commands/pipeline-health/phase-5-report.md +284 -0
- package/commands/pipeline-health/phase-6-summary.md +52 -0
- package/commands/pipeline-health.md +25 -2401
- package/commands/pipeline-resume.md +24 -1
- package/commands/pipeline-status.md +65 -0
- package/commands/qa-sweep.md +4 -1
- package/commands/quality-gate.md +248 -10
- package/commands/recover-orphans.md +576 -0
- package/commands/replay.md +2 -1
- package/commands/review-pr-agents/api.md +135 -0
- package/commands/review-pr-agents/auth.md +137 -0
- package/commands/review-pr-agents/billing.md +101 -0
- package/commands/review-pr-agents/concurrency.md +145 -0
- package/commands/review-pr-agents/database.md +103 -0
- package/commands/review-pr-agents/frontend.md +89 -0
- package/commands/review-pr-agents/infra.md +385 -0
- package/commands/review-pr-agents/protocols.md +185 -0
- package/commands/review-pr-agents/scraper.md +185 -0
- package/commands/review-pr-agents/security.md +406 -0
- package/commands/review-pr-agents.md +45 -1722
- package/commands/review-pr-staging.md +196 -40
- package/commands/review-pr.md +591 -52
- package/commands/rollback.md +2 -1
- package/commands/scope.md +2 -1
- package/commands/security-audit.md +2 -1
- package/commands/signal-planner.md +623 -0
- package/commands/sync-ecosystem.md +1 -0
- package/commands/test-gate.md +17 -1
- package/commands/validate.md +2 -1
- package/commands/work-on/build/architect.md +28 -0
- package/commands/work-on/build/context.md +2 -0
- package/commands/work-on/build/implement.md +3 -1
- package/commands/work-on/build/validate.md +26 -7
- package/commands/work-on/build.md +163 -24
- package/commands/work-on/close.md +130 -3
- package/commands/work-on/decompose.md +19 -1
- package/commands/work-on/investigate.md +117 -2
- package/commands/work-on/review.md +24 -4
- package/commands/work-on-monolithic.md +7 -2
- package/commands/work-on.md +202 -4
- package/docs/BRAND.md +14 -7
- package/docs/CI.md +16 -2
- package/docs/CODEX.md +36 -1
- package/docs/CONFIG.md +5 -8
- package/docs/FORGE-PROTOCOL.md +15 -1064
- package/docs/anthropic-devrel-outreach.md +2 -2
- package/docs/articles/command-decomposition-token-savings.md +214 -0
- package/docs/articles/model-release-playbook.md +131 -0
- package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
- package/docs/claude-breakpoints.json +75 -5
- package/docs/design/abc-benchmark.md +1 -1
- package/docs/design/design-spec-schema.md +1 -1
- package/docs/design/divergent-generation.md +1 -1
- package/docs/design/eval-gate-runbook.md +178 -0
- package/docs/design/show-hn-draft.md +26 -1
- package/docs/design/token-cost-baseline-1298.md +59 -0
- package/docs/eval/README.md +120 -0
- package/docs/eval/corpus/seed-v1.json +6 -0
- package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
- package/docs/site/annotations-explained.md +24 -0
- package/docs/site/command-learning-path.md +51 -14
- package/docs/site/command-reference.md +386 -111
- package/docs/site/for-companies.md +95 -0
- package/docs/site/forge-annotation-protocol.md +12 -256
- package/docs/site/getting-started.md +43 -4
- package/docs/site/github-is-the-memory.md +113 -0
- package/docs/site/how-it-works.md +3 -13
- package/docs/site/index.md +9 -1
- package/docs/spec/eval-run-result.md +147 -0
- package/docs/spec/forge-protocol-v1.md +441 -18
- package/docs/spec/label-state-machine.md +55 -0
- package/docs/spec/review-protocol.md +158 -0
- package/docs/spec-graph-schema.md +28 -0
- package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
- package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
- package/examples/forgedock-demo/README.md +41 -22
- package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
- package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
- package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
- package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
- package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
- package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
- package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
- package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
- package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
- package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
- package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
- package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
- package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
- package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
- package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
- package/examples/forgedock-demo/scripts/smoke.js +18 -1
- package/examples/forgedock-demo/src/db.js +36 -11
- package/examples/forgedock-demo/src/routes/notes.js +106 -9
- package/examples/forgedock-demo/src/server.js +4 -1
- package/package.json +3 -1
- package/packages/protocol/LICENSE +21 -0
- package/packages/protocol/src/cli.js +135 -0
- package/packages/protocol/src/emit.js +146 -0
- package/packages/protocol/src/index.js +43 -0
- package/packages/protocol/src/parse.js +254 -0
- package/packages/protocol/src/types.js +171 -0
- package/packages/protocol/src/validate.js +122 -0
- package/scripts/bench-topology-cost.mjs +372 -0
- package/scripts/check-command-docs-drift.sh +198 -0
- package/scripts/check-protocol-restatements.sh +176 -0
- package/scripts/check-registry/README.md +72 -0
- package/scripts/check-registry/manifest.json +6 -0
- package/scripts/code-index.sh +454 -0
- package/scripts/conformance-check.mjs +168 -0
- package/scripts/doctor-pipeline-state.sh +484 -0
- package/scripts/eval-gate-baseline.json +17 -0
- package/scripts/eval-gate-scorecard.mjs +242 -0
- package/scripts/eval-scorecard-aggregator.mjs +252 -0
- package/scripts/flaky-quarantine.sh +241 -0
- package/scripts/forge-annotation.sh +567 -0
- package/scripts/issue-dedup.sh +236 -0
- package/scripts/validate-annotation-node.mjs +233 -0
- package/scripts/worktree-lifecycle.sh +3 -3
- package/templates/devdocs/agent/using-forgedock.md +14 -0
- package/docs/site/.vitepress/config.mjs +0 -85
package/bin/engine/phases.mjs
CHANGED
|
@@ -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;
|
package/bin/engine/state.mjs
CHANGED
|
@@ -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
|
-
|
|
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>");
|
|
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
|
-
|
|
23
|
+
// Inverse of serializeState: restore both "-->" and "--!>".
|
|
24
|
+
try { return JSON.parse(m[1].trim().replace(/--(!?)>/g, "--$1>")); } catch { return null; }
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
/** Replace the FORGE:STATE block in place, or append one if absent. */
|
package/bin/engine-cli.mjs
CHANGED
|
@@ -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
|
-
|
|
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")
|
|
43
|
-
|
|
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
|
|
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; }
|