forgedock 1.23.1 → 1.24.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.
- package/bin/forgedock.mjs +10 -2
- package/bin/lean/effects.mjs +55 -8
- package/bin/lean/lean.mjs +286 -57
- package/bin/lean/lean.test.mjs +492 -84
- package/bin/lean/prompts.mjs +42 -7
- package/bin/lean/render.mjs +67 -28
- package/bin/runner.mjs +55 -1
- package/commands/orchestrate.md +12 -4
- package/commands/review-pr.md +1 -1
- package/docs/RELEASE-NOTES.md +6 -0
- package/package.json +1 -1
- package/scripts/review-finalize.sh +10 -7
- package/scripts/review-finalize.test.sh +6 -4
package/bin/forgedock.mjs
CHANGED
|
@@ -3209,6 +3209,7 @@ function help() {
|
|
|
3209
3209
|
["npx forgedock run <cmd> [args]", "Run a command headlessly (local claude CLI or Anthropic API)"],
|
|
3210
3210
|
["npx forgedock run-issue <issue> --lane <lane> [--path core|full] [--retry] [--wait-lease]", "Run the lean core work-on on one issue (default; --path full = legacy multi-phase pipeline) (--retry reopens an engine-error run, or a needs-human run once the label is cleared; --wait-lease waits for a held lease; exit 75 = deferred, lease held)"],
|
|
3211
3211
|
["npx forgedock orchestrate <issue...> [--parallel K] [--lane L]", "Run core work-on on several issues with bounded concurrency and Depends-on ordering"],
|
|
3212
|
+
["npx forgedock gc", "Prune stale per-run scratch dirs under ~/.forge/tmp/runs (older than 6h, owner dead)"],
|
|
3212
3213
|
["npx forgedock resume-stalled [--dry-run]", "Fleet stall recovery — re-dispatch expired-lease issues"],
|
|
3213
3214
|
["npx forgedock demo", "Set up a risk-free demo repo and print next steps"],
|
|
3214
3215
|
["npx forgedock labels [setup] [--repo owner/repo]", "Bootstrap ForgeDock-managed labels on a GitHub repo (idempotent)"],
|
|
@@ -3782,10 +3783,10 @@ async function watch() {
|
|
|
3782
3783
|
// The journey-routed commands render their own branded marks (hero/compact);
|
|
3783
3784
|
// splash() would double-brand them. Engine-surface commands, help, and
|
|
3784
3785
|
// unknown-command output keep the logo.
|
|
3785
|
-
const SPLASH_COMMANDS = new Set(["run", "run-issue", "orchestrate", "resume-stalled", "demo", "doctor", "watch", "help", "--help", "-h"]);
|
|
3786
|
+
const SPLASH_COMMANDS = new Set(["run", "run-issue", "orchestrate", "gc", "resume-stalled", "demo", "doctor", "watch", "help", "--help", "-h"]);
|
|
3786
3787
|
const KNOWN_COMMANDS = new Set([
|
|
3787
3788
|
"install", "init", "enable", "disable", "status", "uninstall", "update",
|
|
3788
|
-
"run", "backend-check", "run-issue", "orchestrate", "resume-stalled", "demo", "doctor", "watch", "labels", "config", "stats", "help", "--help", "-h",
|
|
3789
|
+
"run", "backend-check", "run-issue", "orchestrate", "gc", "resume-stalled", "demo", "doctor", "watch", "labels", "config", "stats", "help", "--help", "-h",
|
|
3789
3790
|
"version", "--version", "-v",
|
|
3790
3791
|
]);
|
|
3791
3792
|
if (SPLASH_COMMANDS.has(command) || !KNOWN_COMMANDS.has(command)) splash(command);
|
|
@@ -3983,6 +3984,13 @@ switch (command) {
|
|
|
3983
3984
|
}
|
|
3984
3985
|
break;
|
|
3985
3986
|
}
|
|
3987
|
+
case "gc": {
|
|
3988
|
+
// Best-effort prune of core-run scratch dirs (older than 6h, owner pid dead). Never touches dirs we did not create.
|
|
3989
|
+
const { makeRunTmp } = await import("./lean/effects.mjs");
|
|
3990
|
+
const removed = await makeRunTmp().prune();
|
|
3991
|
+
process.stdout.write(`gc: removed ${removed.length} stale run tmp dir(s)\n`);
|
|
3992
|
+
break;
|
|
3993
|
+
}
|
|
3986
3994
|
case "orchestrate": {
|
|
3987
3995
|
const { orchestrateFromCli } = await import("./engine-cli.mjs");
|
|
3988
3996
|
try {
|
package/bin/lean/effects.mjs
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
/** Core work-on: the real effects behind runCore's injected `io`. Argv only, no shell. */
|
|
4
4
|
import { execFile } from "node:child_process";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
-
import { readFileSync, appendFileSync, mkdirSync } from "node:fs";
|
|
7
|
-
import {
|
|
6
|
+
import { readFileSync, appendFileSync, mkdirSync, statSync, existsSync, rmSync, writeFileSync, readdirSync, lstatSync } from "node:fs";
|
|
7
|
+
import { homedir, hostname } from "node:os";
|
|
8
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
8
9
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import {
|
|
10
|
+
import { runCliBackendAsync, resolveClaudeCliBinary, getClaudeCliCapabilities, buildLeanSessionFlags, buildReadOnlyToolFlags } from "../runner.mjs";
|
|
10
11
|
import { makeIo } from "../engine-cli.mjs";
|
|
11
12
|
import { makeProjector } from "../engine/projector.mjs";
|
|
12
13
|
|
|
@@ -14,23 +15,66 @@ const pexec = promisify(execFile);
|
|
|
14
15
|
const script = (n) => fileURLToPath(new URL(`../../scripts/${n}`, import.meta.url));
|
|
15
16
|
|
|
16
17
|
/** Claude CLI session runner: fresh session, or `--resume <id>` for the builder. Resolves usage + session_id. */
|
|
17
|
-
export function makeLeanRunner({ runCli =
|
|
18
|
-
return async ({ role, issue, cwd, model, resume, system, prompt, readOnly = false }) => {
|
|
18
|
+
export function makeLeanRunner({ runCli = runCliBackendAsync } = {}) {
|
|
19
|
+
return async ({ role, issue, cwd, model, resume, system, prompt, readOnly = false, env = null }) => {
|
|
19
20
|
const bin = resolveClaudeCliBinary(cwd) || "claude";
|
|
20
21
|
const caps = getClaudeCliCapabilities(cwd);
|
|
21
22
|
const flags = [...buildLeanSessionFlags(caps), ...(readOnly ? buildReadOnlyToolFlags(caps) : [])];
|
|
22
|
-
const r = runCli({ spec: { name: `core-${role}` }, userMessage: prompt, systemPrompt: resume ? "" : system,
|
|
23
|
-
args: [String(issue)], cwd, bin, model, sessionFlags: flags, extraArgs: resume ? ["--resume", resume] : [],
|
|
23
|
+
const r = await runCli({ spec: { name: `core-${role}` }, userMessage: prompt, systemPrompt: resume ? "" : system,
|
|
24
|
+
args: [String(issue)], cwd, bin, model, env, sessionFlags: flags, extraArgs: resume ? ["--resume", resume] : [],
|
|
24
25
|
logger: { log() {} } });
|
|
25
26
|
return { text: r.text, sessionId: r.sessionId, usage: r.usage, costUsd: r.costUsd, durationMs: r.durationMs, turns: r.turns, model: r.cliModel ?? "default" };
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
export const RUN_TMP_MAX_AGE_MS = 6 * 3600 * 1000;
|
|
31
|
+
export const runTmpRoot = () => join(homedir(), ".forge", "tmp", "runs");
|
|
32
|
+
const OWNER = ".forge-run-owner";
|
|
33
|
+
const pidAlive = (pid) => { try { process.kill(pid, 0); return true; } catch (e) { return e?.code !== "ESRCH"; } };
|
|
34
|
+
|
|
35
|
+
/** Per-run scratch dirs under ~/.forge/tmp/runs/<issue>-<run>/. Only dirs carrying our owner marker are ever removed. */
|
|
36
|
+
export function makeRunTmp({ root = runTmpRoot(), now = () => Date.now(), alive = pidAlive, host = hostname() } = {}) {
|
|
37
|
+
const inside = (d) => { const r = resolve(d); return r.startsWith(resolve(root) + sep) && !r.slice(resolve(root).length + 1).includes(sep); };
|
|
38
|
+
return {
|
|
39
|
+
async make(issue, runId) {
|
|
40
|
+
const dir = join(root, String(runId));
|
|
41
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
42
|
+
writeFileSync(join(dir, OWNER), JSON.stringify({ issue, pid: process.pid, host, created: now() }));
|
|
43
|
+
return dir;
|
|
44
|
+
},
|
|
45
|
+
/** Remove a dir this module created (marker present, direct child of the runs root). */
|
|
46
|
+
async remove(dir) {
|
|
47
|
+
if (!dir || !inside(dir) || !existsSync(join(dir, OWNER))) return false;
|
|
48
|
+
rmSync(dir, { recursive: true, force: true });
|
|
49
|
+
return true;
|
|
50
|
+
},
|
|
51
|
+
/** Best-effort: dirs older than 6h whose owner pid is dead on this host. Never touches dirs without our marker. */
|
|
52
|
+
async prune({ maxAgeMs = RUN_TMP_MAX_AGE_MS } = {}) {
|
|
53
|
+
const removed = [];
|
|
54
|
+
let names = [];
|
|
55
|
+
try { names = readdirSync(root); } catch { return removed; }
|
|
56
|
+
for (const n of names) {
|
|
57
|
+
const dir = join(root, n);
|
|
58
|
+
try {
|
|
59
|
+
if (!lstatSync(dir).isDirectory()) continue;
|
|
60
|
+
const own = JSON.parse(readFileSync(join(dir, OWNER), "utf8"));
|
|
61
|
+
if (now() - Number(own.created) < maxAgeMs) continue;
|
|
62
|
+
if (own.host === host && Number.isSafeInteger(own.pid) && alive(own.pid)) continue;
|
|
63
|
+
if (own.host !== host) continue;
|
|
64
|
+
rmSync(dir, { recursive: true, force: true });
|
|
65
|
+
removed.push(n);
|
|
66
|
+
} catch { /* not ours or unreadable: leave it */ }
|
|
67
|
+
}
|
|
68
|
+
return removed;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
29
73
|
export function makeLeanIo({ repo }) {
|
|
30
74
|
const base = makeIo();
|
|
31
75
|
const run = (bin) => async (args, opts = {}) => (await pexec(bin, args, { cwd: opts.cwd, maxBuffer: 100 * 1024 * 1024, timeout: 120000 })).stdout;
|
|
32
76
|
return {
|
|
33
|
-
gh: run("gh"), git: run("git"), projector: makeProjector({ gh: base.gh }), runner: makeLeanRunner(), waitCiGreen: base.waitCiGreen,
|
|
77
|
+
gh: run("gh"), git: run("git"), runTmp: makeRunTmp(), projector: makeProjector({ gh: base.gh }), runner: makeLeanRunner(), waitCiGreen: base.waitCiGreen,
|
|
34
78
|
/** Names of the checks the base branch requires (contexts + checks). [] on 404 / no protection. */
|
|
35
79
|
async requiredChecks(lane) {
|
|
36
80
|
try {
|
|
@@ -50,6 +94,9 @@ export function makeLeanIo({ repo }) {
|
|
|
50
94
|
async transitionLabel(issue, state) { await pexec("bash", [script("transition-label.sh"), String(issue), "-R", repo, state], { timeout: 60000 }); },
|
|
51
95
|
fs: {
|
|
52
96
|
read: async (p) => readFileSync(p, "utf8"),
|
|
97
|
+
exists: async (p) => existsSync(p),
|
|
98
|
+
rmdir: async (p) => rmSync(p, { recursive: true, force: true }),
|
|
99
|
+
stat: async (p) => { try { return { mtimeMs: statSync(p).mtimeMs }; } catch { return null; } },
|
|
53
100
|
append: async (p, t) => { mkdirSync(dirname(p), { recursive: true }); appendFileSync(p, t); },
|
|
54
101
|
},
|
|
55
102
|
};
|