forgedock 1.1.10 → 1.2.0
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 +1 -1
- package/bin/cli-spawn-shared.mjs +25 -0
- package/bin/engine-cli.mjs +22 -1
- package/bin/engine.mjs +46 -4
- package/bin/forgedock.mjs +166 -20
- package/bin/hooks/pre-tool-use.mjs +331 -10
- package/bin/init-detect.mjs +1 -1
- package/bin/init-enrich-api.mjs +12 -4
- package/bin/init-enrich-cli.mjs +328 -0
- package/bin/init-enrich.mjs +137 -0
- package/bin/journey.mjs +328 -50
- package/bin/runner.mjs +653 -10
- package/commands/adopt.md +1 -1
- package/commands/analytics.md +10 -7
- package/commands/audit-agents.md +29 -6
- package/commands/audit.md +9 -20
- package/commands/autopilot.md +52 -78
- package/commands/changelog.md +1 -1
- package/commands/ci-audit.md +19 -10
- package/commands/cleanup.md +1 -1
- package/commands/compat-audit.md +1 -1
- package/commands/deploy-info.md +1 -1
- package/commands/deploy-pr.md +1 -1
- package/commands/design-bench.md +1 -1
- package/commands/design-render-critique-loop.md +1 -1
- package/commands/diagnose.md +1 -1
- package/commands/forge-stats.md +1 -1
- package/commands/forgedock-init.md +1 -1
- package/commands/geo-audit.md +9 -6
- package/commands/incident-response.md +15 -5
- package/commands/issue.md +298 -15
- package/commands/milestone.md +45 -18
- package/commands/optimize.md +1 -1
- package/commands/orchestrate/phase-1-resolve.md +20 -5
- package/commands/orchestrate/phase-4-execution.md +6 -1
- package/commands/orchestrate/phase-5-cleanup.md +43 -2
- package/commands/orchestrate/phase-6-report.md +1 -0
- package/commands/orchestrate.md +1 -1
- package/commands/pipeline-health/phase-4-proposals.md +40 -10
- package/commands/pipeline-health/phase-5-report.md +140 -13
- package/commands/pipeline-health.md +1 -1
- package/commands/pipeline-resume.md +1 -1
- package/commands/pipeline-status.md +1 -1
- package/commands/qa-sweep.md +10 -4
- package/commands/recover-orphans.md +1 -1
- package/commands/review-pr-agents/protocols.md +4 -0
- package/commands/review-pr-agents.md +7 -4
- package/commands/review-pr-staging.md +44 -15
- package/commands/review-pr.md +73 -28
- package/commands/rollback.md +15 -5
- package/commands/security-audit.md +14 -11
- package/commands/signal-planner.md +32 -12
- package/commands/spec-doctor.md +1 -1
- package/commands/sync-ecosystem.md +54 -11
- package/commands/test-gate.md +49 -11
- package/commands/validate.md +28 -9
- package/commands/work-on/build/architect.md +1 -1
- package/commands/work-on/build/context.md +1 -1
- package/commands/work-on/build/implement.md +2 -2
- package/commands/work-on/build/validate.md +3 -1
- package/commands/work-on/build.md +1 -1
- package/commands/work-on/close.md +1 -1
- package/commands/work-on/decompose.md +27 -24
- package/commands/work-on/investigate.md +2 -2
- package/commands/work-on/remediate.md +11 -3
- package/commands/work-on/review.md +5 -3
- package/commands/work-on-monolithic.md +1 -1
- package/commands/work-on.md +38 -8
- package/docs/CI.md +18 -0
- package/docs/CONFIG.md +5 -2
- package/docs/site/annotations-explained.md +2 -2
- package/docs/site/command-reference.md +21 -0
- package/docs/spec/review-protocol.md +6 -0
- package/package.json +1 -1
- package/scripts/danger-zones.mjs +3 -1
- package/scripts/transition-label.sh +80 -28
package/README.md
CHANGED
|
@@ -250,7 +250,7 @@ Then just open Claude Code and run `/work-on <issue>`.
|
|
|
250
250
|
|
|
251
251
|
Commands then appear as `/forgedock:work-on`, etc. You still run `npx forgedock init` to generate `forge.yaml`.
|
|
252
252
|
|
|
253
|
-
**Headless / CI:** the pipeline also runs outside Claude Code. `npx forgedock run work-on <issue> --dry-run` previews the assembled prompt and tool plan
|
|
253
|
+
**Headless / CI:** the pipeline also runs outside Claude Code. `npx forgedock run work-on <issue> --dry-run` previews the assembled prompt and tool plan. `npx forgedock run` picks an execution backend automatically (`--backend auto`, the default): if the Claude Code CLI (`claude`) is installed and already authenticated (Pro/Max subscription or a CLI-managed key), it drives the command through that — **no separate `ANTHROPIC_API_KEY` required**. Otherwise it falls back to the Anthropic API directly (`ANTHROPIC_API_KEY` required). Force either path explicitly with `--backend cli` / `--backend api` (or `FORGEDOCK_BACKEND=cli|api`) — the API backend is what CI environments without an interactive `claude` login should use. `npx forgedock run-issue <issue>` executes the same command specs on the durable engine (event-sourced run log, leases, crash-safe resume).
|
|
254
254
|
|
|
255
255
|
**Explicit install command:**
|
|
256
256
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli-spawn-shared.mjs — shared constants for `claude` CLI `spawnSync`
|
|
3
|
+
* invocations across the codebase.
|
|
4
|
+
*
|
|
5
|
+
* Extracted (issue #2032) to stop `maxBuffer: 50 * 1024 * 1024` from
|
|
6
|
+
* drifting independently at each spawn call site (bin/runner.mjs's
|
|
7
|
+
* `runCliBackend` and `run_bash`, bin/init-enrich-cli.mjs's `enrich()`) —
|
|
8
|
+
* all three invoke the same local `claude` CLI and should share one
|
|
9
|
+
* default rather than three copies of the same magic number.
|
|
10
|
+
*
|
|
11
|
+
* Isolated: imports no external modules, so it is safe to import from any
|
|
12
|
+
* module without creating a cycle.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Default `maxBuffer` (bytes) for a `spawnSync` call against the local
|
|
17
|
+
* `claude` CLI. 50 MB comfortably covers a full headless response (stdout)
|
|
18
|
+
* plus any diagnostic stderr without risking silent truncation.
|
|
19
|
+
*
|
|
20
|
+
* `bin/runner.mjs`'s `run_bash` tool handler additionally allows this
|
|
21
|
+
* default to be overridden per-call via `FORGEDOCK_MAX_BUFFER_BYTES` — that
|
|
22
|
+
* override logic lives in `runner.mjs` itself; this constant is only the
|
|
23
|
+
* shared *default*.
|
|
24
|
+
*/
|
|
25
|
+
export const DEFAULT_SPAWN_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
|
package/bin/engine-cli.mjs
CHANGED
|
@@ -224,6 +224,21 @@ export function lastLocalRun(dir) {
|
|
|
224
224
|
* @param {{io?: {gh: Function}, runIssue?: Function}} [deps]
|
|
225
225
|
* Injectable for tests — defaults to real `gh`/`git` (makeIo()) and the real
|
|
226
226
|
* `runIssue` engine driver.
|
|
227
|
+
*
|
|
228
|
+
* Flags:
|
|
229
|
+
* --lane <lane> Required — e.g. `main` or `staging`.
|
|
230
|
+
* --repo <owner/repo> Optional — must match the cwd-resolved repo (forge#1593).
|
|
231
|
+
* --backend <cli|api|auto> Optional (forge#2028). Forwarded to every phase's
|
|
232
|
+
* `runCommand()` call. Omit to keep runner.mjs's own
|
|
233
|
+
* default ("auto" ladder — probes the `claude` CLI, falls
|
|
234
|
+
* back to the API). Mirrors `bin/forgedock.mjs`'s `run()`
|
|
235
|
+
* `--backend` semantics — validation (rejecting invalid
|
|
236
|
+
* values) is left to `runCommand()`'s own `resolveBackend()`
|
|
237
|
+
* rather than duplicated here.
|
|
238
|
+
* --model <id> Optional (forge#2028). Forwarded to every phase's
|
|
239
|
+
* `runCommand()` call; only applies on the "api" backend.
|
|
240
|
+
* Omit to keep runner.mjs's default (`FORGEDOCK_MODEL` env
|
|
241
|
+
* or its built-in default).
|
|
227
242
|
*/
|
|
228
243
|
export async function runFromCli(argv, deps = {}) {
|
|
229
244
|
const issue = parseInt(argv[0], 10);
|
|
@@ -231,12 +246,18 @@ export async function runFromCli(argv, deps = {}) {
|
|
|
231
246
|
const lane = flag(argv, "--lane");
|
|
232
247
|
if (!lane) throw new Error("--lane is required: e.g. --lane main or --lane staging. No default to prevent accidental production targeting.");
|
|
233
248
|
const repo = flag(argv, "--repo");
|
|
249
|
+
const backend = flag(argv, "--backend");
|
|
250
|
+
const model = flag(argv, "--model");
|
|
234
251
|
const io = deps.io ?? makeIo();
|
|
235
252
|
await assertRepoMatchesCwd(io, repo);
|
|
236
253
|
const runIssueFn = deps.runIssue ?? runIssue;
|
|
237
254
|
const agentId = `cli_${process.pid}`;
|
|
238
255
|
const res = await runIssueFn({ issue, dir: runDir(), agentId, lane, io,
|
|
239
|
-
runner: (await import("./runner.mjs")).runCommand, now: () => Date.now()
|
|
256
|
+
runner: (await import("./runner.mjs")).runCommand, now: () => Date.now(),
|
|
257
|
+
// Only forwarded when explicitly provided — omitting them preserves
|
|
258
|
+
// runIssue's/runner.mjs's existing defaults (forge#2028).
|
|
259
|
+
...(backend ? { backend } : {}),
|
|
260
|
+
...(model ? { model } : {}) });
|
|
240
261
|
console.log(`issue #${issue} → ${res.terminalReason}`);
|
|
241
262
|
return res;
|
|
242
263
|
}
|
package/bin/engine.mjs
CHANGED
|
@@ -8,13 +8,49 @@ import { appendEvent, readLog, deriveState, rewriteLog } from "./engine/runlog.m
|
|
|
8
8
|
import { pickPhase, TERMINAL_REASONS } from "./engine/phases.mjs";
|
|
9
9
|
import { reconcileState } from "./engine/reconcile.mjs";
|
|
10
10
|
import { makeProjector } from "./engine/projector.mjs";
|
|
11
|
+
import { VALID_BACKENDS } from "./runner.mjs";
|
|
11
12
|
|
|
12
13
|
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} opts
|
|
17
|
+
* @param {string} [opts.backend] - "cli" | "api" | "auto" (forge#2028). Forwarded
|
|
18
|
+
* to every phase's `runner()` call when supplied. Omit to keep runner.mjs's own
|
|
19
|
+
* default ("auto" ladder — probes the `claude` CLI, falls back to the API).
|
|
20
|
+
* An invalid value throws synchronously (see below) rather than being forwarded.
|
|
21
|
+
* @param {string} [opts.model] - Model id (forge#2028). Forwarded to every phase's
|
|
22
|
+
* `runner()` call when supplied; only applies on the "api" backend. Omit to keep
|
|
23
|
+
* runner.mjs's default (`FORGEDOCK_MODEL` env or its built-in default).
|
|
24
|
+
*/
|
|
14
25
|
export async function runIssue(opts) {
|
|
15
26
|
const { issue, dir, agentId, lane = "staging", io, runner,
|
|
16
27
|
now = () => Date.now(), maxAttempts = DEFAULT_MAX_ATTEMPTS,
|
|
17
|
-
commandsDir = fileURLToPath(new URL("../commands", import.meta.url))
|
|
28
|
+
commandsDir = fileURLToPath(new URL("../commands", import.meta.url)),
|
|
29
|
+
// Optional execution-backend override for every phase's `runner()`
|
|
30
|
+
// call (forge#2028 / MAT-3). Left undefined by default so existing
|
|
31
|
+
// callers keep runner.mjs's own "auto" ladder default unchanged —
|
|
32
|
+
// this is purely additive pass-through, not a new default.
|
|
33
|
+
backend, model } = opts;
|
|
34
|
+
|
|
35
|
+
// forge#2054: validate `backend` before anything else — before state is
|
|
36
|
+
// read/written and before the phase/retry loop begins below. An invalid
|
|
37
|
+
// value must fail fast and non-retryably. Without this check, an invalid
|
|
38
|
+
// backend instead reaches runner.mjs's resolveBackend() deep inside
|
|
39
|
+
// runPhaseWithRetry()'s per-attempt try/catch, throws an uncoded Error, and
|
|
40
|
+
// is silently retried up to `maxAttempts` times (the catch there only
|
|
41
|
+
// fast-fails on `.code === "NO_API_KEY"/"NO_SDK"`) before escalating to
|
|
42
|
+
// needs-human — burning retries on what is actually a config error, not a
|
|
43
|
+
// transient phase failure. runIssue() is the single production choke point
|
|
44
|
+
// (its only caller is bin/engine-cli.mjs's runFromCli()), so validating
|
|
45
|
+
// here protects every current and future caller without requiring each one
|
|
46
|
+
// to remember to validate independently.
|
|
47
|
+
if (backend !== undefined && !VALID_BACKENDS.has(backend)) {
|
|
48
|
+
throw Object.assign(
|
|
49
|
+
new Error(`Invalid backend "${backend}". Must be one of: ${[...VALID_BACKENDS].join(", ")}.`),
|
|
50
|
+
{ code: "INVALID_BACKEND" },
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
18
54
|
const projector = makeProjector(io);
|
|
19
55
|
|
|
20
56
|
// 1. Load + reconcile (GitHub wins).
|
|
@@ -67,7 +103,7 @@ export async function runIssue(opts) {
|
|
|
67
103
|
outcome = { status: "committed", outputs: reconciled.outputs || {} };
|
|
68
104
|
} else {
|
|
69
105
|
if (reconciled.outputs?.pr) state.pr = reconciled.outputs.pr;
|
|
70
|
-
outcome = await runPhaseWithRetry(phase, state, { io, runner, dir, issue, commandsDir, maxAttempts });
|
|
106
|
+
outcome = await runPhaseWithRetry(phase, state, { io, runner, dir, issue, commandsDir, maxAttempts, backend, model });
|
|
71
107
|
}
|
|
72
108
|
|
|
73
109
|
if (outcome.status === "blocked") return await terminate(state, "needs-human", outcome.detail);
|
|
@@ -95,11 +131,17 @@ export async function runIssue(opts) {
|
|
|
95
131
|
}
|
|
96
132
|
|
|
97
133
|
async function runPhaseWithRetry(phase, state, ctx) {
|
|
98
|
-
const { io, runner, dir, issue, commandsDir, maxAttempts } = ctx;
|
|
134
|
+
const { io, runner, dir, issue, commandsDir, maxAttempts, backend, model } = ctx;
|
|
99
135
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
100
136
|
appendEvent(dir, issue, { event: "PHASE_START", phase: phase.id, attempt });
|
|
101
137
|
try {
|
|
102
|
-
await runner({
|
|
138
|
+
await runner({
|
|
139
|
+
commandsDir, commandName: phase.command, args: [String(issue)],
|
|
140
|
+
// Only forwarded when explicitly provided — omitting them preserves
|
|
141
|
+
// runner.mjs's existing default ("auto" backend / DEFAULT_MODEL).
|
|
142
|
+
...(backend ? { backend } : {}),
|
|
143
|
+
...(model ? { model } : {}),
|
|
144
|
+
});
|
|
103
145
|
} catch (e) {
|
|
104
146
|
// A missing API key / SDK is a config error, not a transient phase
|
|
105
147
|
// failure — surface it distinctly instead of burning retries and
|
package/bin/forgedock.mjs
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
parseInstallTier,
|
|
29
29
|
writeForgeYaml,
|
|
30
30
|
backupExisting,
|
|
31
|
+
backfillForgeYaml,
|
|
31
32
|
manualLowConfidenceKeys,
|
|
32
33
|
isEphemeralCachePath,
|
|
33
34
|
writeInstallReceipt,
|
|
@@ -950,6 +951,21 @@ async function statusScreen(c) {
|
|
|
950
951
|
c.stdout.write("\n");
|
|
951
952
|
}
|
|
952
953
|
|
|
954
|
+
/**
|
|
955
|
+
* `npx forgedock init` — detect + AI-enrich + review `forge.yaml`.
|
|
956
|
+
*
|
|
957
|
+
* AI enrichment backend selection (bin/init-enrich.mjs `resolveEnrichBackend`,
|
|
958
|
+
* issue #2004): prefers a local, authenticated `claude` CLI when present,
|
|
959
|
+
* otherwise falls back to the Anthropic API when ANTHROPIC_API_KEY is set,
|
|
960
|
+
* otherwise skips enrichment.
|
|
961
|
+
*
|
|
962
|
+
* Env:
|
|
963
|
+
* FORGEDOCK_INIT_BACKEND Override ("cli"|"api"|"none"|"auto"; default
|
|
964
|
+
* "auto") — pins the enrichment backend instead of
|
|
965
|
+
* the auto-detect ladder above (issue #2023).
|
|
966
|
+
* Independent of FORGEDOCK_BACKEND, which controls
|
|
967
|
+
* the separate `forgedock run` engine backend.
|
|
968
|
+
*/
|
|
953
969
|
async function initFlow(c) {
|
|
954
970
|
const outputPath = join(c.cwd, "forge.yaml");
|
|
955
971
|
const hasExisting = existsSync(outputPath);
|
|
@@ -1874,6 +1890,16 @@ async function doctor(fix = false) {
|
|
|
1874
1890
|
* Reuses forge() (bin/journey.mjs) verbatim — the same repair path already
|
|
1875
1891
|
* used by `npx forgedock update` (see relinkAndHint()) — rather than
|
|
1876
1892
|
* duplicating symlink/copy-mode or hook-registration logic here.
|
|
1893
|
+
*
|
|
1894
|
+
* NOTE: `installRepairRan` only memoizes whether the repair primitive
|
|
1895
|
+
* itself has executed during this invocation — it is a cross-check flag
|
|
1896
|
+
* by necessity (any of the three checks below can trigger it, and it must
|
|
1897
|
+
* not re-run for the other two). It must NOT be read by an individual
|
|
1898
|
+
* check to decide whether THAT check was fixed — a check that was already
|
|
1899
|
+
* healthy before a sibling check triggered repair would incorrectly
|
|
1900
|
+
* report "repaired" too. Each check below tracks its own pre-repair
|
|
1901
|
+
* broken state in a local `*WasBroken` variable for that decision instead.
|
|
1902
|
+
* (forge#1975)
|
|
1877
1903
|
*/
|
|
1878
1904
|
let installRepairRan = false;
|
|
1879
1905
|
async function runInstallRepairOnce() {
|
|
@@ -1954,7 +1980,9 @@ async function doctor(fix = false) {
|
|
|
1954
1980
|
}
|
|
1955
1981
|
|
|
1956
1982
|
let cmdFiles = await detectCommandFiles();
|
|
1983
|
+
let cmdFilesWasBroken = false;
|
|
1957
1984
|
if (!cmdFiles.error && !cmdFiles.ok && fix) {
|
|
1985
|
+
cmdFilesWasBroken = true;
|
|
1958
1986
|
await runInstallRepairOnce();
|
|
1959
1987
|
cmdFiles = await detectCommandFiles();
|
|
1960
1988
|
}
|
|
@@ -1962,7 +1990,7 @@ async function doctor(fix = false) {
|
|
|
1962
1990
|
if (cmdFiles.error) {
|
|
1963
1991
|
fail("Command files", `Could not read commands directory: ${cmdFiles.error}`);
|
|
1964
1992
|
} else if (cmdFiles.ok) {
|
|
1965
|
-
if (fix &&
|
|
1993
|
+
if (fix && cmdFilesWasBroken) {
|
|
1966
1994
|
fixed("Command files", `${cmdFiles.checked} files installed (repaired)`);
|
|
1967
1995
|
} else {
|
|
1968
1996
|
pass("Command files", `${cmdFiles.checked} files installed`);
|
|
@@ -2153,7 +2181,9 @@ async function doctor(fix = false) {
|
|
|
2153
2181
|
}
|
|
2154
2182
|
|
|
2155
2183
|
let hookReg = detectSessionStartHookRegistered();
|
|
2184
|
+
let hookRegWasBroken = false;
|
|
2156
2185
|
if (!hookReg.error && !hookReg.ok && fix) {
|
|
2186
|
+
hookRegWasBroken = true;
|
|
2157
2187
|
await runInstallRepairOnce();
|
|
2158
2188
|
hookReg = detectSessionStartHookRegistered();
|
|
2159
2189
|
}
|
|
@@ -2161,7 +2191,7 @@ async function doctor(fix = false) {
|
|
|
2161
2191
|
if (hookReg.error) {
|
|
2162
2192
|
fail("SessionStart hook", `Cannot read ~/.claude/settings.json: ${hookReg.error}. Run: npx forgedock install`);
|
|
2163
2193
|
} else if (hookReg.ok) {
|
|
2164
|
-
if (fix &&
|
|
2194
|
+
if (fix && hookRegWasBroken) {
|
|
2165
2195
|
fixed("SessionStart hook", "registered in ~/.claude/settings.json (repaired)");
|
|
2166
2196
|
} else {
|
|
2167
2197
|
pass("SessionStart hook", "registered in ~/.claude/settings.json");
|
|
@@ -2221,7 +2251,9 @@ async function doctor(fix = false) {
|
|
|
2221
2251
|
// ever surfaced. This check catches that silent-failure case. (forge#1895)
|
|
2222
2252
|
{
|
|
2223
2253
|
let hookPathResult = detectSessionStartHookPath();
|
|
2254
|
+
let hookPathWasBroken = false;
|
|
2224
2255
|
if (hookPathResult.state === "dangling" && fix) {
|
|
2256
|
+
hookPathWasBroken = true;
|
|
2225
2257
|
await runInstallRepairOnce();
|
|
2226
2258
|
hookPathResult = detectSessionStartHookPath();
|
|
2227
2259
|
}
|
|
@@ -2237,7 +2269,7 @@ async function doctor(fix = false) {
|
|
|
2237
2269
|
`Could not parse a script path out of the registered hook command ("${hookPathResult.command}"). Run: npx forgedock install`,
|
|
2238
2270
|
);
|
|
2239
2271
|
} else if (hookPathResult.state === "ok") {
|
|
2240
|
-
if (fix &&
|
|
2272
|
+
if (fix && hookPathWasBroken) {
|
|
2241
2273
|
fixed("SessionStart hook script path", `refreshed to a valid path (${hookPathResult.path})`);
|
|
2242
2274
|
} else {
|
|
2243
2275
|
pass("SessionStart hook script path", `exists on disk (${hookPathResult.path})`);
|
|
@@ -2704,17 +2736,19 @@ function help() {
|
|
|
2704
2736
|
["npx forgedock enable [dir]", "Activate ForgeDock in a directory"],
|
|
2705
2737
|
["npx forgedock disable [dir]", "Opt a directory out of ForgeDock"],
|
|
2706
2738
|
["npx forgedock status [dir]", "Show ForgeDock state for a directory"],
|
|
2707
|
-
["npx forgedock run <cmd> [args]", "Run a command headlessly
|
|
2739
|
+
["npx forgedock run <cmd> [args]", "Run a command headlessly (local claude CLI or Anthropic API)"],
|
|
2708
2740
|
["npx forgedock run-issue <issue>", "Drive one issue through the durable engine"],
|
|
2709
2741
|
["npx forgedock resume-stalled [--dry-run]", "Fleet stall recovery — re-dispatch expired-lease issues"],
|
|
2710
2742
|
["npx forgedock demo", "Set up a risk-free demo repo and print next steps"],
|
|
2711
2743
|
["npx forgedock labels [setup] [--repo owner/repo]", "Bootstrap ForgeDock-managed labels on a GitHub repo (idempotent)"],
|
|
2744
|
+
["npx forgedock config migrate [dir]", "Backfill missing optional sections into an existing forge.yaml (idempotent)"],
|
|
2712
2745
|
["npx forgedock watch [--repo owner/repo]", "Live per-agent orchestration view (Ctrl+C to exit)"],
|
|
2713
2746
|
["npx forgedock report [--days 30] [--md] [--json]", "30-day pipeline impact receipts for your repo"],
|
|
2714
2747
|
["npx forgedock doctor", "Check installation health"],
|
|
2715
2748
|
["npx forgedock doctor --fix", "Auto-fix deterministic issues (symlinks, hook, labels, legacy block)"],
|
|
2716
2749
|
["npx forgedock update", "Pull latest & reinstall"],
|
|
2717
2750
|
["npx forgedock uninstall", "Remove commands"],
|
|
2751
|
+
["npx forgedock version", "Print the installed version and check for updates"],
|
|
2718
2752
|
["npx forgedock help", "Show this help"],
|
|
2719
2753
|
];
|
|
2720
2754
|
const flagRows = [
|
|
@@ -2723,6 +2757,7 @@ function help() {
|
|
|
2723
2757
|
["--manual", "Plain text prompts instead of the review screen (init)"],
|
|
2724
2758
|
["--verbose", "Show detection sources for every field (init)"],
|
|
2725
2759
|
["--minimal", "Generate a minimal forge.yaml with required sections only (init)"],
|
|
2760
|
+
["--version, -v", "Print the installed version and check for updates"],
|
|
2726
2761
|
];
|
|
2727
2762
|
|
|
2728
2763
|
process.stdout.write(
|
|
@@ -2735,46 +2770,88 @@ function help() {
|
|
|
2735
2770
|
|
|
2736
2771
|
/**
|
|
2737
2772
|
* `forgedock run <command> [args...]` — execute a ForgeDock command spec
|
|
2738
|
-
*
|
|
2773
|
+
* headlessly, outside of an interactive Claude Code session (CI or local).
|
|
2739
2774
|
*
|
|
2740
2775
|
* Flags:
|
|
2741
|
-
* --dry-run
|
|
2742
|
-
* --model <id>
|
|
2743
|
-
* --max-iterations <n>
|
|
2776
|
+
* --dry-run Preview the assembled prompt + tool plan; no call made.
|
|
2777
|
+
* --model <id> Override the model (see resolution order below; api backend only).
|
|
2778
|
+
* --max-iterations <n> Bound the tool-use loop (default: 50; api backend only).
|
|
2779
|
+
* --backend <cli|api|auto> Execution backend (see below; default: auto).
|
|
2744
2780
|
*
|
|
2745
|
-
*
|
|
2746
|
-
*
|
|
2781
|
+
* Two execution backends (issue #2003):
|
|
2782
|
+
* - "cli": shells out to the local Claude Code CLI (`claude --print ...`),
|
|
2783
|
+
* reusing whatever credentials it already has (Pro/Max OAuth or a
|
|
2784
|
+
* CLI-managed key). No ANTHROPIC_API_KEY needed. Requires `claude` on PATH.
|
|
2785
|
+
* - "api": drives the Anthropic SDK directly. Requires ANTHROPIC_API_KEY and
|
|
2786
|
+
* the optional @anthropic-ai/sdk dependency. Needed for CI/headless
|
|
2787
|
+
* environments without an interactively-authenticated `claude` CLI.
|
|
2788
|
+
* - "auto" (default): prefers "cli" when a working `claude` install is
|
|
2789
|
+
* detected, otherwise falls back to "api" unchanged — existing callers
|
|
2790
|
+
* that never pass --backend see no behavior change when `claude` isn't
|
|
2791
|
+
* installed.
|
|
2747
2792
|
*
|
|
2748
|
-
*
|
|
2793
|
+
* The runtime itself lives in bin/runner.mjs.
|
|
2794
|
+
*
|
|
2795
|
+
* Model resolution order (highest precedence first; api backend only — the
|
|
2796
|
+
* cli backend uses whatever model the `claude` CLI itself is configured for):
|
|
2749
2797
|
* 1. --model <id> This flag.
|
|
2750
2798
|
* 2. $FORGEDOCK_MODEL Env var, below.
|
|
2751
2799
|
* 3. forge.yaml `agents.default_model` Read from the run's cwd, if present.
|
|
2752
2800
|
* 4. Hardcoded default "claude-sonnet-5" (bin/runner.mjs DEFAULT_MODEL).
|
|
2753
2801
|
*
|
|
2754
2802
|
* Env:
|
|
2755
|
-
*
|
|
2756
|
-
*
|
|
2757
|
-
*
|
|
2758
|
-
*
|
|
2803
|
+
* FORGEDOCK_BACKEND Default backend ("cli"|"api"|"auto") when --backend is omitted.
|
|
2804
|
+
* FORGEDOCK_MODEL Default model id when --model is omitted.
|
|
2805
|
+
* FORGEDOCK_CLI_TIMEOUT_MS Wall-clock timeout (ms) for the cli backend invocation.
|
|
2806
|
+
* FORGEDOCK_SHELL Override the shell used by run_bash / the cli backend.
|
|
2807
|
+
* Defaults to bash when found (Git Bash / WSL on
|
|
2808
|
+
* Windows, /bin/bash on POSIX), falling back to the
|
|
2809
|
+
* platform default shell otherwise.
|
|
2759
2810
|
*/
|
|
2760
2811
|
async function run() {
|
|
2761
2812
|
const runArgs = restArgs;
|
|
2762
2813
|
let dryRun = false;
|
|
2763
2814
|
let model;
|
|
2764
2815
|
let maxIterations;
|
|
2816
|
+
let backend;
|
|
2765
2817
|
const positional = [];
|
|
2818
|
+
// Consume the value for a `--flag <value>` pair at index `idx` (the flag
|
|
2819
|
+
// itself). Errors loudly instead of silently returning `undefined` when
|
|
2820
|
+
// the flag is the last token or is immediately followed by another flag —
|
|
2821
|
+
// both cases previously left the corresponding variable `undefined`,
|
|
2822
|
+
// which downstream validation treats identically to "flag omitted",
|
|
2823
|
+
// silently falling back to the default instead of erroring. Returns the
|
|
2824
|
+
// consumed value; the caller is responsible for advancing its own loop
|
|
2825
|
+
// index past the consumed token.
|
|
2826
|
+
const requireFlagValue = (flagName, idx) => {
|
|
2827
|
+
const value = runArgs[idx + 1];
|
|
2828
|
+
if (value === undefined || value.startsWith("--")) {
|
|
2829
|
+
process.stderr.write(
|
|
2830
|
+
`${RED}Missing value for ${flagName}. Usage: ${flagName} <value>${RESET}\n`,
|
|
2831
|
+
);
|
|
2832
|
+
process.exit(1);
|
|
2833
|
+
}
|
|
2834
|
+
return value;
|
|
2835
|
+
};
|
|
2766
2836
|
for (let i = 0; i < runArgs.length; i++) {
|
|
2767
2837
|
const a = runArgs[i];
|
|
2768
2838
|
if (a === "--dry-run") {
|
|
2769
2839
|
dryRun = true;
|
|
2770
2840
|
} else if (a === "--model") {
|
|
2771
|
-
model =
|
|
2841
|
+
model = requireFlagValue("--model", i);
|
|
2842
|
+
i++;
|
|
2772
2843
|
} else if (a.startsWith("--model=")) {
|
|
2773
2844
|
model = a.slice("--model=".length);
|
|
2774
2845
|
} else if (a === "--max-iterations") {
|
|
2775
|
-
maxIterations = parseInt(
|
|
2846
|
+
maxIterations = parseInt(requireFlagValue("--max-iterations", i), 10);
|
|
2847
|
+
i++;
|
|
2776
2848
|
} else if (a.startsWith("--max-iterations=")) {
|
|
2777
2849
|
maxIterations = parseInt(a.slice("--max-iterations=".length), 10);
|
|
2850
|
+
} else if (a === "--backend") {
|
|
2851
|
+
backend = requireFlagValue("--backend", i);
|
|
2852
|
+
i++;
|
|
2853
|
+
} else if (a.startsWith("--backend=")) {
|
|
2854
|
+
backend = a.slice("--backend=".length);
|
|
2778
2855
|
} else {
|
|
2779
2856
|
positional.push(a);
|
|
2780
2857
|
}
|
|
@@ -2785,12 +2862,25 @@ async function run() {
|
|
|
2785
2862
|
|
|
2786
2863
|
if (!commandName) {
|
|
2787
2864
|
process.stderr.write(
|
|
2788
|
-
`${RED}Usage: forgedock run <command> [args...] [--dry-run] [--model <id>] [--max-iterations <n>]${RESET}\n`,
|
|
2865
|
+
`${RED}Usage: forgedock run <command> [args...] [--dry-run] [--model <id>] [--max-iterations <n>] [--backend <cli|api|auto>]${RESET}\n`,
|
|
2866
|
+
);
|
|
2867
|
+
process.exit(1);
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
// VALID_BACKENDS is imported from bin/runner.mjs (not re-hardcoded here) so
|
|
2871
|
+
// this CLI-layer flag check and runner.mjs's own library-layer
|
|
2872
|
+
// resolveBackend() validation can never structurally diverge on the set of
|
|
2873
|
+
// accepted values or the wording of the resulting error message (issue
|
|
2874
|
+
// #2013 — the two were previously independently-hardcoded Sets with
|
|
2875
|
+
// near-identical but not-quite-matching error text).
|
|
2876
|
+
const { runCommand, VALID_BACKENDS } = await import("./runner.mjs");
|
|
2877
|
+
if (backend !== undefined && !VALID_BACKENDS.has(backend)) {
|
|
2878
|
+
process.stderr.write(
|
|
2879
|
+
`${RED}Invalid --backend "${backend}". Must be one of: ${[...VALID_BACKENDS].join(", ")}.${RESET}\n`,
|
|
2789
2880
|
);
|
|
2790
2881
|
process.exit(1);
|
|
2791
2882
|
}
|
|
2792
2883
|
|
|
2793
|
-
const { runCommand } = await import("./runner.mjs");
|
|
2794
2884
|
try {
|
|
2795
2885
|
const result = await runCommand({
|
|
2796
2886
|
commandsDir: COMMANDS_DIR,
|
|
@@ -2802,6 +2892,7 @@ async function run() {
|
|
|
2802
2892
|
...(Number.isInteger(maxIterations) && maxIterations > 0
|
|
2803
2893
|
? { maxIterations }
|
|
2804
2894
|
: {}),
|
|
2895
|
+
...(backend ? { backend } : {}),
|
|
2805
2896
|
});
|
|
2806
2897
|
// Treat a non-clean stop (iteration cap hit, or a max_tokens-truncated
|
|
2807
2898
|
// turn) as a failed run so CI/headless callers notice.
|
|
@@ -3051,7 +3142,8 @@ async function watch() {
|
|
|
3051
3142
|
const SPLASH_COMMANDS = new Set(["run", "run-issue", "resume-stalled", "demo", "doctor", "watch", "help", "--help", "-h"]);
|
|
3052
3143
|
const KNOWN_COMMANDS = new Set([
|
|
3053
3144
|
"install", "init", "enable", "disable", "status", "uninstall", "update",
|
|
3054
|
-
"run", "run-issue", "resume-stalled", "demo", "doctor", "watch", "labels", "help", "--help", "-h",
|
|
3145
|
+
"run", "run-issue", "resume-stalled", "demo", "doctor", "watch", "labels", "config", "help", "--help", "-h",
|
|
3146
|
+
"version", "--version", "-v",
|
|
3055
3147
|
]);
|
|
3056
3148
|
if (SPLASH_COMMANDS.has(command) || !KNOWN_COMMANDS.has(command)) splash(command);
|
|
3057
3149
|
|
|
@@ -3200,6 +3292,60 @@ switch (command) {
|
|
|
3200
3292
|
}
|
|
3201
3293
|
break;
|
|
3202
3294
|
}
|
|
3295
|
+
case "config": {
|
|
3296
|
+
const subcommand = positional[1];
|
|
3297
|
+
if (subcommand === "migrate") {
|
|
3298
|
+
// Directory override mirrors `status [dir]` — defaults to cwd.
|
|
3299
|
+
const dir = positional[2] ? resolve(positional[2]) : process.cwd();
|
|
3300
|
+
const result = backfillForgeYaml(dir);
|
|
3301
|
+
if (!result.present) {
|
|
3302
|
+
console.log(
|
|
3303
|
+
`${RED}No forge.yaml found in ${dir}.${RESET}\n` +
|
|
3304
|
+
` Run ${cyan("npx forgedock init")} to generate one.`,
|
|
3305
|
+
);
|
|
3306
|
+
exitCode = 1;
|
|
3307
|
+
} else if (result.added.length === 0) {
|
|
3308
|
+
console.log(
|
|
3309
|
+
`${GREEN}forge.yaml already has all ${result.alreadyPresent.length} optional sections.${RESET} Nothing to migrate.`,
|
|
3310
|
+
);
|
|
3311
|
+
} else {
|
|
3312
|
+
console.log(`${GREEN}Backfilled ${result.added.length} missing optional section(s) into forge.yaml:${RESET}`);
|
|
3313
|
+
for (const key of result.added) console.log(` ${GREEN}+${RESET} ${key}`);
|
|
3314
|
+
console.log(`\n Sections already present (unchanged): ${result.alreadyPresent.join(", ") || "none"}`);
|
|
3315
|
+
console.log(`\n All added sections are commented out — edit forge.yaml to enable the ones you need.`);
|
|
3316
|
+
}
|
|
3317
|
+
} else {
|
|
3318
|
+
console.log(`${RED}Unknown config subcommand: ${subcommand ?? "(none)"}${RESET}`);
|
|
3319
|
+
console.log(
|
|
3320
|
+
`Usage: ${CYAN}npx forgedock config migrate [dir]${RESET}\n` +
|
|
3321
|
+
` Backfills any of the 16 optional forge.yaml sections missing from an existing config (idempotent).`,
|
|
3322
|
+
);
|
|
3323
|
+
exitCode = 1;
|
|
3324
|
+
}
|
|
3325
|
+
break;
|
|
3326
|
+
}
|
|
3327
|
+
case "version":
|
|
3328
|
+
case "--version":
|
|
3329
|
+
case "-v": {
|
|
3330
|
+
// Print the local version immediately — must work offline/instantly,
|
|
3331
|
+
// no network dependency for the primary output (getVersion() reads
|
|
3332
|
+
// package.json off disk only). The latest-version check below is
|
|
3333
|
+
// best-effort: fetchLatestVersion() has its own 5s timeout and never
|
|
3334
|
+
// rejects, so a slow/offline network never blocks or fails this
|
|
3335
|
+
// command. Mirrors the version-check message pattern already used by
|
|
3336
|
+
// update() (see the npm/npx branch above).
|
|
3337
|
+
const localVersion = getVersion();
|
|
3338
|
+
console.log(`forgedock ${localVersion ? `v${localVersion}` : `${YELLOW}version unknown${RESET}`}`);
|
|
3339
|
+
|
|
3340
|
+
const latestVersion = await fetchLatestVersion();
|
|
3341
|
+
if (latestVersion && localVersion && compareVersions(latestVersion, localVersion) > 0) {
|
|
3342
|
+
console.log(
|
|
3343
|
+
` ${GREEN}New version available: v${latestVersion}${RESET} (you have v${localVersion}). ` +
|
|
3344
|
+
`Run ${CYAN}npx forgedock update${RESET} to fetch it.`,
|
|
3345
|
+
);
|
|
3346
|
+
}
|
|
3347
|
+
break;
|
|
3348
|
+
}
|
|
3203
3349
|
case "help":
|
|
3204
3350
|
case "--help":
|
|
3205
3351
|
case "-h":
|