prompts-gpt 0.3.2 → 0.3.4

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/dist/cli.js CHANGED
@@ -3,7 +3,8 @@ import { existsSync, readFileSync, statSync, readdirSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { parseArgs } from "node:util";
6
- import { hasTokenUsage, DEFAULT_PROMPTS_GPT_API_URL, DEFAULT_PROMPTS_GPT_OUT_DIR, DEFAULT_RUN_CONFIG_PATH, PROMPTS_GPT_CREDENTIALS_FILE, PromptsGptApiError, PromptsGptClient, doctor, initRunConfig, loadRunConfig, normalizeConcreteProvider, normalizeOrchestrationAgent, ORCHESTRATION_AGENT_PROFILES, runBatch, runPrompt, resolveRunProvider, warnModelProviderMismatch, sweepPrompt, validateRunConfig, discoverWorkspaceAssets, SUPPORTED_AGENT_TARGETS, detectProviders, loadLocalCredentials, saveLocalCredentials, syncPrompts, writeAgentFiles, writePromptManifest, writePromptMarkdownFiles, ensureGitignoreEntry, isCI, orchestrateParallel, orchestratePipeline, orchestrateEval, captureGitBranch, resolveModelWithWarning, getModelCostTier, estimateTokenCost, } from "./index.js";
6
+ import { hasTokenUsage, DEFAULT_PROMPTS_GPT_API_URL, DEFAULT_PROMPTS_GPT_OUT_DIR, DEFAULT_RUN_CONFIG_PATH, PROMPTS_GPT_CREDENTIALS_FILE, PromptsGptApiError, PromptsGptClient, doctor, initRunConfig, loadRunConfig, normalizeConcreteProvider, normalizeOrchestrationAgent, ORCHESTRATION_AGENT_PROFILES, runBatch, runPrompt, resolveRunProvider, warnModelProviderMismatch, sweepPrompt, validateRunConfig, discoverWorkspaceAssets, SUPPORTED_AGENT_TARGETS, detectProviders, loadLocalCredentials, saveLocalCredentials, syncPrompts, writeAgentFiles, writePromptManifest, writePromptMarkdownFiles, ensureGitignoreEntry, isProcessAlive, isCI, orchestrateParallel, orchestratePipeline, orchestrateEval, captureGitBranch, resolveModelWithWarning, getModelCostTier, estimateTokenCost, DEFAULT_MODELS, } from "./index.js";
7
+ import { PROVIDER_MODELS } from "./model-registry.js";
7
8
  const CLI_EXIT_CODES = {
8
9
  success: 0,
9
10
  general: 1,
@@ -210,7 +211,7 @@ async function runCommand(command, flags) {
210
211
  if (setupPicked === "list") {
211
212
  const { spawnSync: spSync } = await import("node:child_process");
212
213
  const cliEntry = resolveCliEntry();
213
- spSync(process.execPath, [cliEntry, "list"], { stdio: "inherit", cwd });
214
+ spSync(process.execPath, [cliEntry, "list"], { stdio: "inherit", cwd, shell: false, windowsHide: true });
214
215
  }
215
216
  else if (setupPicked !== "done") {
216
217
  const [action, file] = setupPicked.split(":", 2);
@@ -218,7 +219,7 @@ async function runCommand(command, flags) {
218
219
  console.log(`\nRunning: prompts-gpt ${cmd} -f ${file}\n`);
219
220
  const { spawnSync: spSync } = await import("node:child_process");
220
221
  const cliEntry = resolveCliEntry();
221
- const setupResult = spSync(process.execPath, [cliEntry, cmd, "-f", file], { stdio: "inherit", cwd });
222
+ const setupResult = spSync(process.execPath, [cliEntry, cmd, "-f", file], { stdio: "inherit", cwd, shell: false, windowsHide: true });
222
223
  process.exitCode = setupResult.status ?? 1;
223
224
  }
224
225
  }
@@ -496,12 +497,7 @@ async function runCommand(command, flags) {
496
497
  const lockContent = JSON.parse(readFileSync(sweepLockPath, "utf8"));
497
498
  const lockAge = Date.now() - new Date(lockContent.startedAt ?? 0).getTime();
498
499
  const lockAgeHours = lockAge / (60 * 60 * 1000);
499
- let holderAlive = false;
500
- try {
501
- process.kill(lockContent.pid ?? 0, 0);
502
- holderAlive = true;
503
- }
504
- catch { /* dead */ }
500
+ const holderAlive = isProcessAlive(lockContent.pid);
505
501
  if (!holderAlive) {
506
502
  console.log(`\n${sym("⚠", "!")} Stale sweep lock detected (PID ${lockContent.pid} is no longer running)`);
507
503
  console.log(` Started: ${lockContent.startedAt ?? "unknown"}`);
@@ -673,7 +669,7 @@ async function runCommand(command, flags) {
673
669
  console.log(`\nRunning: prompts-gpt ${cmd} -f ${file}\n`);
674
670
  const { spawnSync: spSync } = await import("node:child_process");
675
671
  const cliEntry = resolveCliEntry();
676
- const result = spSync(process.execPath, [cliEntry, cmd, "-f", file], { stdio: "inherit", cwd });
672
+ const result = spSync(process.execPath, [cliEntry, cmd, "-f", file], { stdio: "inherit", cwd, shell: false, windowsHide: true });
677
673
  process.exitCode = result.status ?? 1;
678
674
  }
679
675
  }
@@ -826,6 +822,9 @@ async function runCommand(command, flags) {
826
822
  }
827
823
  if (command === "run") {
828
824
  const cwd = getResolvedCwd(flags);
825
+ if (isCI() && !flags["non-interactive"]) {
826
+ flags["non-interactive"] = true;
827
+ }
829
828
  const promptFile = getStringFlag(flags, "prompt-file");
830
829
  const config = await loadRunConfig(cwd);
831
830
  warnOnConfigIssues(config);
@@ -1014,6 +1013,10 @@ async function runCommand(command, flags) {
1014
1013
  const doRun = async () => {
1015
1014
  if (running)
1016
1015
  return;
1016
+ if (!existsSync(resolvedWatch)) {
1017
+ console.log(` File deleted: ${resolvedWatch}. Skipping run.`);
1018
+ return;
1019
+ }
1017
1020
  running = true;
1018
1021
  console.log(`\n${colorize("▶ Running...", "\x1b[36m")} (${new Date().toLocaleTimeString()})`);
1019
1022
  try {
@@ -1041,20 +1044,28 @@ async function runCommand(command, flags) {
1041
1044
  };
1042
1045
  await doRun();
1043
1046
  let debounce = null;
1047
+ const closeWatcher = (message) => {
1048
+ if (debounce) {
1049
+ clearTimeout(debounce);
1050
+ debounce = null;
1051
+ }
1052
+ if (message)
1053
+ console.log(message);
1054
+ watcher.close();
1055
+ };
1044
1056
  const watcher = fsWatch(resolvedWatch, () => {
1057
+ if (!existsSync(resolvedWatch)) {
1058
+ closeWatcher(` File deleted: ${resolvedWatch}. Exiting watch mode.`);
1059
+ return;
1060
+ }
1045
1061
  if (debounce)
1046
1062
  clearTimeout(debounce);
1047
1063
  debounce = setTimeout(doRun, 500);
1048
1064
  debounce.unref?.();
1049
1065
  });
1050
1066
  watcher.on("error", (err) => {
1051
- if (debounce) {
1052
- clearTimeout(debounce);
1053
- debounce = null;
1054
- }
1055
1067
  console.error(`Watch error: ${err instanceof Error ? err.message : String(err)}`);
1056
- console.log("File may have been deleted. Exiting watch mode.");
1057
- watcher.close();
1068
+ closeWatcher("File may have been deleted. Exiting watch mode.");
1058
1069
  });
1059
1070
  await new Promise((resolve) => {
1060
1071
  let settled = false;
@@ -1141,11 +1152,11 @@ async function runCommand(command, flags) {
1141
1152
  try {
1142
1153
  const { spawn: openSpawn } = await import("node:child_process");
1143
1154
  if (process.platform === "win32") {
1144
- openSpawn("cmd", ["/c", "start", "", result.summaryFile], { detached: true, stdio: "ignore", windowsHide: true }).unref();
1155
+ openSpawn("cmd", ["/c", "start", "", result.summaryFile], { detached: true, stdio: "ignore", windowsHide: true, shell: false }).unref();
1145
1156
  }
1146
1157
  else {
1147
1158
  const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
1148
- openSpawn(openCmd, [result.summaryFile], { detached: true, stdio: "ignore" }).unref();
1159
+ openSpawn(openCmd, [result.summaryFile], { detached: true, stdio: "ignore", shell: false }).unref();
1149
1160
  }
1150
1161
  }
1151
1162
  catch { /* ignore */ }
@@ -1229,6 +1240,14 @@ async function runCommand(command, flags) {
1229
1240
  flags["non-interactive"] = true;
1230
1241
  }
1231
1242
  const sweepPromptFile = getStringFlag(flags, "prompt-file");
1243
+ const sweepFilesFlag = getStringFlag(flags, "sweep-files");
1244
+ const allSweepsFlag = Boolean(flags["all-sweeps"]);
1245
+ if (sweepPromptFile && (sweepFilesFlag || allSweepsFlag)) {
1246
+ throw new CliError("Use either --prompt-file for one sweep, --sweep-files for selected sweeps, or --all-sweeps for every sweep file.", CLI_EXIT_CODES.validation, { helpCommand: "sweep" });
1247
+ }
1248
+ if (sweepFilesFlag && allSweepsFlag) {
1249
+ throw new CliError("Use either --sweep-files or --all-sweeps, not both.", CLI_EXIT_CODES.validation, { helpCommand: "sweep" });
1250
+ }
1232
1251
  if (sweepPromptFile && !existsSync(path.resolve(cwd, sweepPromptFile))) {
1233
1252
  throw new CliError(`Sweep file not found: ${sweepPromptFile}\n\nCheck the path and try again. Run \`prompts-gpt list\` to see available sweep files.`, CLI_EXIT_CODES.validation, { helpCommand: "sweep" });
1234
1253
  }
@@ -1255,7 +1274,7 @@ async function runCommand(command, flags) {
1255
1274
  }
1256
1275
  throw new CliError("No supported provider CLI was found on PATH. Install Codex, Cursor Agent, Claude Code, or Copilot CLI, then run `prompts-gpt doctor`.", CLI_EXIT_CODES.validation, { helpCommand: "providers" });
1257
1276
  }
1258
- if (!sweepPromptFile && !Boolean(flags.json)) {
1277
+ if (!sweepPromptFile && !sweepFilesFlag && !allSweepsFlag && !Boolean(flags.json)) {
1259
1278
  const assets = await discoverWorkspaceAssets(cwd);
1260
1279
  if (assets.sweeps.length === 1) {
1261
1280
  const autoFile = assets.sweeps[0].file;
@@ -1656,6 +1675,159 @@ async function runCommand(command, flags) {
1656
1675
  evaluatorModel: getStringFlag(flags, "eval-model") ?? getStringFlag(flags, "evaluator-model"),
1657
1676
  criteria: getStringFlag(flags, "eval-criteria")?.split(",").map((c) => c.trim()),
1658
1677
  } : undefined;
1678
+ const requestedSweepFiles = await resolveSweepFileSelection(cwd, flags);
1679
+ if (requestedSweepFiles.length > 0) {
1680
+ const rawSweepStrategy = getStringFlag(flags, "sweep-strategy") ?? getStringFlag(flags, "files-mode") ?? "sequential";
1681
+ if (rawSweepStrategy !== "sequential" && rawSweepStrategy !== "parallel") {
1682
+ throw new CliError("--sweep-strategy must be either sequential or parallel.", CLI_EXIT_CODES.validation, { helpCommand: "sweep" });
1683
+ }
1684
+ const rawFileConcurrency = parsePositiveIntFlag(getStringFlag(flags, "file-concurrency"), "file-concurrency");
1685
+ const MAX_FILE_CONCURRENCY = 12;
1686
+ const fileConcurrency = rawSweepStrategy === "parallel"
1687
+ ? Math.min(rawFileConcurrency ?? requestedSweepFiles.length, MAX_FILE_CONCURRENCY, requestedSweepFiles.length)
1688
+ : 1;
1689
+ if (rawFileConcurrency && rawFileConcurrency > MAX_FILE_CONCURRENCY) {
1690
+ console.log(`Warning: --file-concurrency ${rawFileConcurrency} capped to ${MAX_FILE_CONCURRENCY} to prevent resource exhaustion.`);
1691
+ }
1692
+ const rawParallelCount = parsePositiveIntFlag(getStringFlag(flags, "parallel"), "parallel");
1693
+ const MAX_PARALLEL_SWEEPS = 16;
1694
+ const parallelCount = rawParallelCount ? Math.min(rawParallelCount, MAX_PARALLEL_SWEEPS) : undefined;
1695
+ if (rawParallelCount && rawParallelCount > MAX_PARALLEL_SWEEPS) {
1696
+ console.log(`Warning: --parallel ${rawParallelCount} capped to ${MAX_PARALLEL_SWEEPS} to prevent resource exhaustion.`);
1697
+ }
1698
+ const explicitIterations = parsePositiveIntFlag(getStringFlag(flags, "iterations"), "iterations");
1699
+ const runBaseId = getStringFlag(flags, "run-id") || `sweep-plan-${new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14)}`;
1700
+ const planStartMs = Date.now();
1701
+ async function resolveIterationsForFile(file) {
1702
+ if (explicitIterations)
1703
+ return explicitIterations;
1704
+ return await readSweepIterationsFromFrontmatter(path.resolve(cwd, file)) ?? 1;
1705
+ }
1706
+ async function runSweepFile(file, index) {
1707
+ const iterationsForFile = await resolveIterationsForFile(file);
1708
+ const fileSlug = path.basename(file, ".md").replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || `file-${index + 1}`;
1709
+ const fileRunId = `${runBaseId}-${String(index + 1).padStart(2, "0")}-${fileSlug}`;
1710
+ if (parallelCount && parallelCount > 1) {
1711
+ const batchSize = Math.min(parallelCount, iterationsForFile);
1712
+ if (!silent) {
1713
+ console.log(`\n${path.basename(file)}: running ${iterationsForFile} independent iteration${iterationsForFile === 1 ? "" : "s"} with parallelism=${batchSize}`);
1714
+ }
1715
+ const allResults = [];
1716
+ for (let batchStart = 0; batchStart < iterationsForFile; batchStart += batchSize) {
1717
+ const batchEnd = Math.min(batchStart + batchSize, iterationsForFile);
1718
+ const batchPromises = [];
1719
+ for (let i = batchStart; i < batchEnd; i++) {
1720
+ batchPromises.push(sweepPrompt({
1721
+ cwd,
1722
+ promptFile: file,
1723
+ agent,
1724
+ model: getStringFlag(flags, "model"),
1725
+ iterations: 1,
1726
+ iterationTimeoutSeconds: parsePositiveIntFlag(getStringFlag(flags, "iteration-timeout"), "iteration-timeout"),
1727
+ maxRetries: parseNonNegativeIntFlag(getStringFlag(flags, "max-retries"), "max-retries"),
1728
+ artifactsDir: getStringFlag(flags, "artifacts-dir"),
1729
+ runId: `${fileRunId}-iter-${i + 1}`,
1730
+ approveMcps: !Boolean(flags["no-approve-mcps"]),
1731
+ sandboxMode: getStringFlag(flags, "sandbox"),
1732
+ phase: getStringFlag(flags, "phase"),
1733
+ dryRun: Boolean(flags["dry-run"]),
1734
+ maxRunDirs: parsePositiveIntFlag(getStringFlag(flags, "max-run-dirs"), "max-run-dirs"),
1735
+ summaryLines: parsePositiveIntFlag(getStringFlag(flags, "summary-lines"), "summary-lines"),
1736
+ background: Boolean(flags.background),
1737
+ permissionMode: getStringFlag(flags, "permission-mode"),
1738
+ evalAfterEachIteration: evalConfig,
1739
+ onProgress,
1740
+ }));
1741
+ }
1742
+ const batchResults = await Promise.allSettled(batchPromises);
1743
+ for (const batchResult of batchResults) {
1744
+ if (batchResult.status === "fulfilled")
1745
+ allResults.push(batchResult.value);
1746
+ else if (!Boolean(flags.json))
1747
+ console.error(`Sweep failed for ${file}: ${batchResult.reason instanceof Error ? batchResult.reason.message : String(batchResult.reason)}`);
1748
+ }
1749
+ }
1750
+ const succeeded = allResults.reduce((sum, result) => sum + result.succeeded, 0);
1751
+ const failed = allResults.reduce((sum, result) => sum + result.failed, 0) + (iterationsForFile - allResults.length);
1752
+ return { file, iterations: iterationsForFile, succeeded, failed, results: allResults };
1753
+ }
1754
+ const result = await sweepPrompt({
1755
+ cwd,
1756
+ promptFile: file,
1757
+ agent,
1758
+ model: getStringFlag(flags, "model"),
1759
+ iterations: iterationsForFile,
1760
+ iterationTimeoutSeconds: parsePositiveIntFlag(getStringFlag(flags, "iteration-timeout"), "iteration-timeout"),
1761
+ maxRetries: parseNonNegativeIntFlag(getStringFlag(flags, "max-retries"), "max-retries"),
1762
+ artifactsDir: getStringFlag(flags, "artifacts-dir"),
1763
+ runId: fileRunId,
1764
+ approveMcps: !Boolean(flags["no-approve-mcps"]),
1765
+ sandboxMode: getStringFlag(flags, "sandbox"),
1766
+ phase: getStringFlag(flags, "phase"),
1767
+ dryRun: Boolean(flags["dry-run"]),
1768
+ maxRunDirs: parsePositiveIntFlag(getStringFlag(flags, "max-run-dirs"), "max-run-dirs"),
1769
+ summaryLines: parsePositiveIntFlag(getStringFlag(flags, "summary-lines"), "summary-lines"),
1770
+ background: Boolean(flags.background),
1771
+ permissionMode: getStringFlag(flags, "permission-mode"),
1772
+ evalAfterEachIteration: evalConfig,
1773
+ onProgress,
1774
+ });
1775
+ return { file, iterations: result.totalIterations, succeeded: result.succeeded, failed: result.failed, results: [result] };
1776
+ }
1777
+ if (!Boolean(flags.json)) {
1778
+ console.log(`Running ${requestedSweepFiles.length} sweep file${requestedSweepFiles.length === 1 ? "" : "s"} with ${rawSweepStrategy} file execution${rawSweepStrategy === "parallel" ? ` (file concurrency=${fileConcurrency})` : ""}.`);
1779
+ if (parallelCount && parallelCount > 1) {
1780
+ console.log(`Each file runs its iterations independently with --parallel ${parallelCount}.`);
1781
+ }
1782
+ }
1783
+ const plannedResults = [];
1784
+ if (rawSweepStrategy === "parallel") {
1785
+ for (let batchStart = 0; batchStart < requestedSweepFiles.length; batchStart += fileConcurrency) {
1786
+ const batch = requestedSweepFiles.slice(batchStart, batchStart + fileConcurrency);
1787
+ const settled = await Promise.allSettled(batch.map((file, offset) => runSweepFile(file, batchStart + offset)));
1788
+ for (const result of settled) {
1789
+ if (result.status === "fulfilled")
1790
+ plannedResults.push(result.value);
1791
+ else if (!Boolean(flags.json))
1792
+ console.error(result.reason instanceof Error ? result.reason.message : String(result.reason));
1793
+ }
1794
+ }
1795
+ }
1796
+ else {
1797
+ for (let index = 0; index < requestedSweepFiles.length; index++) {
1798
+ plannedResults.push(await runSweepFile(requestedSweepFiles[index], index));
1799
+ }
1800
+ }
1801
+ const totalSucceeded = plannedResults.reduce((sum, result) => sum + result.succeeded, 0);
1802
+ const totalFailed = plannedResults.reduce((sum, result) => sum + result.failed, 0);
1803
+ const totalIterations = plannedResults.reduce((sum, result) => sum + result.iterations, 0);
1804
+ const totalDuration = Date.now() - planStartMs;
1805
+ if (Boolean(flags.json)) {
1806
+ console.log(JSON.stringify({
1807
+ multiFile: true,
1808
+ sweepStrategy: rawSweepStrategy,
1809
+ fileConcurrency,
1810
+ files: plannedResults,
1811
+ totalFiles: requestedSweepFiles.length,
1812
+ totalIterations,
1813
+ totalSucceeded,
1814
+ totalFailed,
1815
+ totalDurationMs: totalDuration,
1816
+ }, null, 2));
1817
+ }
1818
+ else {
1819
+ console.log(`\nSweep plan complete: ${totalSucceeded}/${totalIterations} iterations succeeded across ${requestedSweepFiles.length} file${requestedSweepFiles.length === 1 ? "" : "s"}.`);
1820
+ if (totalFailed > 0)
1821
+ console.log(`Failed iterations: ${totalFailed}`);
1822
+ console.log(`Total wall-clock time: ${formatDuration(totalDuration)}`);
1823
+ for (const result of plannedResults) {
1824
+ console.log(` ${result.file}: ${result.succeeded}/${result.iterations} succeeded`);
1825
+ }
1826
+ }
1827
+ if (totalFailed > 0 || plannedResults.length < requestedSweepFiles.length)
1828
+ process.exitCode = 1;
1829
+ return;
1830
+ }
1659
1831
  // Parallel sweep — run N iterations concurrently
1660
1832
  const rawParallelCount = parsePositiveIntFlag(getStringFlag(flags, "parallel"), "parallel");
1661
1833
  const MAX_PARALLEL_SWEEPS = 16;
@@ -1804,11 +1976,11 @@ async function runCommand(command, flags) {
1804
1976
  try {
1805
1977
  const { spawn: openSpawn } = await import("node:child_process");
1806
1978
  if (process.platform === "win32") {
1807
- openSpawn("cmd", ["/c", "start", "", result.manifestFile], { detached: true, stdio: "ignore", windowsHide: true }).unref();
1979
+ openSpawn("cmd", ["/c", "start", "", result.manifestFile], { detached: true, stdio: "ignore", windowsHide: true, shell: false }).unref();
1808
1980
  }
1809
1981
  else {
1810
1982
  const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
1811
- openSpawn(openCmd, [result.manifestFile], { detached: true, stdio: "ignore" }).unref();
1983
+ openSpawn(openCmd, [result.manifestFile], { detached: true, stdio: "ignore", shell: false }).unref();
1812
1984
  }
1813
1985
  }
1814
1986
  catch { /* ignore */ }
@@ -1912,12 +2084,34 @@ async function runCommand(command, flags) {
1912
2084
  }
1913
2085
  else if (event.type === "provider_start") {
1914
2086
  const stepLabel = event.step ? ` [${event.step}]` : "";
1915
- console.log(` ${orchUc ? "" : ">"} ${event.provider}${stepLabel} starting...`);
2087
+ const modelLabel = event.model ? colorize(` (${event.model})`, "\x1b[2m") : "";
2088
+ const promptLabel = event.promptFile ? colorize(` ${event.promptFile}`, "\x1b[2m") : "";
2089
+ console.log(` ${orchUc ? "▶" : ">"} ${event.provider}${stepLabel}${modelLabel} starting...${promptLabel}`);
2090
+ }
2091
+ else if (event.type === "step_context") {
2092
+ console.log(` ${colorize(orchUc ? "↳" : "->", "\x1b[36m")} Receiving ${formatContextSize(event.contextChars)} context from "${event.source}"`);
2093
+ if (event.contextPreview) {
2094
+ console.log(` ${colorize(truncatePreview(event.contextPreview, 120), "\x1b[2m")}`);
2095
+ }
1916
2096
  }
1917
2097
  else if (event.type === "provider_end") {
1918
2098
  const icon = event.exitCode === 0 ? colorize(orchUc ? "✓" : "OK", "\x1b[32m") : colorize(orchUc ? "✗" : "FAIL", "\x1b[31m");
1919
2099
  console.log(` ${icon} ${event.provider} finished (${formatDuration(event.durationMs)}, exit=${event.exitCode})`);
1920
2100
  }
2101
+ else if (event.type === "step_summary") {
2102
+ if (event.filesChanged.length > 0) {
2103
+ console.log(` ${colorize(orchUc ? "📝" : "[files]", "\x1b[36m")} ${event.filesChanged.length} file(s) changed:`);
2104
+ for (const f of event.filesChanged.slice(0, 10)) {
2105
+ console.log(` ${colorize(f, "\x1b[2m")}`);
2106
+ }
2107
+ if (event.filesChanged.length > 10) {
2108
+ console.log(` ${colorize(`...and ${event.filesChanged.length - 10} more`, "\x1b[2m")}`);
2109
+ }
2110
+ }
2111
+ if (event.outputChars > 0) {
2112
+ console.log(` ${colorize(orchUc ? "💬" : "[out]", "\x1b[36m")} Output: ${formatContextSize(event.outputChars)} ${colorize(`→ passed to next step`, "\x1b[2m")}`);
2113
+ }
2114
+ }
1921
2115
  else if (event.type === "eval_start") {
1922
2116
  console.log(` ${orchUc && supportsUnicode() ? "🧪" : "[eval]"} Evaluating with ${event.evaluator}...`);
1923
2117
  }
@@ -1927,8 +2121,12 @@ async function runCommand(command, flags) {
1927
2121
  else if (event.type === "warning") {
1928
2122
  console.log(` ${colorize(orchUc ? "⚠" : "[warn]", "\x1b[33m")} ${event.text}`);
1929
2123
  }
2124
+ else if (event.type === "info") {
2125
+ console.log(` ${colorize(orchUc ? "ℹ" : "[info]", "\x1b[36m")} ${event.text}`);
2126
+ }
1930
2127
  else if (event.type === "orchestrate_end") {
1931
- console.log(`\n${colorize(orchBar, "\x1b[35m")} Done in ${formatDuration(event.totalDurationMs)}${event.winner ? ` | winner=${event.winner}` : ""} ${colorize(orchBar, "\x1b[35m")}\n`);
2128
+ const artLabel = event.artifactsDir ? ` | logs: ${path.relative(cwd, event.artifactsDir)}` : "";
2129
+ console.log(`\n${colorize(orchBar, "\x1b[35m")} Done in ${formatDuration(event.totalDurationMs)}${event.winner ? ` | winner=${event.winner}` : ""}${artLabel} ${colorize(orchBar, "\x1b[35m")}\n`);
1932
2130
  }
1933
2131
  };
1934
2132
  if (mode === "parallel") {
@@ -1991,20 +2189,22 @@ async function runCommand(command, flags) {
1991
2189
  throw new CliError(`Pipeline steps file not found: ${resolvedStepsFile}`, CLI_EXIT_CODES.usage);
1992
2190
  }
1993
2191
  let stepsJson;
2192
+ let pipelineDefaults = {};
1994
2193
  try {
1995
2194
  const parsed = JSON.parse(readFileSync(resolvedStepsFile, "utf8"));
1996
- if (Array.isArray(parsed)) {
2195
+ if (isPipelineStepArray(parsed)) {
1997
2196
  stepsJson = parsed;
1998
2197
  }
1999
- else if (parsed && typeof parsed === "object" && Array.isArray(parsed.steps)) {
2198
+ else if (isPipelineConfigObject(parsed)) {
2000
2199
  stepsJson = parsed.steps;
2200
+ pipelineDefaults = parsed.defaults ?? {};
2001
2201
  }
2002
2202
  else {
2003
- throw new Error("Pipeline steps config must be either an array of steps or an object with a steps array.");
2203
+ throw new Error("Pipeline config must be an array of steps or an object with { defaults?, steps }.");
2004
2204
  }
2005
2205
  }
2006
2206
  catch (error) {
2007
- if (error instanceof Error && error.message.includes("Pipeline steps config must be either")) {
2207
+ if (error instanceof Error && error.message.includes("Pipeline config must be")) {
2008
2208
  throw new CliError(error.message, CLI_EXIT_CODES.validation);
2009
2209
  }
2010
2210
  throw new CliError(`Pipeline steps file contains invalid JSON: ${resolvedStepsFile}`, CLI_EXIT_CODES.validation);
@@ -2012,15 +2212,22 @@ async function runCommand(command, flags) {
2012
2212
  if (!Array.isArray(stepsJson) || stepsJson.length === 0) {
2013
2213
  throw new CliError("Pipeline steps file must contain a non-empty JSON array", CLI_EXIT_CODES.usage);
2014
2214
  }
2015
- const defaultPipelineAgent = resolveRunAgent(flags, config.defaultAgent);
2016
- const globalPipelineModel = getStringFlag(flags, "model");
2215
+ const defaultPipelineAgent = pipelineDefaults.agent?.trim() || resolveRunAgent(flags, config.defaultAgent);
2216
+ const globalPipelineModel = getStringFlag(flags, "model") || pipelineDefaults.model?.trim();
2217
+ const globalPipelineTimeout = pipelineDefaults.timeout;
2218
+ const globalPipelineRetries = pipelineDefaults.retries;
2219
+ const globalPipelineSandbox = pipelineDefaults.sandboxMode;
2017
2220
  for (const step of stepsJson) {
2018
2221
  if (!step || typeof step.name !== "string" || typeof step.promptFile !== "string") {
2019
2222
  throw new CliError(`Pipeline step entries must include name and promptFile fields: ${resolvedStepsFile}`, CLI_EXIT_CODES.validation);
2020
2223
  }
2021
- if (step.promptFile && !existsSync(path.resolve(cwd, step.promptFile))) {
2224
+ const stepPromptPath = path.resolve(cwd, step.promptFile);
2225
+ if (!existsSync(stepPromptPath)) {
2022
2226
  throw new CliError(`Pipeline step "${step.name}" references missing prompt file: ${step.promptFile}`, CLI_EXIT_CODES.usage);
2023
2227
  }
2228
+ if (!statSync(stepPromptPath).isFile()) {
2229
+ throw new CliError(`Pipeline step "${step.name}" promptFile is not a file: ${step.promptFile}`, CLI_EXIT_CODES.usage);
2230
+ }
2024
2231
  const effectiveAgent = typeof step.agent === "string" && step.agent.trim() ? step.agent : defaultPipelineAgent;
2025
2232
  normalizeOrchestrationAgent(effectiveAgent);
2026
2233
  step.agent = effectiveAgent;
@@ -2030,6 +2237,15 @@ async function runCommand(command, flags) {
2030
2237
  if (!step.model && globalPipelineModel) {
2031
2238
  step.model = globalPipelineModel;
2032
2239
  }
2240
+ if (!step.timeout && globalPipelineTimeout) {
2241
+ step.timeout = globalPipelineTimeout;
2242
+ }
2243
+ if (typeof step.retries !== "number" && typeof globalPipelineRetries === "number") {
2244
+ step.retries = globalPipelineRetries;
2245
+ }
2246
+ if (!step.sandboxMode && globalPipelineSandbox) {
2247
+ step.sandboxMode = globalPipelineSandbox;
2248
+ }
2033
2249
  if (step.model) {
2034
2250
  const stepProvider = resolveRunProvider(normalizeOrchestrationAgent(step.agent), providers, config.providerOrder);
2035
2251
  const modelCheck = validateModelForProvider(step.model, stepProvider);
@@ -2038,11 +2254,40 @@ async function runCommand(command, flags) {
2038
2254
  }
2039
2255
  }
2040
2256
  }
2257
+ if (isTTYInteractive(flags) && !jsonOutput && !globalPipelineModel) {
2258
+ for (const step of stepsJson) {
2259
+ if (!step.model) {
2260
+ const stepProvider = resolveRunProvider(normalizeOrchestrationAgent(step.agent ?? defaultPipelineAgent), providers, config.providerOrder);
2261
+ const modelChoices = getModelChoicesForProvider(stepProvider, config);
2262
+ const defaultModel = config.modelOverrides[stepProvider] || DEFAULT_MODELS[stepProvider] || "auto";
2263
+ modelChoices.unshift({ label: `${defaultModel} (default)`, value: defaultModel });
2264
+ const pickedModel = await interactiveSelect(`Model for step "${step.name}" (${stepProvider}):`, modelChoices.filter((c, i, arr) => arr.findIndex((x) => x.value === c.value) === i));
2265
+ if (pickedModel === "__custom__") {
2266
+ step.model = await interactiveInput("Enter custom model name", "");
2267
+ }
2268
+ else {
2269
+ step.model = pickedModel;
2270
+ }
2271
+ }
2272
+ }
2273
+ }
2274
+ if (!jsonOutput && !dryRun) {
2275
+ console.log(`\n Pipeline Plan (${stepsJson.length} steps):`);
2276
+ for (let idx = 0; idx < stepsJson.length; idx++) {
2277
+ const s = stepsJson[idx];
2278
+ const sp = resolveRunProvider(normalizeOrchestrationAgent(s.agent ?? defaultPipelineAgent), providers, config.providerOrder);
2279
+ const sm = s.model?.trim() || config.modelOverrides[sp] || DEFAULT_MODELS[sp] || "auto";
2280
+ console.log(` ${idx + 1}. ${s.name} ${colorize(`(${sp} / ${sm})`, "\x1b[2m")}`);
2281
+ }
2282
+ console.log();
2283
+ }
2284
+ const continueOnError = Boolean(flags["continue-on-error"]);
2041
2285
  const result = await orchestratePipeline({
2042
2286
  cwd,
2043
2287
  steps: stepsJson,
2044
2288
  timeoutSeconds,
2045
2289
  dryRun,
2290
+ continueOnError,
2046
2291
  onProgress,
2047
2292
  });
2048
2293
  if (jsonOutput) {
@@ -2052,12 +2297,25 @@ async function runCommand(command, flags) {
2052
2297
  console.log("Pipeline Results:");
2053
2298
  for (const step of result.steps) {
2054
2299
  const icon = step.result.exitCode === 0 ? sym("✓", "+") : sym("✗", "x");
2055
- const model = step.result.model && step.result.model !== "dry-run" ? ` model=${step.result.model}` : "";
2056
- console.log(` ${icon} ${step.name} (${step.provider}${model}) — ${formatDuration(step.durationMs)}`);
2300
+ const modelStr = step.result.model && step.result.model !== "dry-run" ? colorize(` model=${step.result.model}`, "\x1b[2m") : "";
2301
+ console.log(` ${icon} ${step.name} (${step.provider}${modelStr}) — ${formatDuration(step.durationMs)}`);
2302
+ if (step.result.summaryFile && existsSync(step.result.summaryFile)) {
2303
+ const summary = readFileSync(step.result.summaryFile, "utf8").trim();
2304
+ if (summary) {
2305
+ const summaryPreview = truncatePreview(summary.replace(/\n/g, " "), 150);
2306
+ console.log(` ${colorize(summaryPreview, "\x1b[2m")}`);
2307
+ }
2308
+ }
2057
2309
  }
2058
2310
  const successCount = result.steps.filter((s) => s.result.exitCode === 0).length;
2059
2311
  const failCount = result.steps.filter((s) => s.result.exitCode !== 0).length;
2060
2312
  console.log(`\n Total: ${formatDuration(result.totalDurationMs)} | ${successCount} passed${failCount > 0 ? `, ${failCount} failed` : ""}`);
2313
+ if (result.artifactsDir) {
2314
+ const relativeArtifacts = path.relative(cwd, result.artifactsDir) || result.artifactsDir;
2315
+ console.log(` Artifacts: ${colorize(relativeArtifacts, "\x1b[2m")}`);
2316
+ console.log(`\n Inspect step details:`);
2317
+ console.log(` prompts-gpt diff ${relativeArtifacts}`);
2318
+ }
2061
2319
  }
2062
2320
  return;
2063
2321
  }
@@ -2195,7 +2453,19 @@ async function runCommand(command, flags) {
2195
2453
  if (!existsSync(runDir)) {
2196
2454
  throw new CliError(`Run directory not found: ${runDir}`, CLI_EXIT_CODES.usage);
2197
2455
  }
2198
- const { readFile: fsRead } = await import("node:fs/promises");
2456
+ const { lstat: fsLstat, readFile: fsRead, realpath: fsRealpath } = await import("node:fs/promises");
2457
+ const runDirStat = await fsLstat(runDir).catch((error) => {
2458
+ throw new CliError(`Cannot inspect run directory ${runDir}: ${error instanceof Error ? error.message : String(error)}`, CLI_EXIT_CODES.usage);
2459
+ });
2460
+ if (!runDirStat.isDirectory() || runDirStat.isSymbolicLink()) {
2461
+ throw new CliError(`Invalid run-id: ${runId} is not a regular run directory.`, CLI_EXIT_CODES.usage);
2462
+ }
2463
+ const [realRunDir, realArtifactsDir] = await Promise.all([fsRealpath(runDir), fsRealpath(artifactsDir)]);
2464
+ const realRunDirCheck = caseInsensitive ? realRunDir.toLowerCase() : realRunDir;
2465
+ const realArtifactsDirCheck = caseInsensitive ? realArtifactsDir.toLowerCase() : realArtifactsDir;
2466
+ if (!realRunDirCheck.startsWith(realArtifactsDirCheck + path.sep) && realRunDirCheck !== realArtifactsDirCheck) {
2467
+ throw new CliError("Invalid run-id: resolved path escapes the artifacts directory.", CLI_EXIT_CODES.usage);
2468
+ }
2199
2469
  const deltaFile = path.join(runDir, "worktree-delta.diff");
2200
2470
  if (existsSync(deltaFile)) {
2201
2471
  const delta = await fsRead(deltaFile, "utf8");
@@ -2238,7 +2508,7 @@ async function runCommand(command, flags) {
2238
2508
  console.log("======================");
2239
2509
  console.log("");
2240
2510
  const { spawnSync: gitCheck } = await import("node:child_process");
2241
- const gitResult = gitCheck("git", ["rev-parse", "--is-inside-work-tree"], { cwd, encoding: "utf8", timeout: 5000, windowsHide: true });
2511
+ const gitResult = gitCheck("git", ["rev-parse", "--is-inside-work-tree"], { cwd, encoding: "utf8", timeout: 5000, windowsHide: true, shell: false });
2242
2512
  if (gitResult.status !== 0) {
2243
2513
  console.log(`${sym("⚠", "!")} Not inside a git repository. Prompts-GPT works best in a git repo for worktree tracking.`);
2244
2514
  console.log(" Run: git init\n");
@@ -2373,7 +2643,7 @@ async function runCommand(command, flags) {
2373
2643
  console.log(`\nRunning: prompts-gpt ${cmd} -f ${file}\n`);
2374
2644
  const { spawnSync: spSync } = await import("node:child_process");
2375
2645
  const cliEntry = resolveCliEntry();
2376
- const result = spSync(process.execPath, [cliEntry, cmd, "-f", file], { stdio: "inherit", cwd });
2646
+ const result = spSync(process.execPath, [cliEntry, cmd, "-f", file], { stdio: "inherit", cwd, shell: false, windowsHide: true });
2377
2647
  process.exitCode = result.status ?? 1;
2378
2648
  return;
2379
2649
  }
@@ -2511,7 +2781,7 @@ async function runCommand(command, flags) {
2511
2781
  if (runQs.toLowerCase() !== "n" && runQs.toLowerCase() !== "no") {
2512
2782
  const { spawnSync: spSync } = await import("node:child_process");
2513
2783
  const cliEntry = resolveCliEntry();
2514
- const qsResult = spSync(process.execPath, [cliEntry, "quickstart", "--cwd", cwd], { stdio: "inherit", cwd });
2784
+ const qsResult = spSync(process.execPath, [cliEntry, "quickstart", "--cwd", cwd], { stdio: "inherit", cwd, shell: false, windowsHide: true });
2515
2785
  process.exitCode = qsResult.status ?? 1;
2516
2786
  return;
2517
2787
  }
@@ -2619,7 +2889,7 @@ async function runCommand(command, flags) {
2619
2889
  console.log(`\nRunning: prompts-gpt run -f ${genFile}\n`);
2620
2890
  const { spawnSync: spSync } = await import("node:child_process");
2621
2891
  const cliEntry = resolveCliEntry();
2622
- const genResult = spSync(process.execPath, [cliEntry, "run", "-f", genFile], { stdio: "inherit", cwd });
2892
+ const genResult = spSync(process.execPath, [cliEntry, "run", "-f", genFile], { stdio: "inherit", cwd, shell: false, windowsHide: true });
2623
2893
  process.exitCode = genResult.status ?? 1;
2624
2894
  }
2625
2895
  }
@@ -2704,20 +2974,90 @@ async function runCommand(command, flags) {
2704
2974
  if (existsSync(filePath) && !Boolean(flags.overwrite)) {
2705
2975
  throw new CliError(`Orchestration file already exists: ${filePath}\nUse --overwrite to replace it.`, CLI_EXIT_CODES.validation);
2706
2976
  }
2707
- const content = isPipelineTemplate
2708
- ? PIPELINE_TEMPLATE
2709
- .replace(/\{goal\}/g, goal.trim())
2710
- : ORCHESTRATION_TEMPLATE
2977
+ if (isPipelineTemplate) {
2978
+ const stepsDir = path.join(orchDir, "pipeline-steps");
2979
+ await fsMkdir(stepsDir, { recursive: true });
2980
+ let numSteps = 3;
2981
+ let stepDefs = [
2982
+ { name: "research", description: `Research and analyze requirements for: ${goal.trim()}`, agent: "codex" },
2983
+ { name: "implement", description: `Implement the solution based on the research analysis`, agent: "claude" },
2984
+ { name: "test-and-review", description: `Test, review, and validate the implementation`, agent: "cursor" },
2985
+ ];
2986
+ if (isTTYInteractive()) {
2987
+ const stepCountChoice = await interactiveSelect("How many pipeline steps?", [
2988
+ { label: "2 steps (research + implement)", value: "2" },
2989
+ { label: "3 steps (research + implement + review)", value: "3" },
2990
+ { label: "4 steps (research + implement + test + review)", value: "4" },
2991
+ ]);
2992
+ numSteps = parseInt(stepCountChoice, 10);
2993
+ if (numSteps === 2) {
2994
+ stepDefs = [
2995
+ { name: "research", description: `Research and analyze: ${goal.trim()}`, agent: "codex" },
2996
+ { name: "implement", description: `Implement based on research`, agent: "claude" },
2997
+ ];
2998
+ }
2999
+ else if (numSteps === 4) {
3000
+ stepDefs = [
3001
+ { name: "research", description: `Research and analyze: ${goal.trim()}`, agent: "codex" },
3002
+ { name: "implement", description: `Implement the solution`, agent: "claude" },
3003
+ { name: "test", description: `Run tests and fix failures`, agent: "cursor" },
3004
+ { name: "review", description: `Code review and final validation`, agent: "codex" },
3005
+ ];
3006
+ }
3007
+ for (let si = 0; si < stepDefs.length; si++) {
3008
+ const s = stepDefs[si];
3009
+ const customName = await interactiveInput(`Step ${si + 1} name`, s.name);
3010
+ s.name = customName || s.name;
3011
+ const agentPick = await interactiveSelect(`Step ${si + 1} ("${s.name}") agent:`, [
3012
+ { label: "codex — OpenAI Codex CLI", value: "codex" },
3013
+ { label: "claude — Anthropic Claude Code", value: "claude" },
3014
+ { label: "cursor — Cursor Agent", value: "cursor" },
3015
+ { label: "copilot — GitHub Copilot", value: "copilot" },
3016
+ ]);
3017
+ s.agent = agentPick;
3018
+ }
3019
+ }
3020
+ const stepFilePaths = [];
3021
+ for (const s of stepDefs) {
3022
+ const stepFileName = `${s.name.replace(/[^a-zA-Z0-9_-]/g, "-").toLowerCase()}.md`;
3023
+ const stepFilePath = path.join(stepsDir, stepFileName);
3024
+ stepFilePaths.push(path.relative(cwd, stepFilePath));
3025
+ if (!existsSync(stepFilePath)) {
3026
+ const stepContent = buildPipelineStepPrompt(s.name, s.description, stepDefs.indexOf(s), stepDefs.length);
3027
+ await fsWriteFile(stepFilePath, stepContent);
3028
+ }
3029
+ }
3030
+ const pipelineConfig = {
3031
+ defaults: { timeout: 900 },
3032
+ steps: stepDefs.map((s, si) => ({
3033
+ name: s.name,
3034
+ promptFile: stepFilePaths[si],
3035
+ agent: s.agent,
3036
+ })),
3037
+ };
3038
+ await fsWriteFile(filePath, JSON.stringify(pipelineConfig, null, 2) + "\n");
3039
+ console.log(`${sym("\u2713", "+")} Created pipeline orchestration:`);
3040
+ console.log(` Config: ${path.relative(cwd, filePath)}`);
3041
+ for (const fp of stepFilePaths) {
3042
+ console.log(` Prompt: ${fp}`);
3043
+ }
3044
+ console.log(`\nRun it with:`);
3045
+ console.log(` prompts-gpt orchestrate --mode pipeline --steps ${path.relative(cwd, filePath)}`);
3046
+ console.log(`\nEdit the step prompt files to customize behavior, then run the pipeline.`);
3047
+ }
3048
+ else {
3049
+ const content = ORCHESTRATION_TEMPLATE
2711
3050
  .replace(/\{title\}/g, title.trim())
2712
3051
  .replace(/\{description\}/g, (description || title).trim())
2713
3052
  .replace('"parallel"', `"${mode}"`)
2714
3053
  .replace("{goal}", goal.trim())
2715
3054
  .replace("{step1}", `Analyze the requirements for: ${goal.trim()}`)
2716
3055
  .replace("{step2}", `Implement the solution based on the analysis`);
2717
- await fsWriteFile(filePath, content);
2718
- console.log(`${sym("\u2713", "+")} Created orchestration file: ${filePath}`);
2719
- console.log(`\nRun it with:`);
2720
- console.log(` prompts-gpt orchestrate --mode ${mode}${isPipelineTemplate ? ` --steps ${path.relative(cwd, filePath)}` : ` -f ${path.relative(cwd, filePath)}`}`);
3056
+ await fsWriteFile(filePath, content);
3057
+ console.log(`${sym("\u2713", "+")} Created orchestration file: ${filePath}`);
3058
+ console.log(`\nRun it with:`);
3059
+ console.log(` prompts-gpt orchestrate --mode ${mode} -f ${path.relative(cwd, filePath)}`);
3060
+ }
2721
3061
  return;
2722
3062
  }
2723
3063
  if (command === "sync") {
@@ -2837,7 +3177,7 @@ async function runCommand(command, flags) {
2837
3177
  const { spawnSync: spSync } = await import("node:child_process");
2838
3178
  const cliEntry = resolveCliEntry();
2839
3179
  const projCwd = getResolvedCwd(flags);
2840
- const syncResult = spSync(process.execPath, [cliEntry, "sync", "--cwd", projCwd], { stdio: "inherit", cwd: projCwd });
3180
+ const syncResult = spSync(process.execPath, [cliEntry, "sync", "--cwd", projCwd], { stdio: "inherit", cwd: projCwd, shell: false, windowsHide: true });
2841
3181
  process.exitCode = syncResult.status ?? 1;
2842
3182
  }
2843
3183
  }
@@ -3124,6 +3464,11 @@ function getCommandOptions(command) {
3124
3464
  quiet: { type: "boolean", short: "q" },
3125
3465
  silent: { type: "boolean" },
3126
3466
  parallel: { type: "string" },
3467
+ "all-sweeps": { type: "boolean" },
3468
+ "sweep-files": { type: "string" },
3469
+ "sweep-strategy": { type: "string" },
3470
+ "files-mode": { type: "string" },
3471
+ "file-concurrency": { type: "string" },
3127
3472
  eval: { type: "boolean" },
3128
3473
  "eval-criteria": { type: "string" },
3129
3474
  "eval-agent": { type: "string" },
@@ -3151,6 +3496,7 @@ function getCommandOptions(command) {
3151
3496
  model: { type: "string" },
3152
3497
  timeout: { type: "string" },
3153
3498
  "dry-run": { type: "boolean" },
3499
+ "continue-on-error": { type: "boolean" },
3154
3500
  "non-interactive": { type: "boolean" },
3155
3501
  "list-models": { type: "boolean" },
3156
3502
  };
@@ -3426,6 +3772,7 @@ function readTokenFromPrompt(command) {
3426
3772
  stdin.setRawMode?.(true);
3427
3773
  }
3428
3774
  catch {
3775
+ cleanup();
3429
3776
  reject(new CliError("Cannot enable raw mode for token prompt. Use --token-stdin instead.", CLI_EXIT_CODES.usage, { helpCommand: command }));
3430
3777
  return;
3431
3778
  }
@@ -3513,15 +3860,54 @@ async function applyDoctorFixes(cwd) {
3513
3860
  skipped.push(`${DEFAULT_PROMPTS_GPT_OUT_DIR}/ directory already exists`);
3514
3861
  }
3515
3862
  try {
3516
- await ensureGitignoreEntry(cwd, ".prompts-gpt/.credentials.json");
3517
- await ensureGitignoreEntry(cwd, ".prompts-gpt/.models.json");
3518
- await ensureGitignoreEntry(cwd, ".scripts/runs/");
3519
- await ensureGitignoreEntry(cwd, ".sweep.lock");
3520
- applied.push("Updated .gitignore with sensitive file and sweep artifact patterns");
3863
+ const gitignoreEntries = [
3864
+ ".prompts-gpt/.credentials.json",
3865
+ ".prompts-gpt/.models.json",
3866
+ ".scripts/runs",
3867
+ ".sweep.lock",
3868
+ ];
3869
+ const gitignoreUpdates = [];
3870
+ for (const entry of gitignoreEntries) {
3871
+ gitignoreUpdates.push(await ensureGitignoreEntry(cwd, entry));
3872
+ }
3873
+ if (gitignoreUpdates.some(Boolean)) {
3874
+ applied.push("Updated .gitignore with sensitive file and sweep artifact patterns");
3875
+ }
3876
+ else {
3877
+ skipped.push(".gitignore already contains sensitive file and sweep artifact patterns");
3878
+ }
3521
3879
  }
3522
3880
  catch {
3523
3881
  failed.push("Could not update .gitignore with sensitive file and sweep artifact patterns");
3524
3882
  }
3883
+ const sweepLockPath = path.resolve(cwd, ".sweep.lock");
3884
+ if (existsSync(sweepLockPath)) {
3885
+ try {
3886
+ const lockRaw = readFileSync(sweepLockPath, "utf8");
3887
+ const lockData = JSON.parse(lockRaw);
3888
+ const lockAge = Date.now() - new Date(lockData.startedAt ?? 0).getTime();
3889
+ const lockAgeHours = lockAge / (60 * 60 * 1000);
3890
+ const holderAlive = isProcessAlive(lockData.pid);
3891
+ if (!holderAlive || (Number.isFinite(lockAgeHours) && lockAgeHours >= 4)) {
3892
+ const { rm: fsRm } = await import("node:fs/promises");
3893
+ await fsRm(sweepLockPath, { force: true });
3894
+ applied.push("Removed stale sweep lock");
3895
+ }
3896
+ else {
3897
+ skipped.push("Sweep lock is active (held by running process)");
3898
+ }
3899
+ }
3900
+ catch {
3901
+ try {
3902
+ const { rm: fsRm } = await import("node:fs/promises");
3903
+ await fsRm(sweepLockPath, { force: true });
3904
+ applied.push("Removed corrupt sweep lock file");
3905
+ }
3906
+ catch {
3907
+ failed.push("Could not remove corrupt sweep lock file");
3908
+ }
3909
+ }
3910
+ }
3525
3911
  return { applied, skipped, failed };
3526
3912
  }
3527
3913
  function getStringFlag(flags, name) {
@@ -3604,6 +3990,64 @@ function formatDuration(ms) {
3604
3990
  return `${minutes}m ${seconds}s`;
3605
3991
  return `${seconds}s`;
3606
3992
  }
3993
+ function formatContextSize(chars) {
3994
+ if (chars < 1000)
3995
+ return `${chars} chars`;
3996
+ if (chars < 100_000)
3997
+ return `${(chars / 1000).toFixed(1)}k chars`;
3998
+ return `${(chars / 1_000_000).toFixed(2)}M chars`;
3999
+ }
4000
+ function truncatePreview(text, maxLen) {
4001
+ const clean = text.replace(/\s+/g, " ").trim();
4002
+ if (clean.length <= maxLen)
4003
+ return clean;
4004
+ return clean.slice(0, maxLen - 3) + "...";
4005
+ }
4006
+ function isPlainRecord(value) {
4007
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
4008
+ }
4009
+ function isOptionalString(value) {
4010
+ return value === undefined || typeof value === "string";
4011
+ }
4012
+ function isOptionalNumber(value) {
4013
+ return value === undefined || (typeof value === "number" && Number.isFinite(value));
4014
+ }
4015
+ function isStringRecord(value) {
4016
+ if (value === undefined)
4017
+ return true;
4018
+ if (!isPlainRecord(value))
4019
+ return false;
4020
+ return Object.entries(value).every(([key, entry]) => key !== "__proto__" && key !== "constructor" && key !== "prototype" && typeof entry === "string");
4021
+ }
4022
+ function isPipelineStepEntry(value) {
4023
+ if (!isPlainRecord(value))
4024
+ return false;
4025
+ return typeof value.name === "string" &&
4026
+ typeof value.promptFile === "string" &&
4027
+ isOptionalString(value.agent) &&
4028
+ isOptionalString(value.model) &&
4029
+ isOptionalNumber(value.timeout) &&
4030
+ isOptionalNumber(value.retries) &&
4031
+ isOptionalString(value.sandboxMode) &&
4032
+ isStringRecord(value.env);
4033
+ }
4034
+ function isPipelineStepArray(value) {
4035
+ return Array.isArray(value) && value.every(isPipelineStepEntry);
4036
+ }
4037
+ function isPipelineDefaults(value) {
4038
+ if (value === undefined)
4039
+ return true;
4040
+ if (!isPlainRecord(value))
4041
+ return false;
4042
+ return isOptionalString(value.agent) &&
4043
+ isOptionalString(value.model) &&
4044
+ isOptionalNumber(value.timeout) &&
4045
+ isOptionalNumber(value.retries) &&
4046
+ isOptionalString(value.sandboxMode);
4047
+ }
4048
+ function isPipelineConfigObject(value) {
4049
+ return isPlainRecord(value) && isPipelineStepArray(value.steps) && isPipelineDefaults(value.defaults);
4050
+ }
3607
4051
  function formatList(values) {
3608
4052
  if (!Array.isArray(values) || values.length === 0)
3609
4053
  return "None";
@@ -3814,82 +4258,6 @@ async function saveLastUsedModel(cwd, provider, model) {
3814
4258
  }
3815
4259
  const CODEX_API_KEY_ENV_NAMES = ["OPENAI_API_KEY", "CODEX_API_KEY", "OPENAI_API_KEY_FILE"];
3816
4260
  const CODEX_CHATGPT_UNSUPPORTED_MODELS = new Set(["gpt-5.5-pro", "gpt-5.4-pro", "gpt-5.5-high-fast"]);
3817
- const PROVIDER_MODELS = Object.freeze({
3818
- codex: [
3819
- { value: "gpt-5.5", label: "gpt-5.5 — frontier coding & reasoning", tier: "frontier" },
3820
- { value: "gpt-5.5-pro", label: "gpt-5.5-pro — smarter, more precise", tier: "frontier" },
3821
- { value: "gpt-5.4", label: "gpt-5.4 — strong coding model", tier: "standard" },
3822
- { value: "gpt-5.4-pro", label: "gpt-5.4-pro — enhanced responses", tier: "standard" },
3823
- { value: "gpt-5.4-mini", label: "gpt-5.4-mini — fast coding & subagents", tier: "fast" },
3824
- { value: "gpt-5.4-nano", label: "gpt-5.4-nano — cheapest high-volume", tier: "budget" },
3825
- { value: "gpt-5.3-codex", label: "gpt-5.3-codex — merged GPT-5 + Codex", tier: "standard" },
3826
- { value: "gpt-5.3-codex-spark", label: "gpt-5.3-codex-spark — 15x faster gen", tier: "fast" },
3827
- { value: "o3", label: "o3 — advanced reasoning", tier: "frontier" },
3828
- ],
3829
- claude: [
3830
- { value: "claude-opus-4-7", label: "claude-opus-4-7 — most capable model", tier: "frontier" },
3831
- { value: "claude-opus-4-6", label: "claude-opus-4-6 — previous gen opus", tier: "frontier" },
3832
- { value: "claude-sonnet-4-6", label: "claude-sonnet-4-6 — balanced default", tier: "standard" },
3833
- { value: "claude-sonnet-4-5", label: "claude-sonnet-4-5 — previous gen sonnet", tier: "standard" },
3834
- { value: "claude-3-5-haiku", label: "claude-3-5-haiku — fastest & cheapest", tier: "fast" },
3835
- { value: "claude-haiku-4-5", label: "claude-haiku-4-5 — fast haiku", tier: "fast" },
3836
- ],
3837
- cursor: [
3838
- { value: "auto", label: "auto — Cursor auto-selects best", tier: "standard" },
3839
- { value: "claude-4.7-opus", label: "claude-4.7-opus — Claude Opus 4.7", tier: "frontier" },
3840
- { value: "claude-4.7-opus-fast", label: "claude-4.7-opus-fast — Claude Opus 4.7 Fast", tier: "frontier" },
3841
- { value: "claude-4.6-opus-high", label: "claude-4.6-opus-high — Claude Opus 4.6 High", tier: "frontier" },
3842
- { value: "claude-4.6-opus-high-thinking", label: "claude-4.6-opus-high-thinking — Claude Opus 4.6 Thinking", tier: "frontier" },
3843
- { value: "claude-4.6-sonnet-high", label: "claude-4.6-sonnet-high — fast + smart", tier: "standard" },
3844
- { value: "gpt-5.5", label: "gpt-5.5 — OpenAI frontier", tier: "frontier" },
3845
- { value: "gpt-5.5-high-fast", label: "gpt-5.5-high-fast — GPT frontier fast", tier: "frontier" },
3846
- { value: "gpt-5.4", label: "gpt-5.4 — OpenAI strong coding", tier: "standard" },
3847
- { value: "gpt-5.4-mini", label: "gpt-5.4-mini — fast & affordable", tier: "fast" },
3848
- { value: "gpt-5.4-nano", label: "gpt-5.4-nano — cheapest", tier: "budget" },
3849
- { value: "gpt-5.3-codex", label: "gpt-5.3-codex — OpenAI codex", tier: "standard" },
3850
- { value: "gpt-5.2", label: "gpt-5.2 — OpenAI coding", tier: "standard" },
3851
- { value: "gpt-5", label: "gpt-5 — OpenAI base", tier: "standard" },
3852
- { value: "gpt-5-fast", label: "gpt-5-fast — faster GPT-5", tier: "standard" },
3853
- { value: "gpt-5-mini", label: "gpt-5-mini — small GPT-5", tier: "fast" },
3854
- { value: "o3-pro-high", label: "o3-pro-high — advanced reasoning", tier: "frontier" },
3855
- { value: "composer-2.5", label: "composer-2.5 — Cursor latest model", tier: "standard" },
3856
- { value: "composer-2", label: "composer-2 — balanced multi-file", tier: "standard" },
3857
- { value: "composer-2-fast", label: "composer-2-fast — speed optimized", tier: "fast" },
3858
- { value: "composer-1.5", label: "composer-1.5 — legacy capable", tier: "standard" },
3859
- { value: "composer-1", label: "composer-1 — legacy model", tier: "fast" },
3860
- { value: "gemini-3.1-pro", label: "gemini-3.1-pro — Google latest", tier: "standard" },
3861
- { value: "gemini-3-pro", label: "gemini-3-pro — Google frontier", tier: "standard" },
3862
- { value: "gemini-3-flash", label: "gemini-3-flash — Google fast", tier: "fast" },
3863
- { value: "gemini-2.5-flash", label: "gemini-2.5-flash — Google budget", tier: "fast" },
3864
- { value: "grok-4.3", label: "grok-4.3 — xAI frontier", tier: "frontier" },
3865
- { value: "grok-4.20", label: "grok-4.20 — xAI standard", tier: "standard" },
3866
- { value: "kimi-k2.5", label: "kimi-k2.5 — Moonshot coding", tier: "standard" },
3867
- { value: "claude-4.5-opus", label: "claude-4.5-opus — previous gen opus", tier: "standard" },
3868
- { value: "claude-4.5-haiku", label: "claude-4.5-haiku — fast haiku", tier: "fast" },
3869
- ],
3870
- copilot: [
3871
- { value: "auto", label: "auto — Copilot auto-selects", tier: "standard" },
3872
- { value: "claude-opus-4.7", label: "claude-opus-4.7 — Anthropic frontier", tier: "frontier" },
3873
- { value: "claude-opus-4.6", label: "claude-opus-4.6 — Anthropic frontier", tier: "frontier" },
3874
- { value: "claude-sonnet-4.6", label: "claude-sonnet-4.6 — Anthropic balanced", tier: "standard" },
3875
- { value: "claude-sonnet-4.5", label: "claude-sonnet-4.5 — Anthropic previous gen", tier: "standard" },
3876
- { value: "claude-opus-4.5", label: "claude-opus-4.5 — Anthropic legacy opus", tier: "standard" },
3877
- { value: "claude-haiku-4.5", label: "claude-haiku-4.5 — fastest", tier: "fast" },
3878
- { value: "gpt-5.5", label: "gpt-5.5 — OpenAI frontier", tier: "frontier" },
3879
- { value: "gpt-5.4", label: "gpt-5.4 — OpenAI standard", tier: "standard" },
3880
- { value: "gpt-5.4-mini", label: "gpt-5.4-mini — fast included", tier: "fast" },
3881
- { value: "gpt-5.4-nano", label: "gpt-5.4-nano — cheapest high-volume", tier: "budget" },
3882
- { value: "gpt-5.3-codex", label: "gpt-5.3-codex — OpenAI coding", tier: "standard" },
3883
- { value: "gpt-5-mini", label: "gpt-5-mini — included fast model", tier: "fast" },
3884
- { value: "gemini-3.1-pro", label: "gemini-3.1-pro — Google latest", tier: "frontier" },
3885
- { value: "gemini-3-pro", label: "gemini-3-pro — Google frontier", tier: "standard" },
3886
- { value: "gemini-2.5-pro", label: "gemini-2.5-pro — Google frontier", tier: "standard" },
3887
- { value: "gemini-2.5-flash", label: "gemini-2.5-flash — Google fast & affordable", tier: "fast" },
3888
- { value: "gemini-3-flash", label: "gemini-3-flash — Google fast", tier: "fast" },
3889
- { value: "raptor-mini", label: "raptor-mini — preview tuned mini", tier: "fast" },
3890
- { value: "goldeneye", label: "goldeneye — preview tuned coding model", tier: "budget" },
3891
- ],
3892
- });
3893
4261
  // Model names are used directly — no alias resolution needed
3894
4262
  function hasCodexApiKeyAuth(env = process.env) {
3895
4263
  return CODEX_API_KEY_ENV_NAMES.some((name) => String(env[name] ?? "").trim().length > 0);
@@ -4342,9 +4710,14 @@ Usage:
4342
4710
  prompts-gpt generate-orchestration [--title <text>] [--goal <text>] [--description <text>] [--mode <parallel|pipeline|eval>] [--overwrite] [--cwd <path>]
4343
4711
 
4344
4712
  Why use it:
4345
- Scaffolds a new orchestration template locally for multi-agent workflows.
4346
- Pipeline mode emits a runnable JSON steps file; parallel and eval modes emit Markdown prompt templates.
4347
- Run interactively (no flags) for a guided experience, or pass all flags for scripting.
4713
+ Scaffolds a new orchestration locally. Pipeline mode creates a JSON config AND step prompt files
4714
+ so you can immediately run the pipeline. Parallel and eval modes create Markdown prompt templates.
4715
+ Run interactively (no flags) for a guided wizard that lets you pick steps, agents, and models.
4716
+
4717
+ What it creates (pipeline mode):
4718
+ .prompts-gpt/orchestrations/<name>.json — Pipeline config with steps
4719
+ .prompts-gpt/orchestrations/pipeline-steps/ — Step prompt Markdown files
4720
+ research.md, implement.md, test-and-review.md
4348
4721
 
4349
4722
  Options:
4350
4723
  --title <text> Required. The orchestration title (used as filename).
@@ -4356,8 +4729,15 @@ Options:
4356
4729
  --help Show this command help.
4357
4730
 
4358
4731
  Examples:
4732
+ # Interactive wizard — pick steps, agents, models
4359
4733
  prompts-gpt generate-orchestration
4734
+
4735
+ # Non-interactive pipeline scaffolding
4360
4736
  prompts-gpt generate-orchestration --title "Full Stack Feature" --goal "Add user auth" --mode pipeline
4737
+
4738
+ # Then customize the generated step prompts and run
4739
+ # Edit: .prompts-gpt/orchestrations/pipeline-steps/*.md
4740
+ prompts-gpt orchestrate --mode pipeline --steps .prompts-gpt/orchestrations/full-stack-feature.json
4361
4741
  `;
4362
4742
  }
4363
4743
  if (command === "completions") {
@@ -4485,6 +4865,8 @@ Examples:
4485
4865
 
4486
4866
  Usage:
4487
4867
  prompts-gpt sweep [-f <path>] [-n <count>] [--agent <name>] [--model <name>] [--dry-run]
4868
+ prompts-gpt sweep --all-sweeps [--sweep-strategy sequential|parallel] [-n <count>]
4869
+ prompts-gpt sweep --sweep-files <a,b,c> [--sweep-strategy sequential|parallel] [-n <count>]
4488
4870
 
4489
4871
  Why use it:
4490
4872
  Runs the same prompt N times, feeding each iteration's summary into the next.
@@ -4496,6 +4878,11 @@ Why use it:
4496
4878
  Options:
4497
4879
  -f, --prompt-file <path> Prompt file to sweep. Auto-detects local sweeps if omitted.
4498
4880
  -n, --iterations <n> Number of iterations. Interactive default: 1.
4881
+ --all-sweeps Run every file in .prompts-gpt/sweeps.
4882
+ --sweep-files <list> Comma-separated sweep paths, filenames, or sweep names.
4883
+ --sweep-strategy <mode> File execution mode: sequential or parallel. Default: sequential.
4884
+ --files-mode <mode> Alias for --sweep-strategy.
4885
+ --file-concurrency <n> Max sweep files to run at once in parallel mode.
4499
4886
  --agent <name> Orchestration profile. Default from config or router.
4500
4887
  --model <name> Model override for the selected provider.
4501
4888
  --iteration-timeout <secs> Timeout per iteration in seconds. Default: 5400 (90 min)
@@ -4510,7 +4897,7 @@ Options:
4510
4897
  --max-run-dirs <n> Max artifact directories to keep. Default: 20
4511
4898
  --summary-lines <n> Lines of summary to extract per iteration. Default: 40
4512
4899
  --dry-run Preview what the sweep would do without executing.
4513
- --parallel <n> Run iterations in parallel batches (experimental).
4900
+ --parallel <n> Run iterations in parallel batches inside each selected file (experimental).
4514
4901
  --quiet, -q Suppress live tool/message logs (keep iteration headers).
4515
4902
  --silent Suppress all output except errors and final result.
4516
4903
  --list-models List available models for the selected provider.
@@ -4534,6 +4921,11 @@ Examples:
4534
4921
  prompts-gpt sweep # interactive picker
4535
4922
  prompts-gpt sweep -f .prompts-gpt/sweeps/sdk-hardening.md # explicit file
4536
4923
  prompts-gpt sweep -f .prompts-gpt/sweeps/design.md -n 5 # 5 iterations
4924
+ prompts-gpt sweep --all-sweeps --sweep-strategy sequential # every sweep file, one after another
4925
+ prompts-gpt sweep --all-sweeps --sweep-strategy parallel -n 2 # every sweep file in parallel, 2 iterations each
4926
+ prompts-gpt sweep --sweep-files design,research --sweep-strategy parallel -n 2
4927
+ prompts-gpt sweep --sweep-files .prompts-gpt/sweeps/a.md,.prompts-gpt/sweeps/b.md --sweep-strategy sequential
4928
+ prompts-gpt sweep -f .prompts-gpt/sweeps/design.md -n 6 --parallel 3
4537
4929
  prompts-gpt sweep --model claude-sonnet-4-6 # specify model
4538
4930
  prompts-gpt sweep --quiet # suppress live logs
4539
4931
  prompts-gpt sweep --list-models --agent codex # see codex models
@@ -4677,7 +5069,7 @@ Options:
4677
5069
  return `prompts-gpt orchestrate
4678
5070
 
4679
5071
  Usage:
4680
- prompts-gpt orchestrate --mode <parallel|pipeline|eval> [--prompt-file <path>] [--providers <list>] [--agent <name>] [--model <name>] [--timeout <secs>] [--evaluator <name>] [--evaluator-model <name>] [--criteria <list>] [--eval-criteria <list>] [--steps <file>] [--dry-run] [--json] [--cwd <path>]
5072
+ prompts-gpt orchestrate --mode <parallel|pipeline|eval> [--prompt-file <path>] [--providers <list>] [--agent <name>] [--model <name>] [--timeout <secs>] [--evaluator <name>] [--evaluator-model <name>] [--criteria <list>] [--eval-criteria <list>] [--steps <file>] [--continue-on-error] [--dry-run] [--json] [--cwd <path>]
4681
5073
 
4682
5074
  Why use it:
4683
5075
  Run multi-agent orchestration — execute the same prompt across multiple providers in parallel,
@@ -4700,6 +5092,7 @@ Options:
4700
5092
  --eval-criteria <list> Alias for --criteria in eval mode and sweep self-evaluation.
4701
5093
  --steps <file> JSON step file for pipeline mode.
4702
5094
  --timeout <secs> Timeout per provider in seconds.
5095
+ --continue-on-error Continue running remaining pipeline steps even if one fails.
4703
5096
  --dry-run Preview what the orchestration would do.
4704
5097
  --list-models Show model choices for the resolved provider(s).
4705
5098
  --non-interactive Skip all interactive prompts.
@@ -4825,8 +5218,8 @@ function slugifyFilename(text, fallback) {
4825
5218
  }
4826
5219
  function buildProgressBar(completed, total, width) {
4827
5220
  const safeWidth = Number.isFinite(width) && width > 0 ? Math.trunc(width) : 20;
4828
- const safeTotal = Math.max(total, 1);
4829
- const safeCompleted = Math.max(0, Math.min(completed, safeTotal));
5221
+ const safeTotal = Number.isFinite(total) && total > 0 ? Math.trunc(total) : 1;
5222
+ const safeCompleted = Number.isFinite(completed) ? Math.max(0, Math.min(Math.trunc(completed), safeTotal)) : 0;
4830
5223
  const fraction = safeCompleted / safeTotal;
4831
5224
  const filled = Math.min(Math.floor(fraction * safeWidth), safeWidth);
4832
5225
  const empty = safeWidth - filled;
@@ -4912,9 +5305,12 @@ function interactiveSelect(prompt, options) {
4912
5305
  const maxVisible = Math.min(options.length, Math.max(3, termRows - 4));
4913
5306
  const useUnicode = supportsUnicode();
4914
5307
  const pointer = useUnicode ? "\u276f" : ">";
4915
- const formatLine = (entry, index, selected) => {
4916
- const numberWidth = String(getVisibleOptions().length).length;
4917
- const num = String(index + 1).padStart(numberWidth, " ");
5308
+ const getScrollStart = (visible) => visible.length <= maxVisible
5309
+ ? 0
5310
+ : Math.max(0, Math.min(cursor - Math.floor(maxVisible / 2), visible.length - maxVisible));
5311
+ const formatLine = (entry, shortcutIndex, selected) => {
5312
+ const numberWidth = String(Math.min(9, maxVisible)).length;
5313
+ const num = shortcutIndex <= 9 ? String(shortcutIndex).padStart(numberWidth, " ") : " ".repeat(numberWidth);
4918
5314
  const prefix = selected ? colorize(pointer, "\x1b[36m") : " ";
4919
5315
  const label = selected ? colorize(entry.label, "\x1b[1m") : entry.label;
4920
5316
  return ` ${prefix} ${num}. ${label}`;
@@ -4935,15 +5331,13 @@ function interactiveSelect(prompt, options) {
4935
5331
  const visibleCount = Math.min(maxVisible, visible.length);
4936
5332
  const posLabel = visible.length > 1 ? ` (${cursor + 1}/${visible.length})` : "";
4937
5333
  const filterLabel = filterText ? colorize(` filter: "${filterText}"`, "\x1b[33m") : "";
4938
- const scrollStart = visible.length <= maxVisible
4939
- ? 0
4940
- : Math.max(0, Math.min(cursor - Math.floor(maxVisible / 2), visible.length - maxVisible));
5334
+ const scrollStart = getScrollStart(visible);
4941
5335
  const lines = [];
4942
5336
  lines.push(`${prompt}${posLabel}${filterLabel}`);
4943
5337
  for (let vi = 0; vi < visibleCount; vi++) {
4944
5338
  const i = scrollStart + vi;
4945
5339
  if (i < visible.length) {
4946
- lines.push(formatLine(visible[i], i, i === cursor));
5340
+ lines.push(formatLine(visible[i], vi + 1, i === cursor));
4947
5341
  }
4948
5342
  }
4949
5343
  if (visible.length > maxVisible) {
@@ -5000,7 +5394,7 @@ function interactiveSelect(prompt, options) {
5000
5394
  return;
5001
5395
  }
5002
5396
  stdin.resume();
5003
- const onSigint = () => {
5397
+ const cancelSelection = () => {
5004
5398
  if (settled)
5005
5399
  return;
5006
5400
  settled = true;
@@ -5011,11 +5405,17 @@ function interactiveSelect(prompt, options) {
5011
5405
  catch { /* stdout may be closed */ }
5012
5406
  reject(new CliError("Selection cancelled.", CLI_EXIT_CODES.general));
5013
5407
  };
5408
+ const onSigint = () => { cancelSelection(); };
5409
+ const onSigterm = () => { cancelSelection(); };
5014
5410
  const cleanup = () => {
5015
5411
  try {
5016
5412
  process.removeListener("SIGINT", onSigint);
5017
5413
  }
5018
5414
  catch { /* ignore */ }
5415
+ try {
5416
+ process.removeListener("SIGTERM", onSigterm);
5417
+ }
5418
+ catch { /* ignore */ }
5019
5419
  try {
5020
5420
  stdin.removeListener("data", onData);
5021
5421
  }
@@ -5047,6 +5447,7 @@ function interactiveSelect(prompt, options) {
5047
5447
  stdin.on("end", onEnd);
5048
5448
  stdin.on("error", onError);
5049
5449
  process.on("SIGINT", onSigint);
5450
+ process.on("SIGTERM", onSigterm);
5050
5451
  const onData = (data) => {
5051
5452
  const visible = getVisibleOptions();
5052
5453
  for (let ci = 0; ci < data.length; ci++) {
@@ -5182,17 +5583,18 @@ function interactiveSelect(prompt, options) {
5182
5583
  }
5183
5584
  else if (ch >= "1" && ch <= "9" && !filterText) {
5184
5585
  const idx = parseInt(ch, 10) - 1;
5185
- if (idx < visible.length && idx < 9) {
5586
+ const visibleIndex = getScrollStart(visible) + idx;
5587
+ if (visibleIndex < visible.length && idx < 9) {
5186
5588
  if (settled)
5187
5589
  return;
5188
5590
  settled = true;
5189
5591
  cleanup();
5190
5592
  stdout.write("\n");
5191
- resolve(visible[idx].value);
5593
+ resolve(visible[visibleIndex].value);
5192
5594
  return;
5193
5595
  }
5194
5596
  }
5195
- else if (ch >= " " && ch <= "~") {
5597
+ else if (isPrintableInteractiveInput(ch)) {
5196
5598
  filterText += ch;
5197
5599
  cursor = 0;
5198
5600
  render();
@@ -5202,6 +5604,11 @@ function interactiveSelect(prompt, options) {
5202
5604
  stdin.on("data", onData);
5203
5605
  });
5204
5606
  }
5607
+ function isPrintableInteractiveInput(value) {
5608
+ if (!value)
5609
+ return false;
5610
+ return !/[\x00-\x1f\x7f]/.test(value);
5611
+ }
5205
5612
  async function interactiveInput(prompt, defaultValue) {
5206
5613
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
5207
5614
  throw new CliError("Interactive input requires a TTY.", CLI_EXIT_CODES.usage);
@@ -5225,6 +5632,14 @@ async function interactiveInput(prompt, defaultValue) {
5225
5632
  resolved = true;
5226
5633
  resolve(defaultValue || "");
5227
5634
  });
5635
+ rl.on("error", (err) => {
5636
+ if (resolved)
5637
+ return;
5638
+ resolved = true;
5639
+ rl.close();
5640
+ rl.removeAllListeners();
5641
+ reject(new CliError(`Input stream error: ${err.message}`, CLI_EXIT_CODES.general));
5642
+ });
5228
5643
  rl.on("SIGINT", () => {
5229
5644
  if (resolved)
5230
5645
  return;
@@ -5236,14 +5651,26 @@ async function interactiveInput(prompt, defaultValue) {
5236
5651
  });
5237
5652
  }
5238
5653
  async function checkPromptsGptSiteReachable(apiUrl) {
5654
+ if (typeof globalThis.fetch !== "function") {
5655
+ return { ok: false, status: null };
5656
+ }
5239
5657
  const target = new URL("/", apiUrl).toString();
5240
- const request = (method) => Promise.race([
5241
- globalThis.fetch(target, { method }),
5242
- new Promise((_, rej) => {
5243
- const timer = setTimeout(() => rej(new Error("timeout")), 5000);
5244
- timer.unref?.();
5245
- }),
5246
- ]);
5658
+ const request = async (method) => {
5659
+ let timer = null;
5660
+ try {
5661
+ return await Promise.race([
5662
+ globalThis.fetch(target, { method }),
5663
+ new Promise((_, reject) => {
5664
+ timer = setTimeout(() => reject(new Error("timeout")), 5000);
5665
+ timer.unref?.();
5666
+ }),
5667
+ ]);
5668
+ }
5669
+ finally {
5670
+ if (timer)
5671
+ clearTimeout(timer);
5672
+ }
5673
+ };
5247
5674
  let response = await request("HEAD");
5248
5675
  if (response.status === 405) {
5249
5676
  response = await request("GET");
@@ -5284,6 +5711,48 @@ async function readSweepFrontmatter(filePath) {
5284
5711
  async function readSweepIterationsFromFrontmatter(filePath) {
5285
5712
  return (await readSweepFrontmatter(filePath)).iterations;
5286
5713
  }
5714
+ async function resolveSweepFileSelection(cwd, flags) {
5715
+ const allSweeps = Boolean(flags["all-sweeps"]);
5716
+ const rawSelected = getStringFlag(flags, "sweep-files");
5717
+ if (!allSweeps && !rawSelected)
5718
+ return [];
5719
+ const assets = await discoverWorkspaceAssets(cwd);
5720
+ if (assets.sweeps.length === 0) {
5721
+ throw new CliError("No sweep files found in .prompts-gpt/sweeps.", CLI_EXIT_CODES.validation, { helpCommand: "sweep" });
5722
+ }
5723
+ if (allSweeps) {
5724
+ return assets.sweeps.map((sweep) => sweep.file);
5725
+ }
5726
+ const requested = rawSelected
5727
+ .split(",")
5728
+ .map((item) => item.trim())
5729
+ .filter(Boolean);
5730
+ if (requested.length === 0) {
5731
+ throw new CliError("--sweep-files must include at least one file path, filename, or sweep name.", CLI_EXIT_CODES.validation, { helpCommand: "sweep" });
5732
+ }
5733
+ const resolved = [];
5734
+ const seen = new Set();
5735
+ for (const item of requested) {
5736
+ const directPath = path.resolve(cwd, item);
5737
+ let matched = existsSync(directPath) ? item : undefined;
5738
+ if (!matched) {
5739
+ const normalizedItem = item.replace(/\.md$/i, "");
5740
+ const asset = assets.sweeps.find((sweep) => {
5741
+ const basename = path.basename(sweep.file, ".md");
5742
+ return sweep.file === item || basename === item || basename === normalizedItem || sweep.name === item || sweep.name === normalizedItem;
5743
+ });
5744
+ matched = asset?.file;
5745
+ }
5746
+ if (!matched || !existsSync(path.resolve(cwd, matched))) {
5747
+ throw new CliError(`Sweep file not found: ${item}\n\nUse --sweep-files with paths, filenames, or names from \`prompts-gpt list\`.`, CLI_EXIT_CODES.validation, { helpCommand: "sweep" });
5748
+ }
5749
+ if (!seen.has(matched)) {
5750
+ seen.add(matched);
5751
+ resolved.push(matched);
5752
+ }
5753
+ }
5754
+ return resolved;
5755
+ }
5287
5756
  function buildGenerateInput(flags) {
5288
5757
  return {
5289
5758
  goal: getStringFlag(flags, "goal") || "",
@@ -5347,7 +5816,7 @@ function printCompletionScript(shell) {
5347
5816
  const commands = [...COMMANDS].sort().join(" ");
5348
5817
  const sweepFlags = "--prompt-file --agent --model --iterations --timeout --retries --json --verbose --dry-run --non-interactive --cwd --help --quiet --list-models";
5349
5818
  const runFlags = "--prompt-file --agent --model --timeout --verbose --dry-run --json --non-interactive --cwd --help --list-models";
5350
- const orchFlags = "--mode --prompt-file --providers --agent --model --timeout --evaluator --evaluator-model --criteria --eval-criteria --steps --dry-run --json --non-interactive --cwd --help --list-models";
5819
+ const orchFlags = "--mode --prompt-file --providers --agent --model --timeout --evaluator --evaluator-model --criteria --eval-criteria --steps --continue-on-error --dry-run --json --non-interactive --cwd --help --list-models";
5351
5820
  if (shell === "zsh") {
5352
5821
  console.log(`# Zsh completions for prompts-gpt
5353
5822
  # Add to ~/.zshrc: eval "$(prompts-gpt completions zsh)"
@@ -5455,19 +5924,29 @@ mode: "parallel"
5455
5924
  - Quality: Is the output production-ready?
5456
5925
  - Correctness: Are there any bugs or issues?
5457
5926
  `;
5458
- const PIPELINE_TEMPLATE = `[
5459
- {
5460
- "name": "analyze",
5461
- "promptFile": ".prompts-gpt/review.md",
5462
- "agent": "codex"
5463
- },
5464
- {
5465
- "name": "implement",
5466
- "promptFile": ".prompts-gpt/review.md",
5467
- "agent": "claude"
5468
- }
5469
- ]
5470
- `;
5927
+ function buildPipelineStepPrompt(stepName, description, stepIndex, totalSteps) {
5928
+ const isFirstStep = stepIndex === 0;
5929
+ const isLastStep = stepIndex === totalSteps - 1;
5930
+ const sections = [
5931
+ `You are the **{{ step_name }}** step ({{ step_index }}/{{ total_steps }}) of a multi-step pipeline.`,
5932
+ "",
5933
+ `## Goal`,
5934
+ "",
5935
+ description,
5936
+ "",
5937
+ ];
5938
+ if (isFirstStep) {
5939
+ sections.push("## Instructions", "", "1. Analyze the codebase structure and understand the existing patterns", "2. Identify the best approach for the task", "3. Produce a structured plan with specific file paths and changes", "", "## Output Format", "", "Provide a structured plan with:", "- Architecture decisions", "- Implementation checklist (numbered, with file paths)", "- Risk assessment", "- Existing patterns to follow", "");
5940
+ }
5941
+ else if (isLastStep) {
5942
+ sections.push("## Instructions", "", "1. Review the output from the previous step(s)", "2. Validate correctness and completeness", "3. Run any available tests: `npm run lint && npm run build`", "4. Fix any issues found", "", "## Output Format", "", "Provide:", "- Files reviewed (pass/fail per file)", "- Tests run and results", "- Issues found and fixed", "- Final status", "");
5943
+ }
5944
+ else {
5945
+ sections.push("## Instructions", "", "1. Read the plan/context from the previous step", "2. Implement each item systematically", "3. Follow existing patterns identified in the research", "4. Handle error states, edge cases, and validation", "", "## Output Format", "", "List every file created or modified with a one-line description.", "");
5946
+ }
5947
+ sections.push("{{ previous_output }}", "");
5948
+ return sections.join("\n");
5949
+ }
5471
5950
  main().catch((error) => {
5472
5951
  if (error instanceof CliError) {
5473
5952
  try {