forgedock 1.0.20 → 1.0.22

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 (49) hide show
  1. package/README.md +146 -77
  2. package/bin/demo.mjs +386 -0
  3. package/bin/forgedock.mjs +357 -30
  4. package/bin/init-detect.mjs +95 -0
  5. package/bin/runner.mjs +780 -0
  6. package/commands/ci-audit.md +469 -0
  7. package/commands/cleanup.md +85 -39
  8. package/commands/deploy-info.md +6 -2
  9. package/commands/forge-stats.md +1 -1
  10. package/commands/optimize.md +1 -1
  11. package/commands/orchestrate.md +684 -33
  12. package/commands/quality-gate.md +166 -46
  13. package/commands/review-pr-agents.md +106 -34
  14. package/commands/review-pr.md +33 -2
  15. package/commands/scope.md +11 -3
  16. package/commands/security-audit.md +24 -16
  17. package/commands/sync-ecosystem.md +2 -2
  18. package/commands/work-on/build/architect.md +47 -3
  19. package/commands/work-on/build/context.md +36 -5
  20. package/commands/work-on/build/validate.md +16 -7
  21. package/commands/work-on/close.md +129 -1
  22. package/commands/work-on/investigate.md +24 -1
  23. package/commands/work-on.md +226 -8
  24. package/docs/comparison.md +135 -0
  25. package/docs/design/developer-objections-ai-context-tools.md +244 -0
  26. package/docs/design/messaging-angles-research.md +179 -0
  27. package/docs/design/show-hn-draft.md +86 -0
  28. package/docs/site/annotations-explained.md +55 -0
  29. package/docs/site/command-learning-path.md +104 -0
  30. package/docs/site/command-reference.md +3 -0
  31. package/docs/site/getting-started.md +36 -14
  32. package/docs/site/troubleshooting.md +208 -0
  33. package/docs/spec/forge-protocol-v1.md +768 -0
  34. package/examples/forgedock-demo/README.md +113 -0
  35. package/examples/forgedock-demo/bootstrap.sh +125 -0
  36. package/examples/forgedock-demo/issues/01-bug-delete-missing-auth.md +37 -0
  37. package/examples/forgedock-demo/issues/02-feature-filter-notes.md +37 -0
  38. package/examples/forgedock-demo/issues/03-refactor-router-table.md +33 -0
  39. package/examples/forgedock-demo/issues/04-perf-index-lookup.md +35 -0
  40. package/examples/forgedock-demo/issues/05-docs-api-reference.md +39 -0
  41. package/examples/forgedock-demo/labels.json +21 -0
  42. package/examples/forgedock-demo/package.json +16 -0
  43. package/examples/forgedock-demo/scripts/smoke.js +61 -0
  44. package/examples/forgedock-demo/src/auth.js +28 -0
  45. package/examples/forgedock-demo/src/db.js +66 -0
  46. package/examples/forgedock-demo/src/routes/notes.js +64 -0
  47. package/examples/forgedock-demo/src/server.js +79 -0
  48. package/package.json +6 -2
  49. package/templates/forge.yaml.minimal +44 -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, pathToFileURL } from "url";
6
- import { dirname, join, relative, resolve } from "path";
6
+ import { basename, dirname, join, relative, resolve } from "path";
7
7
  import { mkdir, symlink, readlink, lstat, readdir, stat, unlink } from "fs/promises";
