forgedock 1.7.0 → 1.7.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/README.md +2 -1
- package/bin/engine-cli.mjs +2 -0
- package/bin/forgedock.mjs +109 -46
- package/bin/opencode-adapter.mjs +618 -92
- package/bin/runner.mjs +47 -5
- package/commands/orchestrate/phase-3-dependency.md +26 -9
- package/commands/orchestrate/phase-4-execution.md +42 -3
- package/commands/review-pr-agents.md +8 -0
- package/commands/review-pr-staging.md +18 -0
- package/commands/review-pr.md +20 -2
- package/docs/CONFIG.md +6 -5
- package/docs/OPENCODE.md +113 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -231,7 +231,8 @@ npx forgedock # checks your environment, installs commands into ~/.claude/comman
|
|
|
231
231
|
```
|
|
232
232
|
|
|
233
233
|
For OpenCode, install namespaced native commands without changing your provider
|
|
234
|
-
or
|
|
234
|
+
or user-owned settings. When migrating an older ForgeDock adapter, it removes
|
|
235
|
+
only exact ForgeDock-managed legacy entries from `opencode.json`:
|
|
235
236
|
|
|
236
237
|
```bash
|
|
237
238
|
npx forgedock opencode install
|
package/bin/engine-cli.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { join } from "node:path";
|
|
|
12
12
|
import { runIssue, DEFAULT_MAX_ATTEMPTS } from "./engine.mjs";
|
|
13
13
|
import { makeProjector } from "./engine/projector.mjs";
|
|
14
14
|
import { readLog, deriveState } from "./engine/runlog.mjs";
|
|
15
|
+
import { assertOpenCodeNativeRuntime } from "./runner.mjs";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Workflow labels that mark an issue as "in the pipeline" (not yet terminal).
|
|
@@ -340,6 +341,7 @@ export function lastLocalRun(dir) {
|
|
|
340
341
|
export async function runFromCli(argv, deps = {}) {
|
|
341
342
|
const issue = parseInt(argv[0], 10);
|
|
342
343
|
if (!Number.isInteger(issue)) throw new Error("usage: forgedock run-issue <issue-number> --lane <lane>");
|
|
344
|
+
assertOpenCodeNativeRuntime({ operation: "forgedock run-issue" });
|
|
343
345
|
const lane = flag(argv, "--lane");
|
|
344
346
|
if (!lane) throw new Error("--lane is required: e.g. --lane main or --lane staging. No default to prevent accidental production targeting.");
|
|
345
347
|
const repo = flag(argv, "--repo");
|
package/bin/forgedock.mjs
CHANGED
|
@@ -416,6 +416,40 @@ function getVersion() {
|
|
|
416
416
|
}
|
|
417
417
|
}
|
|
418
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Read the branch that owns the source currently being executed.
|
|
421
|
+
*
|
|
422
|
+
* This is deliberately best-effort: diagnostics should still identify the
|
|
423
|
+
* source path and version when the checkout has no readable branch ref.
|
|
424
|
+
*
|
|
425
|
+
* @param {string} [dir]
|
|
426
|
+
* @returns {string|null}
|
|
427
|
+
*/
|
|
428
|
+
function getGitBranch(dir = FORGE_HOME) {
|
|
429
|
+
try {
|
|
430
|
+
const branch = execFileSync(
|
|
431
|
+
"git",
|
|
432
|
+
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
433
|
+
{ cwd: dir, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] },
|
|
434
|
+
).trim();
|
|
435
|
+
return branch || null;
|
|
436
|
+
} catch {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Describe the source path the current CLI process resolves from.
|
|
443
|
+
*
|
|
444
|
+
* @param {string|null} [branch]
|
|
445
|
+
* @returns {string}
|
|
446
|
+
*/
|
|
447
|
+
function formatSourceIdentity(branch = null) {
|
|
448
|
+
const version = getVersion() || "unknown";
|
|
449
|
+
const branchLabel = branch ? `, branch ${branch}` : "";
|
|
450
|
+
return `${FORGE_HOME} (v${version}${branchLabel})`;
|
|
451
|
+
}
|
|
452
|
+
|
|
419
453
|
/**
|
|
420
454
|
* Compare two dotted version strings component-wise (numeric, not
|
|
421
455
|
* lexicographic — "1.9.0" must compare as older than "1.10.0").
|
|
@@ -1330,11 +1364,51 @@ async function uninstall() {
|
|
|
1330
1364
|
console.log("");
|
|
1331
1365
|
}
|
|
1332
1366
|
|
|
1333
|
-
// Reinstall = relink commands + re-register the hook
|
|
1334
|
-
// journey: update must not reach
|
|
1335
|
-
// curated forge.yaml (and re-runs AI
|
|
1336
|
-
// idempotent, so it's the repair path for a
|
|
1337
|
-
// symlinks
|
|
1367
|
+
// Reinstall = relink commands + re-register the hook + refresh an already
|
|
1368
|
+
// installed OpenCode adapter. Never the full journey: update must not reach
|
|
1369
|
+
// read/review, which could overwrite a curated forge.yaml (and re-runs AI
|
|
1370
|
+
// enrichment on every update). Also idempotent, so it's the repair path for a
|
|
1371
|
+
// configured repo whose symlinks, hook registration, or managed OpenCode files
|
|
1372
|
+
// got out of sync.
|
|
1373
|
+
async function refreshManagedOpenCodeAdapter() {
|
|
1374
|
+
try {
|
|
1375
|
+
const { getOpenCodeAdapterStatus, installOpenCodeAdapter } = await import("./opencode-adapter.mjs");
|
|
1376
|
+
const status = await getOpenCodeAdapterStatus({ home: HOME, env: process.env });
|
|
1377
|
+
if (!status.installed) return;
|
|
1378
|
+
|
|
1379
|
+
// npm/npx payloads must be persisted before the generated adapter points at
|
|
1380
|
+
// them; git-clone installs are already stable and persistHome returns them
|
|
1381
|
+
// unchanged.
|
|
1382
|
+
const persisted = await persistHome(ctx());
|
|
1383
|
+
const adapterForgeHome = persisted.forgeHome || FORGE_HOME;
|
|
1384
|
+
if (adapterForgeHome === FORGE_HOME && isEphemeralCachePath(FORGE_HOME)) {
|
|
1385
|
+
console.log(
|
|
1386
|
+
` ${YELLOW}OpenCode adapter refresh skipped: the current ForgeDock payload is ephemeral.${RESET}`,
|
|
1387
|
+
);
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
const result = await installOpenCodeAdapter({
|
|
1392
|
+
forgeHome: adapterForgeHome,
|
|
1393
|
+
home: HOME,
|
|
1394
|
+
env: process.env,
|
|
1395
|
+
includeExtras: status.manifest?.includeExtras === true,
|
|
1396
|
+
});
|
|
1397
|
+
console.log(
|
|
1398
|
+
` ${GREEN}Refreshed managed OpenCode adapter (${result.commandCount} commands, ${result.skillCount} skills).${RESET}`,
|
|
1399
|
+
);
|
|
1400
|
+
for (const warning of result.migration.warnings) {
|
|
1401
|
+
console.log(` ${YELLOW}OpenCode adapter warning: ${warning}${RESET}`);
|
|
1402
|
+
}
|
|
1403
|
+
} catch (error) {
|
|
1404
|
+
// OpenCode refresh is best-effort, matching the existing update/relink
|
|
1405
|
+
// behavior: a permissions or ownership problem must not block Claude users.
|
|
1406
|
+
console.log(
|
|
1407
|
+
` ${YELLOW}Could not refresh the managed OpenCode adapter: ${error?.message || String(error)}${RESET}`,
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1338
1412
|
async function relinkAndHint() {
|
|
1339
1413
|
const c = ctx();
|
|
1340
1414
|
const forged = await forge(c);
|
|
@@ -1346,6 +1420,7 @@ async function relinkAndHint() {
|
|
|
1346
1420
|
// (that path calls statusScreen(), never relinkAndHint()) — see the
|
|
1347
1421
|
// writeInstallReceipt() JSDoc in journey.mjs for the full picture.
|
|
1348
1422
|
await writeInstallReceipt(c, { forged });
|
|
1423
|
+
await refreshManagedOpenCodeAdapter();
|
|
1349
1424
|
if (!existsSync(join(c.cwd, "forge.yaml"))) {
|
|
1350
1425
|
const dim = (s) => (c.mode === "none" ? s : `\x1b[2m${s}\x1b[22m`);
|
|
1351
1426
|
c.stdout.write(" " + dim("Configure this repo: npx forgedock init") + "\n");
|
|
@@ -1808,24 +1883,18 @@ async function update() {
|
|
|
1808
1883
|
const separateGlobalInstall = findSeparateGlobalInstall();
|
|
1809
1884
|
if (separateGlobalInstall) {
|
|
1810
1885
|
console.log(
|
|
1811
|
-
` ${YELLOW}Note: this
|
|
1886
|
+
` ${YELLOW}Note: this command resolves to the git clone above. Your global npm install at ${separateGlobalInstall} is NOT touched.${RESET}`,
|
|
1812
1887
|
);
|
|
1813
1888
|
console.log(
|
|
1814
1889
|
` ${YELLOW}Run 'npm update -g forgedock', or 'npx forgedock update' from outside a clone, to update it.${RESET}`,
|
|
1815
1890
|
);
|
|
1816
1891
|
}
|
|
1817
1892
|
try {
|
|
1818
|
-
const branch =
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
}).trim();
|
|
1893
|
+
const branch = getGitBranch();
|
|
1894
|
+
const branchLabel = branch || "unknown branch";
|
|
1895
|
+
console.log(` Source: ${formatSourceIdentity(branch)}`);
|
|
1822
1896
|
|
|
1823
|
-
|
|
1824
|
-
// Detached HEAD ("HEAD") is treated as non-restorable — we leave on main
|
|
1825
|
-
// after the update and warn the user.
|
|
1826
|
-
const isDetached = branch === "HEAD";
|
|
1827
|
-
const needsCheckout = branch !== "main";
|
|
1828
|
-
if (needsCheckout) {
|
|
1897
|
+
if (branch !== "main") {
|
|
1829
1898
|
// Dirty-tree guard: refuse to move HEAD in this clone if there are
|
|
1830
1899
|
// uncommitted TRACKED changes. Untracked files are intentionally
|
|
1831
1900
|
// excluded (--untracked-files=no) so a clean checkout with stray
|
|
@@ -1837,7 +1906,7 @@ async function update() {
|
|
|
1837
1906
|
).trim();
|
|
1838
1907
|
if (porcelain) {
|
|
1839
1908
|
console.log(
|
|
1840
|
-
` ${YELLOW}Working tree has uncommitted changes on branch ${
|
|
1909
|
+
` ${YELLOW}Working tree has uncommitted changes on branch ${branchLabel} — skipping git-based update.${RESET}`,
|
|
1841
1910
|
);
|
|
1842
1911
|
|
|
1843
1912
|
// forge#2460 — distinguish "developing on this checkout" from a
|
|
@@ -1882,16 +1951,13 @@ async function update() {
|
|
|
1882
1951
|
return;
|
|
1883
1952
|
}
|
|
1884
1953
|
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
);
|
|
1893
|
-
}
|
|
1894
|
-
execFileSync("git", ["checkout", "main", "--quiet"], { cwd: FORGE_HOME });
|
|
1954
|
+
console.log(
|
|
1955
|
+
` ${YELLOW}Source checkout is on ${branchLabel}, not main; skipping update to preserve this source.${RESET}`,
|
|
1956
|
+
);
|
|
1957
|
+
console.log(
|
|
1958
|
+
` Run 'npx forgedock update' from a main-branch checkout (or use 'npx forgedock@latest') to refresh ForgeDock.`,
|
|
1959
|
+
);
|
|
1960
|
+
return;
|
|
1895
1961
|
}
|
|
1896
1962
|
|
|
1897
1963
|
const before = execSync("git rev-parse HEAD", {
|
|
@@ -1914,20 +1980,6 @@ async function update() {
|
|
|
1914
1980
|
printGitCloneChangelogSummary(before, after);
|
|
1915
1981
|
}
|
|
1916
1982
|
await relinkAndHint();
|
|
1917
|
-
|
|
1918
|
-
// Restore original branch after a successful update.
|
|
1919
|
-
if (needsCheckout && !isDetached) {
|
|
1920
|
-
try {
|
|
1921
|
-
execFileSync("git", ["checkout", branch, "--quiet"], { cwd: FORGE_HOME });
|
|
1922
|
-
console.log(
|
|
1923
|
-
` Restored branch ${CYAN}${branch}${RESET}.`,
|
|
1924
|
-
);
|
|
1925
|
-
} catch {
|
|
1926
|
-
console.log(
|
|
1927
|
-
` ${YELLOW}⚠ Could not restore branch ${branch} — you are now on main.${RESET}`,
|
|
1928
|
-
);
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1931
1983
|
} catch (err) {
|
|
1932
1984
|
console.log(
|
|
1933
1985
|
` ${YELLOW}Cannot fast-forward — local changes exist. Skipping.${RESET}`,
|
|
@@ -2190,7 +2242,14 @@ async function doctor(fix = false) {
|
|
|
2190
2242
|
console.log("");
|
|
2191
2243
|
|
|
2192
2244
|
const { targetDir: TARGET_DIR } = detectInstallPaths();
|
|
2193
|
-
|
|
2245
|
+
const isGitCloneInstall = existsSync(join(FORGE_HOME, ".git"));
|
|
2246
|
+
const sourceBranch = isGitCloneInstall ? getGitBranch() : null;
|
|
2247
|
+
console.log(
|
|
2248
|
+
isGitCloneInstall
|
|
2249
|
+
? ` Mode: git clone at ${CYAN}${FORGE_HOME}${RESET}`
|
|
2250
|
+
: ` Mode: global (~/.claude)`,
|
|
2251
|
+
);
|
|
2252
|
+
console.log(` Source: ${formatSourceIdentity(sourceBranch)}`);
|
|
2194
2253
|
console.log("");
|
|
2195
2254
|
|
|
2196
2255
|
let failures = 0;
|
|
@@ -2717,7 +2776,6 @@ async function doctor(fix = false) {
|
|
|
2717
2776
|
// absence that isn't a problem should not read as one.
|
|
2718
2777
|
{
|
|
2719
2778
|
try {
|
|
2720
|
-
const isGitCloneInstall = existsSync(join(FORGE_HOME, ".git"));
|
|
2721
2779
|
if (isGitCloneInstall) {
|
|
2722
2780
|
pass("Persisted toolset home (~/.forge)", "skipped — git-clone install links directly from the clone");
|
|
2723
2781
|
} else {
|
|
@@ -3825,7 +3883,7 @@ switch (command) {
|
|
|
3825
3883
|
includeExtras: restArgs.includes("--extras"),
|
|
3826
3884
|
});
|
|
3827
3885
|
process.stdout.write(
|
|
3828
|
-
`Installed ${result.commandCount} OpenCode commands under ${result.configDir}.\n` +
|
|
3886
|
+
`Installed ${result.commandCount} OpenCode commands and ${result.skillCount} skills under ${result.configDir}.\n` +
|
|
3829
3887
|
"Restart OpenCode, then run /forge/work-on <issue>.\n",
|
|
3830
3888
|
);
|
|
3831
3889
|
if (result.migration.removedInstructionsFile || result.migration.removedConfigEntries > 0) {
|
|
@@ -3836,7 +3894,12 @@ switch (command) {
|
|
|
3836
3894
|
for (const warning of result.migration.warnings) process.stderr.write(`Warning: ${warning}\n`);
|
|
3837
3895
|
} else if (action === "status") {
|
|
3838
3896
|
const result = await getOpenCodeAdapterStatus({ home: HOME });
|
|
3839
|
-
if (
|
|
3897
|
+
if (result.legacy) {
|
|
3898
|
+
process.stdout.write(
|
|
3899
|
+
"A legacy ForgeDock OpenCode adapter is installed. Re-run: npx forgedock opencode install\n",
|
|
3900
|
+
);
|
|
3901
|
+
exitCode = 1;
|
|
3902
|
+
} else if (!result.installed) {
|
|
3840
3903
|
process.stdout.write(`OpenCode adapter is not installed under ${result.configDir}.\n`);
|
|
3841
3904
|
exitCode = 1;
|
|
3842
3905
|
} else if (!result.healthy) {
|
|
@@ -3847,7 +3910,7 @@ switch (command) {
|
|
|
3847
3910
|
exitCode = 1;
|
|
3848
3911
|
} else {
|
|
3849
3912
|
process.stdout.write(
|
|
3850
|
-
`OpenCode adapter is healthy (${result.manifest.commandCount} commands) under ${result.configDir}.\n`,
|
|
3913
|
+
`OpenCode adapter is healthy (${result.manifest.commandCount} commands, ${result.manifest.skillCount ?? 0} skills) under ${result.configDir}.\n`,
|
|
3851
3914
|
);
|
|
3852
3915
|
}
|
|
3853
3916
|
} else if (action === "uninstall") {
|