forgedock 1.1.11 → 1.3.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 +9 -3
- package/bin/cli-spawn-shared.mjs +25 -0
- package/bin/engine/phases.mjs +178 -28
- package/bin/engine/state.mjs +39 -7
- package/bin/engine-cli.mjs +85 -3
- package/bin/engine.mjs +71 -8
- package/bin/forgedock.mjs +322 -20
- package/bin/hooks/pre-tool-use.mjs +345 -10
- 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 +116 -15
- 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 +6 -5
- 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 +302 -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 +8 -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 +27 -4
- package/commands/recover-orphans.md +1 -1
- package/commands/replay.md +16 -2
- package/commands/review-pr-agents/protocols.md +4 -0
- package/commands/review-pr-agents/spec-cli.md +179 -0
- package/commands/review-pr-agents.md +7 -4
- package/commands/review-pr-staging.md +44 -15
- package/commands/review-pr.md +122 -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/upgrade-deps.md +518 -0
- package/commands/validate.md +16 -5
- 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 +64 -2
- package/commands/work-on/build.md +10 -2
- package/commands/work-on/close.md +1 -1
- package/commands/work-on/decompose.md +35 -25
- package/commands/work-on/investigate.md +2 -2
- package/commands/work-on/remediate.md +1 -1
- package/commands/work-on/review.md +5 -3
- package/commands/work-on-monolithic.md +1 -1
- package/commands/work-on.md +28 -7
- package/docs/AIDER.md +264 -0
- package/docs/AUTOPILOT-CRON.md +130 -0
- package/docs/CI.md +18 -0
- package/docs/CONFIG.md +41 -2
- package/docs/CURSOR.md +135 -0
- package/docs/OPENCODE.md +312 -0
- package/docs/demo.gif +0 -0
- package/docs/eval/README.md +4 -4
- package/docs/launch/show-hn.md +3 -3
- package/docs/site/annotations-explained.md +2 -2
- package/docs/site/architecture.md +180 -0
- package/docs/site/command-reference.md +38 -0
- package/docs/site/configuration.md +243 -0
- package/docs/site/for-companies.md +4 -4
- package/docs/site/forge-annotation-protocol.md +1 -1
- package/docs/site/github-is-the-memory.md +2 -2
- package/docs/site/how-it-works.md +1 -1
- package/docs/site/public/llms-full.txt +380 -0
- package/docs/site/public/llms.txt +35 -0
- package/docs/spec/review-protocol.md +6 -0
- package/docs/spec-graph-schema.md +3 -3
- package/package.json +2 -2
- package/packages/protocol/README.md +75 -0
- package/packages/protocol/src/card.js +114 -0
- package/packages/protocol/src/cli.js +4 -77
- package/packages/protocol/src/emit.js +14 -3
- package/packages/protocol/src/index.js +2 -1
- package/packages/protocol/src/parse.js +17 -8
- package/packages/protocol/src/types.js +1 -1
- package/packages/protocol/src/validate.js +11 -0
- package/scripts/check-command-side-effects.sh +261 -0
- package/scripts/check-spec-markers.sh +273 -0
- package/scripts/classify-lane.sh +29 -7
- package/scripts/forge-run.sh +322 -0
- package/templates/cross-repo-trigger.yml +282 -0
- package/templates/cursor/forge.mdc +148 -0
- package/templates/github-actions/forgedock-autopilot.yml +113 -0
package/bin/forgedock.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
4
|
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
|
-
import { dirname, join, relative, resolve } from "path";
|
|
6
|
+
import { dirname, join, relative, resolve, sep } from "path";
|
|
7
7
|
import { mkdir, lstat, readlink, readdir, unlink, readFile, writeFile } from "fs/promises";
|
|
8
8
|
import {
|
|
9
9
|
existsSync,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
renameSync,
|
|
14
14
|
unlinkSync,
|
|
15
15
|
} from "fs";
|
|
16
|
-
import { execSync, execFileSync } from "child_process";
|
|
16
|
+
import { execSync, execFileSync, spawnSync } from "child_process";
|
|
17
17
|
import { homedir } from "os";
|
|
18
18
|
import https from "https";
|
|
19
19
|
import {
|
|
@@ -951,6 +951,21 @@ async function statusScreen(c) {
|
|
|
951
951
|
c.stdout.write("\n");
|
|
952
952
|
}
|
|
953
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
|
+
*/
|
|
954
969
|
async function initFlow(c) {
|
|
955
970
|
const outputPath = join(c.cwd, "forge.yaml");
|
|
956
971
|
const hasExisting = existsSync(outputPath);
|
|
@@ -1460,6 +1475,216 @@ async function printVersionAvailableChangelog(localVersion, latestVersion) {
|
|
|
1460
1475
|
}
|
|
1461
1476
|
}
|
|
1462
1477
|
|
|
1478
|
+
/**
|
|
1479
|
+
* Detect whether FORGE_HOME resolves inside npm's global install tree, i.e.
|
|
1480
|
+
* this process is running as a globally-installed `forgedock` package (not
|
|
1481
|
+
* an ephemeral npx/dlx cache extraction). Used by update()'s npm-mode branch
|
|
1482
|
+
* (forge#2133) to decide whether a newer published version can be installed
|
|
1483
|
+
* in place with `npm install -g forgedock@latest`, versus the npx-cache case
|
|
1484
|
+
* where there is nothing durable to reinstall over.
|
|
1485
|
+
*
|
|
1486
|
+
* Resolution: `npm root -g` reports the global node_modules directory; if
|
|
1487
|
+
* FORGE_HOME sits under `{globalRoot}/forgedock`, this is a global install.
|
|
1488
|
+
* Fails closed (returns false) on any error — the pre-existing advisory-only
|
|
1489
|
+
* behavior is always a safe fallback when detection is inconclusive.
|
|
1490
|
+
*
|
|
1491
|
+
* @returns {boolean}
|
|
1492
|
+
*/
|
|
1493
|
+
function isGlobalNpmInstall() {
|
|
1494
|
+
try {
|
|
1495
|
+
const globalRoot = execSync("npm root -g", {
|
|
1496
|
+
encoding: "utf-8",
|
|
1497
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1498
|
+
timeout: 5000,
|
|
1499
|
+
}).trim();
|
|
1500
|
+
if (!globalRoot) return false;
|
|
1501
|
+
const globalPkgDir = resolve(join(globalRoot, "forgedock"));
|
|
1502
|
+
const home = resolve(FORGE_HOME);
|
|
1503
|
+
return home === globalPkgDir || home.startsWith(globalPkgDir + sep);
|
|
1504
|
+
} catch {
|
|
1505
|
+
return false;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
/**
|
|
1510
|
+
* Maximum number of additional self-update re-exec attempts allowed after the
|
|
1511
|
+
* first (i.e. up to MAX_SELF_UPDATE_ATTEMPTS + 1 total `npm install -g`
|
|
1512
|
+
* attempts across the parent process and its re-exec'd children). Guards
|
|
1513
|
+
* against unbounded recursion when `npm install -g` reports success but the
|
|
1514
|
+
* resolved version never actually advances (stale registry mirror/proxy,
|
|
1515
|
+
* cache issue, mismatched FORGE_HOME resolution) — see forge#2158.
|
|
1516
|
+
*/
|
|
1517
|
+
const MAX_SELF_UPDATE_ATTEMPTS = 1;
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* Strict semver-shape check for a version string before it is interpolated
|
|
1521
|
+
* into a shell-invoked command (see forge#2180). Intentionally narrow —
|
|
1522
|
+
* digits-dot-digits-dot-digits with an optional `-prerelease`/`+build`
|
|
1523
|
+
* suffix restricted to `[0-9A-Za-z.-]` — so it accepts every real value
|
|
1524
|
+
* `fetchLatestVersion()` can return from the npm registry's `version` field,
|
|
1525
|
+
* while rejecting anything containing shell metacharacters
|
|
1526
|
+
* (`; | & $ \` ( ) < > " ' \n` etc.) that `shell: true` would otherwise
|
|
1527
|
+
* hand straight to cmd.exe/`/bin/sh`.
|
|
1528
|
+
*
|
|
1529
|
+
* The regex above is fully anchored (`^...$`) but its quantifiers (`\d+`,
|
|
1530
|
+
* `[0-9A-Za-z.-]+`) are unbounded, so shape validity alone does not bound
|
|
1531
|
+
* string *length* — a pathologically long value could still match. This
|
|
1532
|
+
* is defense-in-depth only (see forge#2195): the character-class
|
|
1533
|
+
* restriction already rules out shell metacharacters at any length, and
|
|
1534
|
+
* `fetchLatestVersion()`'s 65536-byte HTTP response cap already bounds
|
|
1535
|
+
* `version` in practice. The explicit length cap below rejects oversized
|
|
1536
|
+
* input before the regex ever runs, closing the gap outright rather than
|
|
1537
|
+
* relying on an upstream cap this function has no visibility into.
|
|
1538
|
+
*
|
|
1539
|
+
* @param {string} version
|
|
1540
|
+
* @returns {boolean}
|
|
1541
|
+
*/
|
|
1542
|
+
function isValidSemverShape(version) {
|
|
1543
|
+
const MAX_VERSION_LENGTH = 100;
|
|
1544
|
+
return (
|
|
1545
|
+
typeof version === "string" &&
|
|
1546
|
+
version.length <= MAX_VERSION_LENGTH &&
|
|
1547
|
+
/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
/**
|
|
1552
|
+
* Install the newer published version globally via `npm install -g
|
|
1553
|
+
* forgedock@{version}`, then re-exec this same file path (which npm has just
|
|
1554
|
+
* overwritten with the new package's contents) so the rest of update()'s
|
|
1555
|
+
* persist/relink phase runs against the *new* payload instead of the stale
|
|
1556
|
+
* in-memory one (forge#2133 — previously the advisory-only branch would
|
|
1557
|
+
* persist/relink from whatever was already resolved, silently downgrading
|
|
1558
|
+
* a newer persisted ~/.forge/ or leaving the global install stuck on the
|
|
1559
|
+
* old version indefinitely).
|
|
1560
|
+
*
|
|
1561
|
+
* Depth guard (forge#2158): the re-exec'd child process could, in principle,
|
|
1562
|
+
* observe the same "newer version available" condition again (e.g. a stale
|
|
1563
|
+
* registry mirror/proxy reports success without the resolved version ever
|
|
1564
|
+
* advancing) and recurse indefinitely, each cycle blocking up to ~125s. The
|
|
1565
|
+
* attempt count is propagated across the re-exec via the
|
|
1566
|
+
* `FORGEDOCK_SELF_UPDATE_ATTEMPT` environment variable (parent process env is
|
|
1567
|
+
* never mutated — the counter is only passed to the spawned child's own
|
|
1568
|
+
* environment). Once `MAX_SELF_UPDATE_ATTEMPTS` is reached, this function
|
|
1569
|
+
* fails loudly with an actionable message and returns `false` instead of
|
|
1570
|
+
* attempting another install/re-exec cycle.
|
|
1571
|
+
*
|
|
1572
|
+
* Fail-open: any error during install or re-exec falls back to the
|
|
1573
|
+
* pre-existing advisory message — the caller must treat a `false` return as
|
|
1574
|
+
* "could not self-update, printed manual instructions instead."
|
|
1575
|
+
*
|
|
1576
|
+
* Windows note (forge#2180): `npm` is a `.cmd` batch launcher on Windows, and
|
|
1577
|
+
* Node's `execFileSync`/`spawnSync` cannot invoke `.cmd`/`.bat` files without
|
|
1578
|
+
* `shell: true` (nodejs/node#3675) — confirmed empirically on a real Windows
|
|
1579
|
+
* 11 machine: `execFileSync("npm", [...])` ENOENTs, and even the exact
|
|
1580
|
+
* `npm.cmd` filename EINVALs, without a shell. `shell: true` is required
|
|
1581
|
+
* here so the install actually runs on Windows. Because that widens the
|
|
1582
|
+
* command line to shell interpretation, `version` — sourced from an
|
|
1583
|
+
* unauthenticated `https://registry.npmjs.org` response in
|
|
1584
|
+
* `fetchLatestVersion()` — is validated against a strict semver shape
|
|
1585
|
+
* (`isValidSemverShape`) before it is ever interpolated into the command,
|
|
1586
|
+
* closing the injection surface `shell: true` would otherwise open (see the
|
|
1587
|
+
* repo-wide execSync→execFileSync shell-injection hardening precedent in
|
|
1588
|
+
* forge#1703/forge#413).
|
|
1589
|
+
*
|
|
1590
|
+
* @param {string} version - target version to install (e.g. "1.2.0")
|
|
1591
|
+
* @returns {boolean} true if re-exec was launched (process will exit before
|
|
1592
|
+
* returning in the success path); false if self-update failed, was capped
|
|
1593
|
+
* by the depth guard, and the caller should fall back to advisory-only
|
|
1594
|
+
* behavior.
|
|
1595
|
+
*/
|
|
1596
|
+
function selfUpdateGlobalInstall(version) {
|
|
1597
|
+
// Math.max(0, ...) clamps out negative values: `Number.parseInt(x, 10) || 0`
|
|
1598
|
+
// only substitutes 0 for NaN/0/"" — a negative numeric string (e.g. a
|
|
1599
|
+
// corrupted or manually-set FORGEDOCK_SELF_UPDATE_ATTEMPT="-1") is truthy
|
|
1600
|
+
// and passes through unclamped, which would let a negative attempt count
|
|
1601
|
+
// stay perpetually below MAX_SELF_UPDATE_ATTEMPTS. Defense-in-depth only —
|
|
1602
|
+
// no known path sets this env var to a negative value today. (forge#2168)
|
|
1603
|
+
const attempt = Math.max(
|
|
1604
|
+
0,
|
|
1605
|
+
Number.parseInt(process.env.FORGEDOCK_SELF_UPDATE_ATTEMPT, 10) || 0,
|
|
1606
|
+
);
|
|
1607
|
+
|
|
1608
|
+
// `attempt` counts completed installs so far (0 = no installs yet). The
|
|
1609
|
+
// guard must allow MAX_SELF_UPDATE_ATTEMPTS + 1 total installs (see the
|
|
1610
|
+
// doc comment above this function) — i.e. it should only trip once
|
|
1611
|
+
// `attempt` has already reached that total, not one call early. Using
|
|
1612
|
+
// `>=` here tripped after only 1 install instead of the documented 2
|
|
1613
|
+
// (forge#2203 — the retry this guard exists to allow never actually ran).
|
|
1614
|
+
if (attempt > MAX_SELF_UPDATE_ATTEMPTS) {
|
|
1615
|
+
console.log(
|
|
1616
|
+
` ${YELLOW}Self-update did not converge after ${attempt} attempt(s) — the installed version may not be advancing (stale registry mirror/cache?).${RESET}`,
|
|
1617
|
+
);
|
|
1618
|
+
console.log(
|
|
1619
|
+
` Run ${CYAN}npm install -g forgedock@latest${RESET} manually, then ${CYAN}npx forgedock update${RESET} again.`,
|
|
1620
|
+
);
|
|
1621
|
+
return false;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// Validate before this value ever reaches the shell-invoked install below
|
|
1625
|
+
// (forge#2180) — fail open to the same advisory message a failed install
|
|
1626
|
+
// would print, rather than throwing or passing an unvalidated string to
|
|
1627
|
+
// `shell: true`.
|
|
1628
|
+
if (!isValidSemverShape(version)) {
|
|
1629
|
+
console.log(
|
|
1630
|
+
` ${YELLOW}Self-update aborted: received an unexpected version string ("${version}").${RESET}`,
|
|
1631
|
+
);
|
|
1632
|
+
console.log(
|
|
1633
|
+
` Run ${CYAN}npm install -g forgedock@latest${RESET} manually, then ${CYAN}npx forgedock update${RESET} again.`,
|
|
1634
|
+
);
|
|
1635
|
+
return false;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
try {
|
|
1639
|
+
console.log(` Installing ${CYAN}forgedock@${version}${RESET} globally...`);
|
|
1640
|
+
// shell: true is required to resolve npm.cmd on Windows (see the
|
|
1641
|
+
// Windows note in this function's doc comment, forge#2180). `version`
|
|
1642
|
+
// is validated as strict semver above before this point.
|
|
1643
|
+
execFileSync("npm", ["install", "-g", `forgedock@${version}`], {
|
|
1644
|
+
stdio: "inherit",
|
|
1645
|
+
timeout: 120000,
|
|
1646
|
+
shell: true,
|
|
1647
|
+
});
|
|
1648
|
+
} catch (err) {
|
|
1649
|
+
console.log(
|
|
1650
|
+
` ${YELLOW}Global self-update failed: ${err && err.message ? err.message : String(err)}${RESET}`,
|
|
1651
|
+
);
|
|
1652
|
+
console.log(
|
|
1653
|
+
` Run ${CYAN}npm install -g forgedock@latest${RESET} manually, then ${CYAN}npx forgedock update${RESET} again.`,
|
|
1654
|
+
);
|
|
1655
|
+
return false;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
console.log(
|
|
1659
|
+
` ${GREEN}Installed v${version}.${RESET} Re-running update to finish persist/relink...`,
|
|
1660
|
+
);
|
|
1661
|
+
try {
|
|
1662
|
+
const result = spawnSync(process.execPath, [__filename, "update"], {
|
|
1663
|
+
stdio: "inherit",
|
|
1664
|
+
env: {
|
|
1665
|
+
...process.env,
|
|
1666
|
+
FORGEDOCK_SELF_UPDATE_ATTEMPT: String(attempt + 1),
|
|
1667
|
+
},
|
|
1668
|
+
});
|
|
1669
|
+
// `result.signal` is set (non-null) when the re-exec'd child was
|
|
1670
|
+
// terminated by a signal (e.g. SIGTERM/SIGKILL) rather than exiting
|
|
1671
|
+
// cleanly — in that case `result.status` is `null`, and
|
|
1672
|
+
// `result.status ?? 0` would silently collapse a killed child into a
|
|
1673
|
+
// reported "success" (exit 0). Exit non-zero explicitly for the
|
|
1674
|
+
// signal-killed case; the non-signaled path is unchanged. (forge#2159)
|
|
1675
|
+
process.exit(result.signal ? 1 : (result.status ?? 0));
|
|
1676
|
+
// Unreachable — process.exit() above terminates the process. Present
|
|
1677
|
+
// only to satisfy linters expecting a return on every path.
|
|
1678
|
+
return true;
|
|
1679
|
+
} catch (err) {
|
|
1680
|
+
console.log(
|
|
1681
|
+
` ${YELLOW}Could not re-run update after install: ${err && err.message ? err.message : String(err)}${RESET}`,
|
|
1682
|
+
);
|
|
1683
|
+
console.log(` Run ${CYAN}npx forgedock update${RESET} again manually.`);
|
|
1684
|
+
return false;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1463
1688
|
async function update() {
|
|
1464
1689
|
console.log("");
|
|
1465
1690
|
console.log(`${BOLD}ForgeDock${RESET} — Checking for updates`);
|
|
@@ -1554,8 +1779,25 @@ async function update() {
|
|
|
1554
1779
|
console.log(
|
|
1555
1780
|
` ${GREEN}New version available: v${latestVersion}${RESET} (you have v${localVersion}).`,
|
|
1556
1781
|
);
|
|
1557
|
-
console.log(` Run ${CYAN}npx forgedock@latest${RESET} to fetch it.`);
|
|
1558
1782
|
await printVersionAvailableChangelog(localVersion, latestVersion);
|
|
1783
|
+
|
|
1784
|
+
// forge#2133 — a global npm install can actually be upgraded in place;
|
|
1785
|
+
// do that instead of only printing an advisory (which previously left
|
|
1786
|
+
// global installs stuck on whatever version was first installed,
|
|
1787
|
+
// since every subsequent `npx forgedock update` re-resolves the same
|
|
1788
|
+
// stale global binary). selfUpdateGlobalInstall() re-execs and exits
|
|
1789
|
+
// the process on success, so anything after this block only runs when
|
|
1790
|
+
// self-update wasn't attempted or failed.
|
|
1791
|
+
if (isGlobalNpmInstall()) {
|
|
1792
|
+
selfUpdateGlobalInstall(latestVersion);
|
|
1793
|
+
// selfUpdateGlobalInstall() only returns (rather than exiting the
|
|
1794
|
+
// process) when the install or re-exec failed — it already printed
|
|
1795
|
+
// manual-recovery instructions, so fall through to the persist
|
|
1796
|
+
// refresh below using the still-stale local payload (fail-open,
|
|
1797
|
+
// matches pre-existing behavior for this failure case).
|
|
1798
|
+
} else {
|
|
1799
|
+
console.log(` Run ${CYAN}npx forgedock@latest${RESET} to fetch it.`);
|
|
1800
|
+
}
|
|
1559
1801
|
} else {
|
|
1560
1802
|
console.log(` Already up to date (v${localVersion}).`);
|
|
1561
1803
|
}
|
|
@@ -2721,7 +2963,7 @@ function help() {
|
|
|
2721
2963
|
["npx forgedock enable [dir]", "Activate ForgeDock in a directory"],
|
|
2722
2964
|
["npx forgedock disable [dir]", "Opt a directory out of ForgeDock"],
|
|
2723
2965
|
["npx forgedock status [dir]", "Show ForgeDock state for a directory"],
|
|
2724
|
-
["npx forgedock run <cmd> [args]", "Run a command headlessly
|
|
2966
|
+
["npx forgedock run <cmd> [args]", "Run a command headlessly (local claude CLI or Anthropic API)"],
|
|
2725
2967
|
["npx forgedock run-issue <issue>", "Drive one issue through the durable engine"],
|
|
2726
2968
|
["npx forgedock resume-stalled [--dry-run]", "Fleet stall recovery — re-dispatch expired-lease issues"],
|
|
2727
2969
|
["npx forgedock demo", "Set up a risk-free demo repo and print next steps"],
|
|
@@ -2755,46 +2997,88 @@ function help() {
|
|
|
2755
2997
|
|
|
2756
2998
|
/**
|
|
2757
2999
|
* `forgedock run <command> [args...]` — execute a ForgeDock command spec
|
|
2758
|
-
*
|
|
3000
|
+
* headlessly, outside of an interactive Claude Code session (CI or local).
|
|
2759
3001
|
*
|
|
2760
3002
|
* Flags:
|
|
2761
|
-
* --dry-run
|
|
2762
|
-
* --model <id>
|
|
2763
|
-
* --max-iterations <n>
|
|
3003
|
+
* --dry-run Preview the assembled prompt + tool plan; no call made.
|
|
3004
|
+
* --model <id> Override the model (see resolution order below; api backend only).
|
|
3005
|
+
* --max-iterations <n> Bound the tool-use loop (default: 50; api backend only).
|
|
3006
|
+
* --backend <cli|api|auto> Execution backend (see below; default: auto).
|
|
2764
3007
|
*
|
|
2765
|
-
*
|
|
2766
|
-
*
|
|
3008
|
+
* Two execution backends (issue #2003):
|
|
3009
|
+
* - "cli": shells out to the local Claude Code CLI (`claude --print ...`),
|
|
3010
|
+
* reusing whatever credentials it already has (Pro/Max OAuth or a
|
|
3011
|
+
* CLI-managed key). No ANTHROPIC_API_KEY needed. Requires `claude` on PATH.
|
|
3012
|
+
* - "api": drives the Anthropic SDK directly. Requires ANTHROPIC_API_KEY and
|
|
3013
|
+
* the optional @anthropic-ai/sdk dependency. Needed for CI/headless
|
|
3014
|
+
* environments without an interactively-authenticated `claude` CLI.
|
|
3015
|
+
* - "auto" (default): prefers "cli" when a working `claude` install is
|
|
3016
|
+
* detected, otherwise falls back to "api" unchanged — existing callers
|
|
3017
|
+
* that never pass --backend see no behavior change when `claude` isn't
|
|
3018
|
+
* installed.
|
|
2767
3019
|
*
|
|
2768
|
-
*
|
|
3020
|
+
* The runtime itself lives in bin/runner.mjs.
|
|
3021
|
+
*
|
|
3022
|
+
* Model resolution order (highest precedence first; api backend only — the
|
|
3023
|
+
* cli backend uses whatever model the `claude` CLI itself is configured for):
|
|
2769
3024
|
* 1. --model <id> This flag.
|
|
2770
3025
|
* 2. $FORGEDOCK_MODEL Env var, below.
|
|
2771
3026
|
* 3. forge.yaml `agents.default_model` Read from the run's cwd, if present.
|
|
2772
3027
|
* 4. Hardcoded default "claude-sonnet-5" (bin/runner.mjs DEFAULT_MODEL).
|
|
2773
3028
|
*
|
|
2774
3029
|
* Env:
|
|
2775
|
-
*
|
|
2776
|
-
*
|
|
2777
|
-
*
|
|
2778
|
-
*
|
|
3030
|
+
* FORGEDOCK_BACKEND Default backend ("cli"|"api"|"auto") when --backend is omitted.
|
|
3031
|
+
* FORGEDOCK_MODEL Default model id when --model is omitted.
|
|
3032
|
+
* FORGEDOCK_CLI_TIMEOUT_MS Wall-clock timeout (ms) for the cli backend invocation.
|
|
3033
|
+
* FORGEDOCK_SHELL Override the shell used by run_bash / the cli backend.
|
|
3034
|
+
* Defaults to bash when found (Git Bash / WSL on
|
|
3035
|
+
* Windows, /bin/bash on POSIX), falling back to the
|
|
3036
|
+
* platform default shell otherwise.
|
|
2779
3037
|
*/
|
|
2780
3038
|
async function run() {
|
|
2781
3039
|
const runArgs = restArgs;
|
|
2782
3040
|
let dryRun = false;
|
|
2783
3041
|
let model;
|
|
2784
3042
|
let maxIterations;
|
|
3043
|
+
let backend;
|
|
2785
3044
|
const positional = [];
|
|
3045
|
+
// Consume the value for a `--flag <value>` pair at index `idx` (the flag
|
|
3046
|
+
// itself). Errors loudly instead of silently returning `undefined` when
|
|
3047
|
+
// the flag is the last token or is immediately followed by another flag —
|
|
3048
|
+
// both cases previously left the corresponding variable `undefined`,
|
|
3049
|
+
// which downstream validation treats identically to "flag omitted",
|
|
3050
|
+
// silently falling back to the default instead of erroring. Returns the
|
|
3051
|
+
// consumed value; the caller is responsible for advancing its own loop
|
|
3052
|
+
// index past the consumed token.
|
|
3053
|
+
const requireFlagValue = (flagName, idx) => {
|
|
3054
|
+
const value = runArgs[idx + 1];
|
|
3055
|
+
if (value === undefined || value.startsWith("--")) {
|
|
3056
|
+
process.stderr.write(
|
|
3057
|
+
`${RED}Missing value for ${flagName}. Usage: ${flagName} <value>${RESET}\n`,
|
|
3058
|
+
);
|
|
3059
|
+
process.exit(1);
|
|
3060
|
+
}
|
|
3061
|
+
return value;
|
|
3062
|
+
};
|
|
2786
3063
|
for (let i = 0; i < runArgs.length; i++) {
|
|
2787
3064
|
const a = runArgs[i];
|
|
2788
3065
|
if (a === "--dry-run") {
|
|
2789
3066
|
dryRun = true;
|
|
2790
3067
|
} else if (a === "--model") {
|
|
2791
|
-
model =
|
|
3068
|
+
model = requireFlagValue("--model", i);
|
|
3069
|
+
i++;
|
|
2792
3070
|
} else if (a.startsWith("--model=")) {
|
|
2793
3071
|
model = a.slice("--model=".length);
|
|
2794
3072
|
} else if (a === "--max-iterations") {
|
|
2795
|
-
maxIterations = parseInt(
|
|
3073
|
+
maxIterations = parseInt(requireFlagValue("--max-iterations", i), 10);
|
|
3074
|
+
i++;
|
|
2796
3075
|
} else if (a.startsWith("--max-iterations=")) {
|
|
2797
3076
|
maxIterations = parseInt(a.slice("--max-iterations=".length), 10);
|
|
3077
|
+
} else if (a === "--backend") {
|
|
3078
|
+
backend = requireFlagValue("--backend", i);
|
|
3079
|
+
i++;
|
|
3080
|
+
} else if (a.startsWith("--backend=")) {
|
|
3081
|
+
backend = a.slice("--backend=".length);
|
|
2798
3082
|
} else {
|
|
2799
3083
|
positional.push(a);
|
|
2800
3084
|
}
|
|
@@ -2805,12 +3089,25 @@ async function run() {
|
|
|
2805
3089
|
|
|
2806
3090
|
if (!commandName) {
|
|
2807
3091
|
process.stderr.write(
|
|
2808
|
-
`${RED}Usage: forgedock run <command> [args...] [--dry-run] [--model <id>] [--max-iterations <n>]${RESET}\n`,
|
|
3092
|
+
`${RED}Usage: forgedock run <command> [args...] [--dry-run] [--model <id>] [--max-iterations <n>] [--backend <cli|api|auto>]${RESET}\n`,
|
|
3093
|
+
);
|
|
3094
|
+
process.exit(1);
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
// VALID_BACKENDS is imported from bin/runner.mjs (not re-hardcoded here) so
|
|
3098
|
+
// this CLI-layer flag check and runner.mjs's own library-layer
|
|
3099
|
+
// resolveBackend() validation can never structurally diverge on the set of
|
|
3100
|
+
// accepted values or the wording of the resulting error message (issue
|
|
3101
|
+
// #2013 — the two were previously independently-hardcoded Sets with
|
|
3102
|
+
// near-identical but not-quite-matching error text).
|
|
3103
|
+
const { runCommand, VALID_BACKENDS } = await import("./runner.mjs");
|
|
3104
|
+
if (backend !== undefined && !VALID_BACKENDS.has(backend)) {
|
|
3105
|
+
process.stderr.write(
|
|
3106
|
+
`${RED}Invalid --backend "${backend}". Must be one of: ${[...VALID_BACKENDS].join(", ")}.${RESET}\n`,
|
|
2809
3107
|
);
|
|
2810
3108
|
process.exit(1);
|
|
2811
3109
|
}
|
|
2812
3110
|
|
|
2813
|
-
const { runCommand } = await import("./runner.mjs");
|
|
2814
3111
|
try {
|
|
2815
3112
|
const result = await runCommand({
|
|
2816
3113
|
commandsDir: COMMANDS_DIR,
|
|
@@ -2822,6 +3119,7 @@ async function run() {
|
|
|
2822
3119
|
...(Number.isInteger(maxIterations) && maxIterations > 0
|
|
2823
3120
|
? { maxIterations }
|
|
2824
3121
|
: {}),
|
|
3122
|
+
...(backend ? { backend } : {}),
|
|
2825
3123
|
});
|
|
2826
3124
|
// Treat a non-clean stop (iteration cap hit, or a max_tokens-truncated
|
|
2827
3125
|
// turn) as a failed run so CI/headless callers notice.
|
|
@@ -3167,7 +3465,11 @@ switch (command) {
|
|
|
3167
3465
|
case "run-issue": {
|
|
3168
3466
|
const { runFromCli } = await import("./engine-cli.mjs");
|
|
3169
3467
|
try {
|
|
3170
|
-
await runFromCli(restArgs);
|
|
3468
|
+
const result = await runFromCli(restArgs);
|
|
3469
|
+
// forge#2175: make a needs-human escalation distinguishable from a
|
|
3470
|
+
// successful run by exit code, mirroring the resume-stalled case's
|
|
3471
|
+
// existing convention below (result.failed.length > 0 -> exitCode 1).
|
|
3472
|
+
if (result && result.terminalReason === "needs-human") exitCode = 1;
|
|
3171
3473
|
} catch (err) {
|
|
3172
3474
|
process.stderr.write(`${RED}${err.message}${RESET}\n`);
|
|
3173
3475
|
exitCode = 1;
|