8
8
  import {
9
9
  existsSync,
@@ -33,6 +33,7 @@ import {
33
33
  YELLOW,
34
34
  CYAN,
35
35
  } from "./tui.mjs";
36
+ import { buildMinimalForgeYaml } from "./init-detect.mjs";
36
37
 
37
38
  const __filename = fileURLToPath(import.meta.url);
38
39
  const __dirname = dirname(__filename);
@@ -346,6 +347,9 @@ function writeSymlinkSentinel() {
346
347
  const content =
347
348
  `# ForgeDock command symlinks — DO NOT REPOINT\n` +
348
349
  `# Source: ${COMMANDS_DIR}\n` +
350
+ // Machine-readable version line — read by install() on the next run to
351
+ // detect first-time vs. update installs and render a version diff (#1146).
352
+ `# Version: ${getVersion()}\n` +
349
353
  `#\n` +
350
354
  `# These symlinks are managed by ForgeDock (https://forgedock.com).\n` +
351
355
  `# ForgeDock owns the global ~/.claude/commands/ namespace.\n` +
@@ -1050,6 +1054,28 @@ async function linkScripts(step) {
1050
1054
  }
1051
1055
 
1052
1056
  async function install() {
1057
+ // Detect first-time vs. update install by inspecting the namespace sentinel
1058
+ // BEFORE the install steps overwrite it. A missing sentinel means this is a
1059
+ // first install; a present one carries the previously installed version (if
1060
+ // it was written by a build that records `# Version:`). Reading is non-fatal:
1061
+ // any error is treated as a first install so guidance still renders. (#1146)
1062
+ const sentinelPath = join(TARGET_DIR, ".symlink-source");
1063
+ let priorSentinel = null;
1064
+ try {
1065
+ priorSentinel = readFileSync(sentinelPath, "utf-8");
1066
+ } catch {
1067
+ priorSentinel = null;
1068
+ }
1069
+ const isFirstInstall = priorSentinel === null;
1070
+ const priorVersion = priorSentinel
1071
+ ? priorSentinel.match(/^# Version:\s*(.+)$/m)?.[1]?.trim() || null
1072
+ : null;
1073
+ const currentVersion = getVersion();
1074
+
1075
+ // Captured from the "Linking commands" step so the post-install summary can
1076
+ // report how many commands were installed/changed without recounting.
1077
+ let linkStats = { installed: 0, updated: 0, skipped: 0 };
1078
+
1053
1079
  const result = await runSteps([
1054
1080
  {
1055
1081
  label: "Checking environment",
@@ -1086,6 +1112,8 @@ async function install() {
1086
1112
  async run(step) {
1087
1113
  const { installed, updated, skipped } = await linkCommands(step);
1088
1114
  const total = installed + updated + skipped;
1115
+ // Hoist for the post-install summary box (#1146).
1116
+ linkStats = { installed, updated, skipped };
1089
1117
  step.note(
1090
1118
  `${green(String(installed))} installed, ${updated} updated, ${dim(String(skipped))} skipped (${total} commands total)`,
1091
1119
  );
@@ -1231,18 +1259,65 @@ async function install() {
1231
1259
  ]);
1232
1260
 
1233
1261
  if (result.ok) {
1234
- process.stderr.write(
1235
- box(
1236
- [
1237
- `${green("✔")} ${bold("ForgeDock is ready")}`,
1238
- "",
1239
- `Commands installed to ${cyan(TARGET_DIR)}`,
1240
- `Scripts installed to ${cyan(SCRIPTS_TARGET_DIR)}`,
1241
- `Next: ${cyan("cd <your-project>")} then ${cyan("npx forgedock init")}`,
1242
- ],
1243
- { title: "ForgeDock Installed" },
1244
- ) + "\n",
1245
- );
1262
+ const totalCommands =
1263
+ linkStats.installed + linkStats.updated + linkStats.skipped;
1264
+ const changedCommands = linkStats.installed + linkStats.updated;
1265
+ const versionLabel = currentVersion ? `v${currentVersion}` : "";
1266
+
1267
+ if (isFirstInstall) {
1268
+ // First-time install — show the full guided next-steps box so the user
1269
+ // has a prioritized path from "installed" to "using it". (#1146)
1270
+ const cmd = (text) => cyan(text.padEnd(22));
1271
+ process.stderr.write(
1272
+ box(
1273
+ [
1274
+ `${green("✔")} ${bold(`ForgeDock ${versionLabel}`.trim())} installed`,
1275
+ `${green("✔")} ${totalCommands} commands → ${cyan(TARGET_DIR)}`,
1276
+ "",
1277
+ bold("What's next?"),
1278
+ "",
1279
+ dim("First time?"),
1280
+ ` ${cmd("npx forgedock demo")}${dim("Try the pipeline risk-free")}`,
1281
+ "",
1282
+ dim("Setting up your repo?"),
1283
+ ` ${cmd("npx forgedock init")}${dim("Configure forge.yaml")}`,
1284
+ ` ${cmd("npx forgedock doctor")}${dim("Verify your setup")}`,
1285
+ "",
1286
+ dim("Ready to go?"),
1287
+ ` ${cyan("/work-on #N".padEnd(22))}${dim("Run it in Claude Code")}`,
1288
+ ],
1289
+ { title: "ForgeDock Installed" },
1290
+ ) + "\n",
1291
+ );
1292
+ } else {
1293
+ // Update install — show a compact version diff and what changed, with a
1294
+ // changelog link instead of the full first-run guidance. (#1146)
1295
+ let headline;
1296
+ if (priorVersion && currentVersion && priorVersion !== currentVersion) {
1297
+ headline = `Updated from ${bold(`v${priorVersion}`)} → ${bold(`v${currentVersion}`)}.`;
1298
+ } else if (currentVersion && priorVersion === currentVersion) {
1299
+ headline = `Reinstalled ${bold(`v${currentVersion}`)}.`;
1300
+ } else if (currentVersion) {
1301
+ headline = `Updated to ${bold(`v${currentVersion}`)}.`;
1302
+ } else {
1303
+ headline = "Update complete.";
1304
+ }
1305
+ const changeNote =
1306
+ changedCommands > 0
1307
+ ? ` ${changedCommands} new/changed command${changedCommands === 1 ? "" : "s"}.`
1308
+ : " Commands already up to date.";
1309
+ process.stderr.write(
1310
+ box(
1311
+ [
1312
+ `${green("✔")} ${headline}${changeNote}`,
1313
+ "",
1314
+ `Changelog: ${cyan("https://github.com/RapierCraftStudios/ForgeDock/releases")}`,
1315
+ `Verify: ${cyan("npx forgedock doctor")}`,
1316
+ ],
1317
+ { title: "ForgeDock Updated" },
1318
+ ) + "\n",
1319
+ );
1320
+ }
1246
1321
  }
1247
1322
  }
1248
1323
 
@@ -1476,13 +1551,15 @@ async function update() {
1476
1551
  console.log("");
1477
1552
  }
1478
1553
 
1479
- async function init(fromInstall = false) {
1554
+ async function init(fromInstall = false, minimal = false) {
1480
1555
  // When called from install() via runSteps(), suppress stdout to prevent
1481
1556
  // interleaving with the TUI spinner on stderr. runSteps() provides the
1482
1557
  // "Generating forge.yaml" step label as visual feedback. (#812)
1483
1558
  const log = fromInstall ? () => {} : console.log;
1484
1559
  log("");
1485
- log(`${BOLD}ForgeDock${RESET} — Generating forge.yaml`);
1560
+ log(
1561
+ `${BOLD}ForgeDock${RESET} — Generating forge.yaml${minimal ? " (minimal)" : ""}`,
1562
+ );
1486
1563
  log("");
1487
1564
 
1488
1565
  const cwd = process.cwd();
@@ -1688,7 +1765,7 @@ async function init(fromInstall = false) {
1688
1765
  `forge.yaml.bak.${new Date().toISOString().replace(/[:.]/g, "-")}`,
1689
1766
  )
1690
1767
  : baseBak;
1691
- const backupName = backupPath.split("/").pop();
1768
+ const backupName = basename(backupPath);
1692
1769
  renameSync(outputPath, backupPath);
1693
1770
  log(` ${YELLOW}Backed up${RESET}: forge.yaml → ${backupName}`);
1694
1771
  }
@@ -1707,7 +1784,7 @@ async function init(fromInstall = false) {
1707
1784
  const safeDefaultBranch = defaultBranch.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
1708
1785
  const safeStagingBranch = stagingBranch.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
1709
1786
 
1710
- const content = `# forge.yaml — ForgeDock Configuration
1787
+ const fullContent = `# forge.yaml — ForgeDock Configuration
1711
1788
  #
1712
1789
  # Auto-generated by: npx forgedock init
1713
1790
  # Edit this file with your project details.
@@ -1825,9 +1902,26 @@ branches:
1825
1902
  # # override_phrase: "OVERRIDE: shipping with test failures \u2014"
1826
1903
  `;
1827
1904
 
1905
+ // When --minimal is requested, emit only the three required sections
1906
+ // (project, paths, branches) — no commented optional blocks. The minimal
1907
+ // output still passes `forgedock doctor` and drives `/work-on`, while staying
1908
+ // ~20 lines instead of ~200. (#1148)
1909
+ const content = minimal
1910
+ ? buildMinimalForgeYaml({
1911
+ projectName,
1912
+ owner,
1913
+ repo,
1914
+ description,
1915
+ root: cwd,
1916
+ worktreeBase,
1917
+ defaultBranch,
1918
+ stagingBranch,
1919
+ })
1920
+ : fullContent;
1921
+
1828
1922
  atomicWriteFile(outputPath, content);
1829
1923
 
1830
- log(` ${GREEN}Created${RESET}: forge.yaml`);
1924
+ log(` ${GREEN}Created${RESET}: forge.yaml${minimal ? " (minimal)" : ""}`);
1831
1925
  log("");
1832
1926
 
1833
1927
  if (fromInstall) {
@@ -1857,9 +1951,15 @@ branches:
1857
1951
  log(
1858
1952
  ` 2. Add ${CYAN}forge.yaml${RESET} to ${CYAN}.gitignore${RESET} if it contains sensitive paths`,
1859
1953
  );
1860
- log(
1861
- ` 3. Run ${CYAN}/forgedock-init${RESET} inside Claude Code for guided AI-powered setup`,
1862
- );
1954
+ if (minimal) {
1955
+ log(
1956
+ ` 3. Need more? Add optional sections from ${CYAN}forge.yaml.example${RESET} (see ${CYAN}docs/CONFIG.md${RESET})`,
1957
+ );
1958
+ } else {
1959
+ log(
1960
+ ` 3. Run ${CYAN}/forgedock-init${RESET} inside Claude Code for guided AI-powered setup`,
1961
+ );
1962
+ }
1863
1963
  log("");
1864
1964
  }
1865
1965
  }
@@ -2109,13 +2209,27 @@ async function doctor() {
2109
2209
  symlinkOk = false;
2110
2210
  }
2111
2211
  } else {
2112
- // Regular file — not a symlink (install left a copy)
2113
- broken++;
2114
- brokenLinks.push(`${rel} (not a symlink)`);
2115
- symlinkOk = false;
2212
+ // Regular file — copy-mode install (Windows without Developer Mode).
2213
+ // linkCommands() falls back to copyFile() on EPERM/EACCES, so a regular
2214
+ // file is valid as long as its content matches the source. <!-- Added: forge#1174 -->
2215
+ try {
2216
+ const srcContent = readFileSync(src, "utf-8");
2217
+ const tgtContent = readFileSync(tgt, "utf-8");
2218
+ if (srcContent !== tgtContent) {
2219
+ broken++;
2220
+ brokenLinks.push(`${rel} (stale copy)`);
2221
+ symlinkOk = false;
2222
+ }
2223
+ // else: content matches — valid copy-mode install, not broken
2224
+ } catch {
2225
+ // Could not read one of the files — treat as broken.
2226
+ broken++;
2227
+ brokenLinks.push(`${rel} (unreadable)`);
2228
+ symlinkOk = false;
2229
+ }
2116
2230
  }
2117
2231
  } catch {
2118
- // Symlink missing
2232
+ // File missing
2119
2233
  broken++;
2120
2234
  brokenLinks.push(`${rel} (missing)`);
2121
2235
  symlinkOk = false;
@@ -2123,15 +2237,15 @@ async function doctor() {
2123
2237
  }
2124
2238
 
2125
2239
  if (symlinkOk) {
2126
- pass("Command symlinks", `${checked} symlinks valid`);
2240
+ pass("Command files", `${checked} files installed`);
2127
2241
  } else {
2128
2242
  fail(
2129
- "Command symlinks",
2243
+ "Command files",
2130
2244
  `Run: npx forgedock install (${broken}/${checked} broken: ${brokenLinks.slice(0, 3).join(", ")}${brokenLinks.length > 3 ? "…" : ""})`,
2131
2245
  );
2132
2246
  }
2133
2247
  } catch (err) {
2134
- fail("Command symlinks", `Could not read commands directory: ${err.message}`);
2248
+ fail("Command files", `Could not read commands directory: ${err.message}`);
2135
2249
  }
2136
2250
  }
2137
2251
 
@@ -2441,6 +2555,106 @@ async function doctor() {
2441
2555
  }
2442
2556
  }
2443
2557
 
2558
+ // ── Check 10: yq installed ────────────────────────────────────────────────
2559
+ // yq is a hard dependency: pipeline commands (work-on, review-pr, orchestrate)
2560
+ // read forge.yaml via yq. Without it, those commands fail. Fixed literal
2561
+ // command — no interpolation, so no injection surface (cf. #663/#789/#807).
2562
+ {
2563
+ let yqVersion = "";
2564
+ try {
2565
+ yqVersion = execSync("yq --version", {
2566
+ stdio: ["ignore", "pipe", "ignore"],
2567
+ encoding: "utf-8",
2568
+ timeout: 5000,
2569
+ }).trim();
2570
+ } catch {
2571
+ // yq not installed or not on PATH
2572
+ }
2573
+
2574
+ if (yqVersion) {
2575
+ pass("yq", yqVersion);
2576
+ } else {
2577
+ fail(
2578
+ "yq",
2579
+ "Install the YAML processor (used to read forge.yaml): https://github.com/mikefarah/yq#install",
2580
+ );
2581
+ }
2582
+ }
2583
+
2584
+ // ── Check 11: Claude Code installed + version compatible ───────────────────
2585
+ // The compatibility floor is the @anthropic-ai/claude-code peerDependency in
2586
+ // package.json (read dynamically so it never drifts from the declared floor).
2587
+ // Not-on-PATH is a warning, not a failure: Claude Code may run as the host
2588
+ // process without exposing the `claude` CLI on PATH, so a hard fail would be a
2589
+ // false negative. Only an installed-but-too-old version is a hard failure.
2590
+ {
2591
+ // Resolve the minimum compatible version from package.json peerDependencies.
2592
+ let minClaudeVersion = "2.0.0";
2593
+ try {
2594
+ const pkgRaw = readFileSync(join(FORGE_HOME, "package.json"), "utf-8");
2595
+ const pkg = JSON.parse(pkgRaw);
2596
+ const range = pkg?.peerDependencies?.["@anthropic-ai/claude-code"];
2597
+ if (typeof range === "string") {
2598
+ const m = range.match(/(\d+\.\d+\.\d+)/);
2599
+ if (m) minClaudeVersion = m[1];
2600
+ }
2601
+ } catch {
2602
+ // package.json unreadable — keep the hardcoded fallback floor.
2603
+ }
2604
+
2605
+ let claudeRaw = "";
2606
+ let claudeInstalled = false;
2607
+ try {
2608
+ claudeRaw = execSync("claude --version", {
2609
+ stdio: ["ignore", "pipe", "ignore"],
2610
+ encoding: "utf-8",
2611
+ timeout: 5000,
2612
+ }).trim();
2613
+ claudeInstalled = true;
2614
+ } catch {
2615
+ // claude CLI not on PATH
2616
+ }
2617
+
2618
+ if (!claudeInstalled) {
2619
+ warn(
2620
+ "Claude Code",
2621
+ `\`claude\` CLI not found on PATH (need >= v${minClaudeVersion}). If you run Claude Code without the CLI on PATH this is fine; otherwise install it: https://docs.anthropic.com/en/docs/claude-code`,
2622
+ );
2623
+ } else {
2624
+ const verMatch = claudeRaw.match(/(\d+)\.(\d+)\.(\d+)/);
2625
+ if (!verMatch) {
2626
+ warn(
2627
+ "Claude Code",
2628
+ `installed but version string could not be parsed ("${claudeRaw}"). Expected >= v${minClaudeVersion}.`,
2629
+ );
2630
+ } else {
2631
+ const found = [
2632
+ Number(verMatch[1]),
2633
+ Number(verMatch[2]),
2634
+ Number(verMatch[3]),
2635
+ ];
2636
+ const floor = minClaudeVersion.split(".").map(Number);
2637
+ // Lexicographic semver comparison: found >= floor ?
2638
+ let compatible = true;
2639
+ for (let i = 0; i < 3; i++) {
2640
+ if (found[i] > floor[i]) break;
2641
+ if (found[i] < floor[i]) {
2642
+ compatible = false;
2643
+ break;
2644
+ }
2645
+ }
2646
+ if (compatible) {
2647
+ pass("Claude Code", `v${found.join(".")} (compatible, >= v${minClaudeVersion})`);
2648
+ } else {
2649
+ fail(
2650
+ "Claude Code",
2651
+ `v${found.join(".")} is below the supported floor v${minClaudeVersion}. Update Claude Code: https://docs.anthropic.com/en/docs/claude-code`,
2652
+ );
2653
+ }
2654
+ }
2655
+ }
2656
+ }
2657
+
2444
2658
  // ── Summary ────────────────────────────────────────────────────────────────
2445
2659
  console.log("");
2446
2660
  if (failures === 0 && warnings === 0) {
@@ -2466,8 +2680,11 @@ function help() {
2466
2680
  ["npx forgedock", "Install commands (default)"],
2467
2681
  ["npx forgedock install", "Install commands"],
2468
2682
  ["npx forgedock init", "Generate forge.yaml config for your project"],
2683
+ ["npx forgedock init --minimal", "Generate a minimal forge.yaml (required sections only)"],
2469
2684
  ["npx forgedock uninstall", "Remove commands"],
2470
2685
  ["npx forgedock update", "Pull latest & reinstall"],
2686
+ ["npx forgedock run <cmd> [args]", "Run a command headlessly via the Anthropic API"],
2687
+ ["npx forgedock demo", "Set up a risk-free demo repo and print next steps"],
2471
2688
  ["npx forgedock enable [dir]", "Mark directory as ForgeDock-managed"],
2472
2689
  ["npx forgedock disable [dir]", "Opt directory out of ForgeDock"],
2473
2690
  ["npx forgedock status [dir]", "Show resolved state for a directory"],
@@ -2480,6 +2697,110 @@ function help() {
2480
2697
  );
2481
2698
  }
2482
2699
 
2700
+ /**
2701
+ * `forgedock run <command> [args...]` — execute a ForgeDock command spec
2702
+ * directly via the Anthropic API, outside of Claude Code (headless / CI).
2703
+ *
2704
+ * Flags:
2705
+ * --dry-run Preview the assembled prompt + tool plan; no API call.
2706
+ * --model <id> Override the model (default: claude-sonnet-4-5 or $FORGEDOCK_MODEL).
2707
+ * --max-iterations <n> Bound the tool-use loop (default: 50).
2708
+ *
2709
+ * The live loop requires ANTHROPIC_API_KEY and the optional @anthropic-ai/sdk
2710
+ * dependency. The runtime itself lives in bin/runner.mjs.
2711
+ *
2712
+ * Env:
2713
+ * FORGEDOCK_MODEL Default model id when --model is omitted.
2714
+ * FORGEDOCK_SHELL Override the shell used by run_bash. Defaults to bash when
2715
+ * found (Git Bash / WSL on Windows, /bin/bash on POSIX),
2716
+ * falling back to the platform default shell otherwise.
2717
+ */
2718
+ async function run() {
2719
+ const runArgs = args.slice(1);
2720
+ let dryRun = false;
2721
+ let model;
2722
+ let maxIterations;
2723
+ const positional = [];
2724
+ for (let i = 0; i < runArgs.length; i++) {
2725
+ const a = runArgs[i];
2726
+ if (a === "--dry-run") {
2727
+ dryRun = true;
2728
+ } else if (a === "--model") {
2729
+ model = runArgs[++i];
2730
+ } else if (a.startsWith("--model=")) {
2731
+ model = a.slice("--model=".length);
2732
+ } else if (a === "--max-iterations") {
2733
+ maxIterations = parseInt(runArgs[++i], 10);
2734
+ } else if (a.startsWith("--max-iterations=")) {
2735
+ maxIterations = parseInt(a.slice("--max-iterations=".length), 10);
2736
+ } else {
2737
+ positional.push(a);
2738
+ }
2739
+ }
2740
+
2741
+ const commandName = positional[0];
2742
+ const commandArgs = positional.slice(1);
2743
+
2744
+ if (!commandName) {
2745
+ process.stderr.write(
2746
+ `${RED}Usage: forgedock run <command> [args...] [--dry-run] [--model <id>] [--max-iterations <n>]${RESET}\n`,
2747
+ );
2748
+ process.exit(1);
2749
+ }
2750
+
2751
+ const { runCommand } = await import("./runner.mjs");
2752
+ try {
2753
+ const result = await runCommand({
2754
+ commandsDir: COMMANDS_DIR,
2755
+ commandName,
2756
+ args: commandArgs,
2757
+ cwd: process.cwd(),
2758
+ dryRun,
2759
+ ...(model ? { model } : {}),
2760
+ ...(Number.isInteger(maxIterations) && maxIterations > 0
2761
+ ? { maxIterations }
2762
+ : {}),
2763
+ });
2764
+ // Treat a non-clean stop (iteration cap hit, or a max_tokens-truncated
2765
+ // turn) as a failed run so CI/headless callers notice.
2766
+ if (
2767
+ result &&
2768
+ (result.status === "max-iterations" || result.status === "incomplete")
2769
+ ) {
2770
+ process.exitCode = 1;
2771
+ }
2772
+ } catch (err) {
2773
+ process.stderr.write(`${RED}${err.message}${RESET}\n`);
2774
+ process.exit(1);
2775
+ }
2776
+ }
2777
+
2778
+ /**
2779
+ * `forgedock demo` — one-command demo mode (issue #1145).
2780
+ *
2781
+ * Stands up a runnable ForgeDock demo at a predictable location (default
2782
+ * ~/forgedock-demo) with zero required decisions, then prints the exact next
2783
+ * steps. Clones the live demo repo when available, otherwise falls back to the
2784
+ * bundled scaffold at FORGE_HOME/examples/forgedock-demo. The runtime lives in
2785
+ * bin/demo.mjs.
2786
+ */
2787
+ async function demo() {
2788
+ const { runDemo } = await import("./demo.mjs");
2789
+ try {
2790
+ const result = await runDemo({
2791
+ forgeHome: FORGE_HOME,
2792
+ args: args.slice(1),
2793
+ cwd: process.cwd(),
2794
+ });
2795
+ if (result && result.status === "error") {
2796
+ process.exitCode = 1;
2797
+ }
2798
+ } catch (err) {
2799
+ process.stderr.write(`${RED}${err.message}${RESET}\n`);
2800
+ process.exit(1);
2801
+ }
2802
+ }
2803
+
2483
2804
  splash();
2484
2805
 
2485
2806
  switch (command) {
@@ -2487,7 +2808,7 @@ switch (command) {
2487
2808
  await install();
2488
2809
  break;
2489
2810
  case "init":
2490
- await init();
2811
+ await init(false, args.includes("--minimal"));
2491
2812
  break;
2492
2813
  case "uninstall":
2493
2814
  await uninstall();
@@ -2495,6 +2816,12 @@ switch (command) {
2495
2816
  case "update":
2496
2817
  await update();
2497
2818
  break;
2819
+ case "run":
2820
+ await run();
2821
+ break;
2822
+ case "demo":
2823
+ await demo();
2824
+ break;
2498
2825
  case "enable":
2499
2826
  await enable(args[1]);
2500
2827
  break;
@@ -323,3 +323,98 @@ export async function detectConfig(cwd = process.cwd()) {
323
323
  meta: { remoteDetected },
324
324
  };
325
325
  }
326
+
327
+ // ---------------------------------------------------------------------------
328
+ // Minimal forge.yaml template builder
329
+ // ---------------------------------------------------------------------------
330
+
331
+ /**
332
+ * Escape a value for embedding inside a YAML double-quoted scalar.
333
+ *
334
+ * Backslash MUST be escaped first to avoid double-escaping an already-escaped
335
+ * quote. This mirrors the escaping applied by the full-config generator in
336
+ * forgedock.mjs and prevents Windows paths (which contain `\`) from corrupting
337
+ * the emitted YAML. (Ref: forge#810 — init() did not escape backslashes.)
338
+ *
339
+ * @param {string} value
340
+ * @returns {string}
341
+ */
342
+ function escapeYamlScalar(value) {
343
+ return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
344
+ }
345
+
346
+ /**
347
+ * Build a minimal forge.yaml document containing ONLY the three required
348
+ * sections — project, paths, branches — with no commented optional blocks.
349
+ *
350
+ * This is the `npx forgedock init --minimal` output. It is intentionally
351
+ * complete enough to pass `npx forgedock doctor` (which requires project,
352
+ * paths, AND branches) and to drive `/work-on` (which needs
353
+ * paths.worktree_base for git worktrees) — while staying ~20 lines instead of
354
+ * the ~200-line full template. Optional sections (repos, project_board,
355
+ * review, verification, …) fall back to sensible defaults and can be added
356
+ * later from forge.yaml.example / docs/CONFIG.md.
357
+ *
358
+ * Pure: returns a string, performs no I/O.
359
+ *
360
+ * @param {object} opts
361
+ * @param {string} opts.projectName - Human-readable project name.
362
+ * @param {string} opts.owner - GitHub org/user that owns the repo.
363
+ * @param {string} opts.repo - Repository name (no owner prefix).
364
+ * @param {string} [opts.description] - One-line project description (omitted if empty).
365
+ * @param {string} opts.root - Absolute path to the project root.
366
+ * @param {string} opts.worktreeBase - Directory where git worktrees are created.
367
+ * @param {string} opts.defaultBranch - Default branch (e.g. main).
368
+ * @param {string} opts.stagingBranch - Staging branch for fast-lane PRs.
369
+ * @returns {string} The minimal forge.yaml content.
370
+ */
371
+ export function buildMinimalForgeYaml({
372
+ projectName,
373
+ owner,
374
+ repo,
375
+ description = "",
376
+ root,
377
+ worktreeBase,
378
+ defaultBranch,
379
+ stagingBranch,
380
+ }) {
381
+ const e = escapeYamlScalar;
382
+ const descLine = description
383
+ ? `\n description: "${e(description)}"`
384
+ : "";
385
+
386
+ return `# forge.yaml — ForgeDock Configuration (minimal)
387
+ #
388
+ # Generated by: npx forgedock init --minimal
389
+ #
390
+ # Only project, paths, and branches are required — everything else is optional
391
+ # and falls back to sensible defaults. Start here and add sections as you need
392
+ # them. See forge.yaml.example and docs/CONFIG.md for the full reference.
393
+
394
+ # =============================================================================
395
+ # PROJECT (REQUIRED)
396
+ # =============================================================================
397
+
398
+ project:
399
+ name: "${e(projectName)}"
400
+ owner: "${e(owner)}"
401
+ repo: "${e(repo)}"${descLine}
402
+
403
+ # =============================================================================
404
+ # PATHS (REQUIRED)
405
+ # =============================================================================
406
+
407
+ paths:
408
+ root: "${e(root)}"
409
+ worktree_base: "${e(worktreeBase)}"
410
+
411
+ # =============================================================================
412
+ # BRANCHES (REQUIRED)
413
+ # =============================================================================
414
+
415
+ branches:
416
+ default: "${e(defaultBranch)}"
417
+ staging: "${e(stagingBranch)}"
418
+ feature_pattern: "milestone/{slug}"
419
+ `;
420
+ }