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.
Files changed (109) hide show
  1. package/README.md +9 -3
  2. package/bin/cli-spawn-shared.mjs +25 -0
  3. package/bin/engine/phases.mjs +178 -28
  4. package/bin/engine/state.mjs +39 -7
  5. package/bin/engine-cli.mjs +85 -3
  6. package/bin/engine.mjs +71 -8
  7. package/bin/forgedock.mjs +322 -20
  8. package/bin/hooks/pre-tool-use.mjs +345 -10
  9. package/bin/init-enrich-api.mjs +12 -4
  10. package/bin/init-enrich-cli.mjs +328 -0
  11. package/bin/init-enrich.mjs +137 -0
  12. package/bin/journey.mjs +116 -15
  13. package/bin/runner.mjs +653 -10
  14. package/commands/adopt.md +1 -1
  15. package/commands/analytics.md +10 -7
  16. package/commands/audit-agents.md +29 -6
  17. package/commands/audit.md +9 -20
  18. package/commands/autopilot.md +52 -78
  19. package/commands/changelog.md +1 -1
  20. package/commands/ci-audit.md +19 -10
  21. package/commands/cleanup.md +1 -1
  22. package/commands/compat-audit.md +1 -1
  23. package/commands/deploy-info.md +1 -1
  24. package/commands/deploy-pr.md +1 -1
  25. package/commands/design-bench.md +1 -1
  26. package/commands/design-render-critique-loop.md +1 -1
  27. package/commands/diagnose.md +1 -1
  28. package/commands/forge-stats.md +6 -5
  29. package/commands/forgedock-init.md +1 -1
  30. package/commands/geo-audit.md +9 -6
  31. package/commands/incident-response.md +15 -5
  32. package/commands/issue.md +302 -15
  33. package/commands/milestone.md +45 -18
  34. package/commands/optimize.md +1 -1
  35. package/commands/orchestrate/phase-1-resolve.md +20 -5
  36. package/commands/orchestrate/phase-4-execution.md +8 -1
  37. package/commands/orchestrate/phase-5-cleanup.md +43 -2
  38. package/commands/orchestrate/phase-6-report.md +1 -0
  39. package/commands/orchestrate.md +1 -1
  40. package/commands/pipeline-health/phase-4-proposals.md +40 -10
  41. package/commands/pipeline-health/phase-5-report.md +140 -13
  42. package/commands/pipeline-health.md +1 -1
  43. package/commands/pipeline-resume.md +1 -1
  44. package/commands/pipeline-status.md +1 -1
  45. package/commands/qa-sweep.md +27 -4
  46. package/commands/recover-orphans.md +1 -1
  47. package/commands/replay.md +16 -2
  48. package/commands/review-pr-agents/protocols.md +4 -0
  49. package/commands/review-pr-agents/spec-cli.md +179 -0
  50. package/commands/review-pr-agents.md +7 -4
  51. package/commands/review-pr-staging.md +44 -15
  52. package/commands/review-pr.md +122 -28
  53. package/commands/rollback.md +15 -5
  54. package/commands/security-audit.md +14 -11
  55. package/commands/signal-planner.md +32 -12
  56. package/commands/spec-doctor.md +1 -1
  57. package/commands/sync-ecosystem.md +54 -11
  58. package/commands/test-gate.md +49 -11
  59. package/commands/upgrade-deps.md +518 -0
  60. package/commands/validate.md +16 -5
  61. package/commands/work-on/build/architect.md +1 -1
  62. package/commands/work-on/build/context.md +1 -1
  63. package/commands/work-on/build/implement.md +2 -2
  64. package/commands/work-on/build/validate.md +64 -2
  65. package/commands/work-on/build.md +10 -2
  66. package/commands/work-on/close.md +1 -1
  67. package/commands/work-on/decompose.md +35 -25
  68. package/commands/work-on/investigate.md +2 -2
  69. package/commands/work-on/remediate.md +1 -1
  70. package/commands/work-on/review.md +5 -3
  71. package/commands/work-on-monolithic.md +1 -1
  72. package/commands/work-on.md +28 -7
  73. package/docs/AIDER.md +264 -0
  74. package/docs/AUTOPILOT-CRON.md +130 -0
  75. package/docs/CI.md +18 -0
  76. package/docs/CONFIG.md +41 -2
  77. package/docs/CURSOR.md +135 -0
  78. package/docs/OPENCODE.md +312 -0
  79. package/docs/demo.gif +0 -0
  80. package/docs/eval/README.md +4 -4
  81. package/docs/launch/show-hn.md +3 -3
  82. package/docs/site/annotations-explained.md +2 -2
  83. package/docs/site/architecture.md +180 -0
  84. package/docs/site/command-reference.md +38 -0
  85. package/docs/site/configuration.md +243 -0
  86. package/docs/site/for-companies.md +4 -4
  87. package/docs/site/forge-annotation-protocol.md +1 -1
  88. package/docs/site/github-is-the-memory.md +2 -2
  89. package/docs/site/how-it-works.md +1 -1
  90. package/docs/site/public/llms-full.txt +380 -0
  91. package/docs/site/public/llms.txt +35 -0
  92. package/docs/spec/review-protocol.md +6 -0
  93. package/docs/spec-graph-schema.md +3 -3
  94. package/package.json +2 -2
  95. package/packages/protocol/README.md +75 -0
  96. package/packages/protocol/src/card.js +114 -0
  97. package/packages/protocol/src/cli.js +4 -77
  98. package/packages/protocol/src/emit.js +14 -3
  99. package/packages/protocol/src/index.js +2 -1
  100. package/packages/protocol/src/parse.js +17 -8
  101. package/packages/protocol/src/types.js +1 -1
  102. package/packages/protocol/src/validate.js +11 -0
  103. package/scripts/check-command-side-effects.sh +261 -0
  104. package/scripts/check-spec-markers.sh +273 -0
  105. package/scripts/classify-lane.sh +29 -7
  106. package/scripts/forge-run.sh +322 -0
  107. package/templates/cross-repo-trigger.yml +282 -0
  108. package/templates/cursor/forge.mdc +148 -0
  109. package/templates/github-actions/forgedock-autopilot.yml +113 -0
package/bin/journey.mjs CHANGED
@@ -601,6 +601,7 @@ export function makeCtx(overrides = {}) {
601
601
  const argv = overrides.argv ?? process.argv.slice(2);
602
602
  const nodeVersion = overrides.nodeVersion ?? process.versions.node;
603
603
  const enrichFn = overrides.enrichFn ?? enrich;
604
+ const isCliAvailableFn = overrides.isCliAvailableFn ?? isClaudeCliAvailable;
604
605
  const openFn = overrides.openFn ?? openUrl;
605
606
  const confirmFn = overrides.confirmFn ?? confirm;
606
607
  const platform = overrides.platform ?? process.platform;
@@ -626,6 +627,7 @@ export function makeCtx(overrides = {}) {
626
627
  release,
627
628
  linkStrategy: "symlink",
628
629
  enrichFn,
630
+ isCliAvailableFn,
629
631
  openFn,
630
632
  confirmFn,
631
633
  exec: (cmd, args) =>
@@ -745,7 +747,8 @@ export async function preflight(ctx) {
745
747
  // Act II — Forging: command symlinks + SessionStart hook (Task 6)
746
748
  // ---------------------------------------------------------------------------
747
749
 
748
- import { mkdir, symlink, readlink, lstat, readdir, rename, copyFile, readFile, writeFile, unlink } from "fs/promises";
750
+ import { mkdir, symlink, readlink, lstat, readdir, rename, copyFile, readFile, writeFile, unlink, rm } from "fs/promises";
751
+ import { compareVersions } from "./registry.mjs";
749
752
  import { relative, dirname as pathDirname } from "path";
750
753
  import {
751
754
  installSessionStartHook,
@@ -1071,6 +1074,54 @@ async function copyDirIfChanged(srcDir, destDir) {
1071
1074
  return { copied, unchanged };
1072
1075
  }
1073
1076
 
1077
+ /**
1078
+ * Recursively delete entries in `destDir` that no longer exist in `srcDir`.
1079
+ * The mirror-image of copyDirIfChanged(): that function is additive-only (it
1080
+ * copies new/changed files but never removes anything), so a file dropped or
1081
+ * renamed upstream would otherwise linger in the persisted `~/.forge/` copy
1082
+ * forever (forge#2133). A missing `srcDir` or `destDir` is treated as
1083
+ * "nothing to reconcile" rather than an error — mirrors copyDirIfChanged()'s
1084
+ * own ENOENT handling.
1085
+ *
1086
+ * @param {string} srcDir
1087
+ * @param {string} destDir
1088
+ * @returns {Promise<{ removed: number }>}
1089
+ */
1090
+ async function removeOrphans(srcDir, destDir) {
1091
+ let destEntries;
1092
+ try {
1093
+ destEntries = await readdir(destDir, { withFileTypes: true });
1094
+ } catch (err) {
1095
+ if (err.code === "ENOENT") return { removed: 0 };
1096
+ throw err;
1097
+ }
1098
+
1099
+ let srcNames = null;
1100
+ try {
1101
+ const srcEntries = await readdir(srcDir, { withFileTypes: true });
1102
+ srcNames = new Set(srcEntries.map((e) => e.name));
1103
+ } catch (err) {
1104
+ if (err.code !== "ENOENT") throw err;
1105
+ // srcDir missing entirely — every dest entry is an orphan.
1106
+ srcNames = new Set();
1107
+ }
1108
+
1109
+ let removed = 0;
1110
+ for (const entry of destEntries) {
1111
+ const destPath = join(destDir, entry.name);
1112
+ if (!srcNames.has(entry.name)) {
1113
+ await rm(destPath, { recursive: true, force: true });
1114
+ removed++;
1115
+ continue;
1116
+ }
1117
+ if (entry.isDirectory()) {
1118
+ const sub = await removeOrphans(join(srcDir, entry.name), destPath);
1119
+ removed += sub.removed;
1120
+ }
1121
+ }
1122
+ return { removed };
1123
+ }
1124
+
1074
1125
  /**
1075
1126
  * Copy ForgeDock's own installable payload (bin/, commands/, scripts/,
1076
1127
  * templates/) from wherever the package currently resolved — npm global
@@ -1113,6 +1164,7 @@ async function copyDirIfChanged(srcDir, destDir) {
1113
1164
  * version: string,
1114
1165
  * filesCopied?: number,
1115
1166
  * filesUnchanged?: number,
1167
+ * filesRemoved?: number,
1116
1168
  * }>}
1117
1169
  */
1118
1170
  export async function persistHome(ctx) {
@@ -1140,15 +1192,55 @@ export async function persistHome(ctx) {
1140
1192
  // proceed with version === ""
1141
1193
  }
1142
1194
 
1195
+ // Downgrade guard (forge#2133): if a newer version is already persisted at
1196
+ // ~/.forge/version than the source package we're about to copy from, skip
1197
+ // the copy entirely rather than silently overwriting newer files with
1198
+ // older ones. This happens when a stale resolved package (e.g. a plain
1199
+ // `npx forgedock update` that resolved an old global install) runs after
1200
+ // ~/.forge/ was already refreshed to a newer version by some other path
1201
+ // (e.g. `npx forgedock@latest`). Both `version` and the persisted value
1202
+ // must be non-empty for the guard to apply — an unreadable/missing
1203
+ // ~/.forge/version is treated as "no guard needed" so first-run persist
1204
+ // is never blocked.
1205
+ const persistedVersionPath = join(persistedHome, "version");
1206
+ let existingPersistedVersion = "";
1207
+ let persistedVersionFileExists = true;
1208
+ try {
1209
+ existingPersistedVersion = readFileSync(persistedVersionPath, "utf-8").trim();
1210
+ } catch {
1211
+ // missing/unreadable — proceed, nothing to guard against. Tracked
1212
+ // separately from existingPersistedVersion's "" default so the
1213
+ // versionChanged check below (a fresh persist with no prior version
1214
+ // file) is still correctly treated as "changed" even when the source
1215
+ // package's own version also resolves to "" (e.g. missing package.json).
1216
+ persistedVersionFileExists = false;
1217
+ }
1218
+ if (
1219
+ version &&
1220
+ existingPersistedVersion &&
1221
+ compareVersions(version, existingPersistedVersion) < 0
1222
+ ) {
1223
+ return {
1224
+ forgeHome: persistedHome,
1225
+ migrated: false,
1226
+ skipped: true,
1227
+ reason: `refusing to downgrade ~/.forge/ from v${existingPersistedVersion} to v${version} — source package is older than what's already persisted`,
1228
+ version: existingPersistedVersion,
1229
+ };
1230
+ }
1231
+
1143
1232
  try {
1144
1233
  await mkdir(persistedHome, { recursive: true });
1145
1234
 
1146
1235
  let filesCopied = 0;
1147
1236
  let filesUnchanged = 0;
1237
+ let filesRemoved = 0;
1148
1238
  for (const name of PERSIST_HOME_DIRS) {
1149
1239
  const res = await copyDirIfChanged(join(source, name), join(persistedHome, name));
1150
1240
  filesCopied += res.copied;
1151
1241
  filesUnchanged += res.unchanged;
1242
+ const pruned = await removeOrphans(join(source, name), join(persistedHome, name));
1243
+ filesRemoved += pruned.removed;
1152
1244
  }
1153
1245
 
1154
1246
  // Also persist package.json itself (not just PERSIST_HOME_DIRS) — several
@@ -1179,18 +1271,19 @@ export async function persistHome(ctx) {
1179
1271
 
1180
1272
  // Write ~/.forge/version — content-compared like everything else here so
1181
1273
  // an unchanged version doesn't touch the file's mtime on every re-run.
1182
- const versionPath = join(persistedHome, "version");
1183
- let versionChanged = true;
1184
- try {
1185
- versionChanged = readFileSync(versionPath, "utf-8").trim() !== version;
1186
- } catch {
1187
- // missing/unreadable — treat as changed
1188
- }
1274
+ // Reuses persistedVersionPath/existingPersistedVersion read above for the
1275
+ // downgrade guard rather than re-reading the same file a second time. A
1276
+ // version file that didn't exist yet (persistedVersionFileExists: false)
1277
+ // always counts as "changed", regardless of whether the source's own
1278
+ // version happens to also be "" (e.g. missing package.json) — otherwise
1279
+ // "" !== "" would wrongly read as unchanged on a fresh, never-persisted
1280
+ // ~/.forge/.
1281
+ let versionChanged = !persistedVersionFileExists || existingPersistedVersion !== version;
1189
1282
  if (versionChanged) {
1190
- const tmpVersionPath = versionPath + ".tmp";
1283
+ const tmpVersionPath = persistedVersionPath + ".tmp";
1191
1284
  try {
1192
1285
  writeFileSync(tmpVersionPath, version + "\n", "utf-8");
1193
- renameSync(tmpVersionPath, versionPath);
1286
+ renameSync(tmpVersionPath, persistedVersionPath);
1194
1287
  } catch (err) {
1195
1288
  try { unlinkSync(tmpVersionPath); } catch { /* best-effort cleanup */ }
1196
1289
  throw err;
@@ -1199,11 +1292,12 @@ export async function persistHome(ctx) {
1199
1292
 
1200
1293
  return {
1201
1294
  forgeHome: persistedHome,
1202
- migrated: filesCopied > 0 || versionChanged,
1295
+ migrated: filesCopied > 0 || filesRemoved > 0 || versionChanged,
1203
1296
  skipped: false,
1204
1297
  version,
1205
1298
  filesCopied,
1206
1299
  filesUnchanged,
1300
+ filesRemoved,
1207
1301
  };
1208
1302
  } catch (err) {
1209
1303
  // Fail-open (forge#383): a permission error, disk-full, etc. must never
@@ -1720,7 +1814,8 @@ export async function forge(ctx) {
1720
1814
  // ---------------------------------------------------------------------------
1721
1815
 
1722
1816
  import { detectConfig } from "./init-detect.mjs";
1723
- import { enrich } from "./init-enrich-api.mjs";
1817
+ import { enrich, resolveEnrichBackend } from "./init-enrich.mjs";
1818
+ import { isClaudeCliAvailable } from "./runner.mjs";
1724
1819
  import { annotatedReviewScreen, box, confirm, getLogoTagline } from "./tui.mjs";
1725
1820
 
1726
1821
  const badgeOf = (field) => field.confidence;
@@ -1751,16 +1846,22 @@ export async function read(ctx) {
1751
1846
  ];
1752
1847
  await revealRows(rows, { mode: ctx.mode, motion: ctx.motion, writer: w });
1753
1848
 
1754
- if (ctx.env.ANTHROPIC_API_KEY) {
1849
+ const enrichBackend = resolveEnrichBackend({
1850
+ cwd: ctx.cwd,
1851
+ env: ctx.env,
1852
+ isCliAvailableFn: ctx.isCliAvailableFn,
1853
+ });
1854
+
1855
+ if (enrichBackend !== "none") {
1755
1856
  try {
1756
1857
  w.write(" " + dimLine(ctx, "✦ enriching with AI…") + "\n");
1757
- const enriched = await ctx.enrichFn(draft);
1858
+ const enriched = await ctx.enrichFn(draft, { backend: enrichBackend, cwd: ctx.cwd, env: ctx.env });
1758
1859
  if (enriched && typeof enriched === "object") draft = enriched;
1759
1860
  } catch {
1760
1861
  w.write(" " + dimLine(ctx, "✦ AI enrichment unavailable — continuing with detection only") + "\n");
1761
1862
  }
1762
1863
  } else {
1763
- w.write(" " + dimLine(ctx, "✦ no ANTHROPIC_API_KEY — skipping AI enrichment") + "\n");
1864
+ w.write(" " + dimLine(ctx, "✦ no Claude Code CLI or ANTHROPIC_API_KEY — skipping AI enrichment") + "\n");
1764
1865
  }
1765
1866
 
1766
1867
  return { draft, description };