@cleocode/cleo 2026.3.17 → 2026.3.19

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/mcp/index.js CHANGED
@@ -1809,6 +1809,16 @@ var init_registry = __esm({
1809
1809
  sessionRequired: false,
1810
1810
  requiredParams: ["version"]
1811
1811
  },
1812
+ {
1813
+ gateway: "query",
1814
+ domain: "pipeline",
1815
+ operation: "release.channel.show",
1816
+ description: "Show the current release channel based on git branch (latest/beta/alpha)",
1817
+ tier: 0,
1818
+ idempotent: true,
1819
+ sessionRequired: false,
1820
+ requiredParams: []
1821
+ },
1812
1822
  {
1813
1823
  gateway: "mutate",
1814
1824
  domain: "pipeline",
@@ -1869,6 +1879,16 @@ var init_registry = __esm({
1869
1879
  sessionRequired: false,
1870
1880
  requiredParams: []
1871
1881
  },
1882
+ {
1883
+ gateway: "mutate",
1884
+ domain: "pipeline",
1885
+ operation: "release.cancel",
1886
+ description: "pipeline.release.cancel (mutate)",
1887
+ tier: 0,
1888
+ idempotent: false,
1889
+ sessionRequired: false,
1890
+ requiredParams: ["version"]
1891
+ },
1872
1892
  {
1873
1893
  gateway: "mutate",
1874
1894
  domain: "pipeline",
@@ -19948,10 +19968,10 @@ async function systemLog(projectRoot, filters) {
19948
19968
  }
19949
19969
  async function queryAuditLogSqlite(projectRoot, filters) {
19950
19970
  try {
19951
- const { join: join63 } = await import("node:path");
19952
- const { existsSync: existsSync63 } = await import("node:fs");
19953
- const dbPath = join63(projectRoot, ".cleo", "tasks.db");
19954
- if (!existsSync63(dbPath)) {
19971
+ const { join: join64 } = await import("node:path");
19972
+ const { existsSync: existsSync64 } = await import("node:fs");
19973
+ const dbPath = join64(projectRoot, ".cleo", "tasks.db");
19974
+ if (!existsSync64(dbPath)) {
19955
19975
  const offset = filters?.offset ?? 0;
19956
19976
  const limit = filters?.limit ?? 20;
19957
19977
  return {
@@ -20695,10 +20715,10 @@ async function readProjectMeta(projectPath) {
20695
20715
  }
20696
20716
  async function readProjectId(projectPath) {
20697
20717
  try {
20698
- const { readFileSync: readFileSync45, existsSync: existsSync63 } = await import("node:fs");
20718
+ const { readFileSync: readFileSync46, existsSync: existsSync64 } = await import("node:fs");
20699
20719
  const infoPath = join34(projectPath, ".cleo", "project-info.json");
20700
- if (!existsSync63(infoPath)) return "";
20701
- const data = JSON.parse(readFileSync45(infoPath, "utf-8"));
20720
+ if (!existsSync64(infoPath)) return "";
20721
+ const data = JSON.parse(readFileSync46(infoPath, "utf-8"));
20702
20722
  return typeof data.projectId === "string" ? data.projectId : "";
20703
20723
  } catch {
20704
20724
  return "";
@@ -28490,7 +28510,8 @@ async function generateReleaseChangelog(version, loadTasksFn, cwd) {
28490
28510
  }
28491
28511
  function buildEntry(task) {
28492
28512
  const cleanTitle = capitalize(stripConventionalPrefix(task.title));
28493
- const desc6 = task.description?.trim();
28513
+ const safeDesc = task.description?.replace(/\r?\n/g, " ").replace(/\s{2,}/g, " ").trim();
28514
+ const desc6 = safeDesc;
28494
28515
  const shouldIncludeDesc = (() => {
28495
28516
  if (!desc6 || desc6.length === 0) return false;
28496
28517
  const titleNorm = cleanTitle.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim();
@@ -28507,23 +28528,29 @@ async function generateReleaseChangelog(version, loadTasksFn, cwd) {
28507
28528
  }
28508
28529
  function categorizeTask(task) {
28509
28530
  if (task.type === "epic") return "changes";
28510
- const labels = task.labels ?? [];
28511
- const titleLower = stripConventionalPrefix(task.title).toLowerCase();
28512
- const rawTitleLower = task.title.toLowerCase();
28531
+ const taskType = (task.type ?? "").toLowerCase();
28532
+ if (taskType === "test") return "tests";
28533
+ if (taskType === "fix" || taskType === "bugfix") return "fixes";
28534
+ if (taskType === "feat" || taskType === "feature") return "features";
28535
+ if (taskType === "docs" || taskType === "doc") return "docs";
28536
+ if (taskType === "chore" || taskType === "refactor") return "chores";
28513
28537
  if (/^feat(\([^)]+\))?:/.test(task.title.toLowerCase())) return "features";
28514
28538
  if (/^fix(\([^)]+\))?:/.test(task.title.toLowerCase())) return "fixes";
28515
28539
  if (/^docs?(\([^)]+\))?:/.test(task.title.toLowerCase())) return "docs";
28516
28540
  if (/^test(\([^)]+\))?:/.test(task.title.toLowerCase())) return "tests";
28517
28541
  if (/^(chore|refactor|style|ci|build|perf)(\([^)]+\))?:/.test(task.title.toLowerCase())) return "chores";
28518
- if (labels.some((l) => ["feat", "feature", "enhancement", "add"].includes(l.toLowerCase()))) return "features";
28542
+ const labels = task.labels ?? [];
28543
+ if (labels.some((l) => ["test", "testing"].includes(l.toLowerCase()))) return "tests";
28519
28544
  if (labels.some((l) => ["fix", "bug", "bugfix", "regression"].includes(l.toLowerCase()))) return "fixes";
28545
+ if (labels.some((l) => ["feat", "feature", "enhancement", "add"].includes(l.toLowerCase()))) return "features";
28520
28546
  if (labels.some((l) => ["docs", "documentation"].includes(l.toLowerCase()))) return "docs";
28521
- if (labels.some((l) => ["test", "testing"].includes(l.toLowerCase()))) return "tests";
28522
28547
  if (labels.some((l) => ["chore", "refactor", "cleanup", "maintenance"].includes(l.toLowerCase()))) return "chores";
28523
- if (titleLower.startsWith("add ") || titleLower.includes("implement") || titleLower.startsWith("create ") || titleLower.startsWith("introduce ")) return "features";
28548
+ const titleLower = stripConventionalPrefix(task.title).toLowerCase();
28549
+ const rawTitleLower = task.title.toLowerCase();
28550
+ if (titleLower.startsWith("test") || titleLower.includes("test") && titleLower.includes("add")) return "tests";
28524
28551
  if (titleLower.includes("bug") || titleLower.startsWith("fix") || titleLower.includes("regression") || titleLower.includes("broken")) return "fixes";
28552
+ if (titleLower.startsWith("add ") || titleLower.includes("implement") || titleLower.startsWith("create ") || titleLower.startsWith("introduce ")) return "features";
28525
28553
  if (titleLower.startsWith("doc") || titleLower.includes("documentation") || titleLower.includes("readme") || titleLower.includes("changelog")) return "docs";
28526
- if (titleLower.startsWith("test") || titleLower.includes("test") && titleLower.includes("add")) return "tests";
28527
28554
  if (titleLower.startsWith("chore") || titleLower.includes("refactor") || titleLower.includes("cleanup") || titleLower.includes("migrate") || titleLower.includes("upgrade") || titleLower.includes("remove ") || titleLower.startsWith("audit")) return "chores";
28528
28555
  if (rawTitleLower.startsWith("feat")) return "features";
28529
28556
  return "changes";
@@ -28534,6 +28561,10 @@ async function generateReleaseChangelog(version, loadTasksFn, cwd) {
28534
28561
  if (task.type === "epic") continue;
28535
28562
  if (task.labels?.some((l) => l.toLowerCase() === "epic")) continue;
28536
28563
  if (/^epic:/i.test(task.title.trim())) continue;
28564
+ const labelsLower = (task.labels ?? []).map((l) => l.toLowerCase());
28565
+ if (labelsLower.some((l) => ["research", "internal", "spike", "audit"].includes(l))) continue;
28566
+ if (["spike", "research"].includes((task.type ?? "").toLowerCase())) continue;
28567
+ if (/^(research|investigate|audit|spike)\s/i.test(task.title.trim())) continue;
28537
28568
  const category = categorizeTask(task);
28538
28569
  const entry = buildEntry(task);
28539
28570
  if (category === "features") features.push(entry);
@@ -28591,7 +28622,7 @@ async function generateReleaseChangelog(version, loadTasksFn, cwd) {
28591
28622
  }
28592
28623
  const { customBlocks } = parseChangelogBlocks(existingChangelogContent);
28593
28624
  const changelogBody = sections.slice(2).join("\n");
28594
- await writeChangelogSection(normalizedVersion, changelogBody, customBlocks, changelogPath);
28625
+ await writeChangelogSection(normalizedVersion.replace(/^v/, ""), changelogBody, customBlocks, changelogPath);
28595
28626
  return {
28596
28627
  version: normalizedVersion,
28597
28628
  changelog,
@@ -28664,7 +28695,7 @@ async function tagRelease(version, cwd) {
28664
28695
  await db.update(releaseManifests).set({ status: "tagged", taggedAt }).where(eq14(releaseManifests.version, normalizedVersion)).run();
28665
28696
  return { version: normalizedVersion, status: "tagged", taggedAt };
28666
28697
  }
28667
- async function runReleaseGates(version, loadTasksFn, cwd) {
28698
+ async function runReleaseGates(version, loadTasksFn, cwd, opts) {
28668
28699
  if (!version) {
28669
28700
  throw new Error("version is required");
28670
28701
  }
@@ -28712,23 +28743,31 @@ async function runReleaseGates(version, loadTasksFn, cwd) {
28712
28743
  message: existsSync50(distPath) ? "dist/cli/index.js present" : "dist/ not built \u2014 run: npm run build"
28713
28744
  });
28714
28745
  }
28715
- let workingTreeClean = true;
28716
- let dirtyFiles = [];
28717
- try {
28718
- const porcelain = execFileSync7("git", ["status", "--porcelain"], {
28719
- cwd: projectRoot,
28720
- encoding: "utf-8",
28721
- stdio: "pipe"
28746
+ if (opts?.dryRun) {
28747
+ gates.push({
28748
+ name: "clean_working_tree",
28749
+ status: "passed",
28750
+ message: "Skipped in dry-run mode"
28751
+ });
28752
+ } else {
28753
+ let workingTreeClean = true;
28754
+ let dirtyFiles = [];
28755
+ try {
28756
+ const porcelain = execFileSync7("git", ["status", "--porcelain"], {
28757
+ cwd: projectRoot,
28758
+ encoding: "utf-8",
28759
+ stdio: "pipe"
28760
+ });
28761
+ dirtyFiles = porcelain.split("\n").filter((l) => l.trim()).filter((l) => !l.startsWith("?? ")).map((l) => l.slice(3).trim()).filter((f) => f !== "CHANGELOG.md" && f !== "VERSION" && f !== "package.json");
28762
+ workingTreeClean = dirtyFiles.length === 0;
28763
+ } catch {
28764
+ }
28765
+ gates.push({
28766
+ name: "clean_working_tree",
28767
+ status: workingTreeClean ? "passed" : "failed",
28768
+ message: workingTreeClean ? "Working tree clean (excluding CHANGELOG.md, VERSION, package.json)" : `Uncommitted changes in: ${dirtyFiles.slice(0, 5).join(", ")}${dirtyFiles.length > 5 ? ` (+${dirtyFiles.length - 5} more)` : ""}`
28722
28769
  });
28723
- dirtyFiles = porcelain.split("\n").filter((l) => l.trim()).map((l) => l.slice(3).trim()).filter((f) => f !== "CHANGELOG.md" && f !== "VERSION" && f !== "package.json");
28724
- workingTreeClean = dirtyFiles.length === 0;
28725
- } catch {
28726
28770
  }
28727
- gates.push({
28728
- name: "clean_working_tree",
28729
- status: workingTreeClean ? "passed" : "failed",
28730
- message: workingTreeClean ? "Working tree clean (excluding CHANGELOG.md, VERSION, package.json)" : `Uncommitted changes in: ${dirtyFiles.slice(0, 5).join(", ")}${dirtyFiles.length > 5 ? ` (+${dirtyFiles.length - 5} more)` : ""}`
28731
- });
28732
28771
  const isPreRelease = normalizedVersion.includes("-");
28733
28772
  let currentBranch = "";
28734
28773
  try {
@@ -28788,6 +28827,28 @@ async function runReleaseGates(version, loadTasksFn, cwd) {
28788
28827
  metadata
28789
28828
  };
28790
28829
  }
28830
+ async function cancelRelease(version, projectRoot) {
28831
+ if (!version) {
28832
+ throw new Error("version is required");
28833
+ }
28834
+ const normalizedVersion = normalizeVersion(version);
28835
+ const db = await getDb(projectRoot);
28836
+ const rows = await db.select().from(releaseManifests).where(eq14(releaseManifests.version, normalizedVersion)).limit(1).all();
28837
+ if (rows.length === 0) {
28838
+ return { success: false, message: `Release ${normalizedVersion} not found`, version: normalizedVersion };
28839
+ }
28840
+ const status = rows[0].status;
28841
+ const cancellableStates = ["draft", "prepared"];
28842
+ if (!cancellableStates.includes(status)) {
28843
+ return {
28844
+ success: false,
28845
+ message: `Cannot cancel a release in '${status}' state. Use 'release rollback' instead.`,
28846
+ version: normalizedVersion
28847
+ };
28848
+ }
28849
+ await db.delete(releaseManifests).where(eq14(releaseManifests.version, normalizedVersion)).run();
28850
+ return { success: true, message: `Release ${normalizedVersion} cancelled and removed`, version: normalizedVersion };
28851
+ }
28791
28852
  async function rollbackRelease(version, reason, cwd) {
28792
28853
  if (!version) {
28793
28854
  throw new Error("version is required");
@@ -28862,7 +28923,8 @@ async function pushRelease(version, remote, cwd, opts) {
28862
28923
  encoding: "utf-8",
28863
28924
  stdio: ["pipe", "pipe", "pipe"]
28864
28925
  });
28865
- if (statusOutput.trim().length > 0) {
28926
+ const trackedDirty = statusOutput.split("\n").filter((l) => l.trim() && !l.startsWith("?? ")).join("\n");
28927
+ if (trackedDirty.trim().length > 0) {
28866
28928
  throw new Error(
28867
28929
  "Git working tree is not clean. Commit or stash changes before pushing (config: release.push.requireCleanTree=true)."
28868
28930
  );
@@ -28994,6 +29056,179 @@ var init_guards = __esm({
28994
29056
  }
28995
29057
  });
28996
29058
 
29059
+ // src/core/release/version-bump.ts
29060
+ import { existsSync as existsSync51, readFileSync as readFileSync39, writeFileSync as writeFileSync8 } from "node:fs";
29061
+ import { join as join49 } from "node:path";
29062
+ function readConfigValueSync2(path, defaultValue, cwd) {
29063
+ try {
29064
+ const configPath = join49(getCleoDir(cwd), "config.json");
29065
+ if (!existsSync51(configPath)) return defaultValue;
29066
+ const config = JSON.parse(readFileSync39(configPath, "utf-8"));
29067
+ const keys = path.split(".");
29068
+ let value = config;
29069
+ for (const key of keys) {
29070
+ if (value == null || typeof value !== "object") return defaultValue;
29071
+ value = value[key];
29072
+ }
29073
+ return value ?? defaultValue;
29074
+ } catch {
29075
+ return defaultValue;
29076
+ }
29077
+ }
29078
+ function validateVersionFormat(version) {
29079
+ return VERSION_WITH_PRERELEASE.test(version);
29080
+ }
29081
+ function getVersionBumpConfig(cwd) {
29082
+ try {
29083
+ const raw = readConfigValueSync2("release.versionBump.files", [], cwd);
29084
+ return raw.map((entry) => ({
29085
+ file: entry.path ?? entry.file ?? "",
29086
+ strategy: entry.strategy,
29087
+ field: entry.jsonPath?.replace(/^\./, "") ?? entry.field,
29088
+ key: entry.key,
29089
+ section: entry.section,
29090
+ pattern: entry.sedPattern ?? entry.pattern
29091
+ })).filter((t) => t.file !== "");
29092
+ } catch {
29093
+ return [];
29094
+ }
29095
+ }
29096
+ function bumpFile(target, newVersion, projectRoot) {
29097
+ const filePath = join49(projectRoot, target.file);
29098
+ if (!existsSync51(filePath)) {
29099
+ return {
29100
+ file: target.file,
29101
+ strategy: target.strategy,
29102
+ success: false,
29103
+ error: `File not found: ${target.file}`
29104
+ };
29105
+ }
29106
+ try {
29107
+ const content = readFileSync39(filePath, "utf-8");
29108
+ let previousVersion;
29109
+ let newContent;
29110
+ switch (target.strategy) {
29111
+ case "plain": {
29112
+ previousVersion = content.trim();
29113
+ newContent = newVersion + "\n";
29114
+ break;
29115
+ }
29116
+ case "json": {
29117
+ const field = target.field ?? "version";
29118
+ const json = JSON.parse(content);
29119
+ previousVersion = getNestedField(json, field);
29120
+ setNestedField(json, field, newVersion);
29121
+ newContent = JSON.stringify(json, null, 2) + "\n";
29122
+ break;
29123
+ }
29124
+ case "toml": {
29125
+ const key = target.key ?? "version";
29126
+ const versionRegex = new RegExp(`^(${key}\\s*=\\s*")([^"]+)(")`, "m");
29127
+ const match = content.match(versionRegex);
29128
+ previousVersion = match?.[2];
29129
+ newContent = content.replace(versionRegex, `$1${newVersion}$3`);
29130
+ break;
29131
+ }
29132
+ case "sed": {
29133
+ const pattern = target.pattern ?? "";
29134
+ if (!pattern.includes("{{VERSION}}")) {
29135
+ return {
29136
+ file: target.file,
29137
+ strategy: target.strategy,
29138
+ success: false,
29139
+ error: "sed strategy requires {{VERSION}} placeholder in pattern"
29140
+ };
29141
+ }
29142
+ const regex = new RegExp(pattern.replace("{{VERSION}}", "([\\d.]+)"));
29143
+ const match = content.match(regex);
29144
+ previousVersion = match?.[1];
29145
+ newContent = content.replace(
29146
+ regex,
29147
+ pattern.replace("{{VERSION}}", newVersion)
29148
+ );
29149
+ break;
29150
+ }
29151
+ default:
29152
+ return {
29153
+ file: target.file,
29154
+ strategy: target.strategy,
29155
+ success: false,
29156
+ error: `Unknown strategy: ${target.strategy}`
29157
+ };
29158
+ }
29159
+ writeFileSync8(filePath, newContent, "utf-8");
29160
+ return {
29161
+ file: target.file,
29162
+ strategy: target.strategy,
29163
+ success: true,
29164
+ previousVersion,
29165
+ newVersion
29166
+ };
29167
+ } catch (err) {
29168
+ return {
29169
+ file: target.file,
29170
+ strategy: target.strategy,
29171
+ success: false,
29172
+ error: String(err)
29173
+ };
29174
+ }
29175
+ }
29176
+ function bumpVersionFromConfig(newVersion, options = {}, cwd) {
29177
+ if (!validateVersionFormat(newVersion)) {
29178
+ throw new CleoError(
29179
+ 6 /* VALIDATION_ERROR */,
29180
+ `Invalid version: '${newVersion}' (expected X.Y.Z or YYYY.M.patch)`
29181
+ );
29182
+ }
29183
+ const targets = getVersionBumpConfig(cwd);
29184
+ if (targets.length === 0) {
29185
+ throw new CleoError(
29186
+ 1 /* GENERAL_ERROR */,
29187
+ "No version bump targets configured. Add release.versionBump.files to .cleo/config.json"
29188
+ );
29189
+ }
29190
+ const projectRoot = getProjectRoot(cwd);
29191
+ const results = [];
29192
+ if (options.dryRun) {
29193
+ for (const target of targets) {
29194
+ results.push({
29195
+ file: target.file,
29196
+ strategy: target.strategy,
29197
+ success: true,
29198
+ newVersion
29199
+ });
29200
+ }
29201
+ return { results, allSuccess: true };
29202
+ }
29203
+ for (const target of targets) {
29204
+ results.push(bumpFile(target, newVersion, projectRoot));
29205
+ }
29206
+ const allSuccess = results.every((r) => r.success);
29207
+ return { results, allSuccess };
29208
+ }
29209
+ function getNestedField(obj, path) {
29210
+ return path.split(".").reduce((acc, key) => acc?.[key], obj);
29211
+ }
29212
+ function setNestedField(obj, path, value) {
29213
+ const parts = path.split(".");
29214
+ let current = obj;
29215
+ for (let i = 0; i < parts.length - 1; i++) {
29216
+ if (typeof current[parts[i]] !== "object") current[parts[i]] = {};
29217
+ current = current[parts[i]];
29218
+ }
29219
+ current[parts[parts.length - 1]] = value;
29220
+ }
29221
+ var VERSION_WITH_PRERELEASE;
29222
+ var init_version_bump = __esm({
29223
+ "src/core/release/version-bump.ts"() {
29224
+ "use strict";
29225
+ init_paths();
29226
+ init_errors();
29227
+ init_exit_codes();
29228
+ VERSION_WITH_PRERELEASE = /^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/;
29229
+ }
29230
+ });
29231
+
28997
29232
  // src/dispatch/engines/release-engine.ts
28998
29233
  import { execFileSync as execFileSync8 } from "node:child_process";
28999
29234
  function isAgentContext() {
@@ -29116,6 +29351,23 @@ async function releaseRollback(version, reason, projectRoot) {
29116
29351
  return engineError(code, message);
29117
29352
  }
29118
29353
  }
29354
+ async function releaseCancel(version, projectRoot) {
29355
+ if (!version) {
29356
+ return engineError("E_INVALID_INPUT", "version is required");
29357
+ }
29358
+ try {
29359
+ const result = await cancelRelease(version, projectRoot);
29360
+ if (!result.success) {
29361
+ const code = result.message.includes("not found") ? "E_NOT_FOUND" : "E_INVALID_STATE";
29362
+ return engineError(code, result.message);
29363
+ }
29364
+ return { success: true, data: result };
29365
+ } catch (err) {
29366
+ const message = err.message;
29367
+ const code = message.includes("not found") ? "E_NOT_FOUND" : "E_CANCEL_FAILED";
29368
+ return engineError(code, message);
29369
+ }
29370
+ }
29119
29371
  async function releasePush(version, remote, projectRoot, opts) {
29120
29372
  if (isAgentContext()) {
29121
29373
  const hasEntry = await hasManifestEntry(version, projectRoot);
@@ -29159,7 +29411,7 @@ async function releasePush(version, remote, projectRoot, opts) {
29159
29411
  }
29160
29412
  }
29161
29413
  async function releaseShip(params, projectRoot) {
29162
- const { version, epicId, remote, dryRun = false } = params;
29414
+ const { version, epicId, remote, dryRun = false, bump = true } = params;
29163
29415
  if (!version) {
29164
29416
  return engineError("E_INVALID_INPUT", "version is required");
29165
29417
  }
@@ -29167,30 +29419,51 @@ async function releaseShip(params, projectRoot) {
29167
29419
  return engineError("E_INVALID_INPUT", "epicId is required");
29168
29420
  }
29169
29421
  const cwd = projectRoot ?? resolveProjectRoot();
29422
+ const steps = [];
29170
29423
  const logStep = (n, total, label, done, error) => {
29424
+ let msg;
29171
29425
  if (done === void 0) {
29172
- console.log(`[Step ${n}/${total}] ${label}...`);
29426
+ msg = `[Step ${n}/${total}] ${label}...`;
29173
29427
  } else if (done) {
29174
- console.log(` \u2713 ${label}`);
29428
+ msg = ` \u2713 ${label}`;
29175
29429
  } else {
29176
- console.log(` \u2717 ${label}: ${error ?? "failed"}`);
29430
+ msg = ` \u2717 ${label}: ${error ?? "failed"}`;
29177
29431
  }
29432
+ steps.push(msg);
29433
+ console.log(msg);
29178
29434
  };
29435
+ const bumpTargets = getVersionBumpConfig(cwd);
29436
+ const shouldBump = bump && bumpTargets.length > 0;
29179
29437
  try {
29180
- logStep(1, 7, "Validate release gates");
29438
+ if (shouldBump) {
29439
+ logStep(0, 8, "Bump version files");
29440
+ if (!dryRun) {
29441
+ const bumpResults = bumpVersionFromConfig(version, { dryRun: false }, cwd);
29442
+ if (!bumpResults.allSuccess) {
29443
+ const failed = bumpResults.results.filter((r) => !r.success).map((r) => r.file);
29444
+ steps.push(` ! Version bump partial: failed for ${failed.join(", ")}`);
29445
+ } else {
29446
+ logStep(0, 8, "Bump version files", true);
29447
+ }
29448
+ } else {
29449
+ logStep(0, 8, "Bump version files", true);
29450
+ }
29451
+ }
29452
+ logStep(1, 8, "Validate release gates");
29181
29453
  const gatesResult = await runReleaseGates(
29182
29454
  version,
29183
29455
  () => loadTasks2(projectRoot),
29184
- projectRoot
29456
+ projectRoot,
29457
+ { dryRun }
29185
29458
  );
29186
29459
  if (gatesResult && !gatesResult.allPassed) {
29187
29460
  const failedGates = gatesResult.gates.filter((g) => g.status === "failed");
29188
- logStep(1, 7, "Validate release gates", false, failedGates.map((g) => g.name).join(", "));
29461
+ logStep(1, 8, "Validate release gates", false, failedGates.map((g) => g.name).join(", "));
29189
29462
  return engineError("E_LIFECYCLE_GATE_FAILED", `Release gates failed for ${version}: ${failedGates.map((g) => g.name).join(", ")}`, {
29190
29463
  details: { gates: gatesResult.gates, failedCount: gatesResult.failedCount }
29191
29464
  });
29192
29465
  }
29193
- logStep(1, 7, "Validate release gates", true);
29466
+ logStep(1, 8, "Validate release gates", true);
29194
29467
  let resolvedChannel = "latest";
29195
29468
  let currentBranchForPR = "HEAD";
29196
29469
  try {
@@ -29210,7 +29483,7 @@ async function releaseShip(params, projectRoot) {
29210
29483
  if (gateMetadata?.currentBranch) {
29211
29484
  currentBranchForPR = gateMetadata.currentBranch;
29212
29485
  }
29213
- logStep(2, 7, "Check epic completeness");
29486
+ logStep(2, 8, "Check epic completeness");
29214
29487
  let releaseTaskIds = [];
29215
29488
  try {
29216
29489
  const manifest = await showManifestRelease(version, projectRoot);
@@ -29221,13 +29494,13 @@ async function releaseShip(params, projectRoot) {
29221
29494
  const epicCheck = await checkEpicCompleteness(releaseTaskIds, projectRoot, epicAccessor);
29222
29495
  if (epicCheck.hasIncomplete) {
29223
29496
  const incomplete = epicCheck.epics.filter((e) => e.missingChildren.length > 0).map((e) => `${e.epicId}: missing ${e.missingChildren.map((c) => c.id).join(", ")}`).join("; ");
29224
- logStep(2, 7, "Check epic completeness", false, incomplete);
29497
+ logStep(2, 8, "Check epic completeness", false, incomplete);
29225
29498
  return engineError("E_LIFECYCLE_GATE_FAILED", `Epic completeness check failed: ${incomplete}`, {
29226
29499
  details: { epics: epicCheck.epics }
29227
29500
  });
29228
29501
  }
29229
- logStep(2, 7, "Check epic completeness", true);
29230
- logStep(3, 7, "Check task double-listing");
29502
+ logStep(2, 8, "Check epic completeness", true);
29503
+ logStep(3, 8, "Check task double-listing");
29231
29504
  const allReleases = await listManifestReleases(projectRoot);
29232
29505
  const existingReleases = (allReleases.releases ?? []).filter((r) => r.version !== version);
29233
29506
  const doubleCheck = checkDoubleListing(
@@ -29236,39 +29509,38 @@ async function releaseShip(params, projectRoot) {
29236
29509
  );
29237
29510
  if (doubleCheck.hasDoubleListing) {
29238
29511
  const dupes = doubleCheck.duplicates.map((d) => `${d.taskId} (in ${d.releases.join(", ")})`).join("; ");
29239
- logStep(3, 7, "Check task double-listing", false, dupes);
29512
+ logStep(3, 8, "Check task double-listing", false, dupes);
29240
29513
  return engineError("E_VALIDATION", `Double-listing detected: ${dupes}`, {
29241
29514
  details: { duplicates: doubleCheck.duplicates }
29242
29515
  });
29243
29516
  }
29244
- logStep(3, 7, "Check task double-listing", true);
29245
- logStep(4, 7, "Generate CHANGELOG");
29246
- const changelogResult = await generateReleaseChangelog(
29247
- version,
29248
- () => loadTasks2(projectRoot),
29249
- projectRoot
29250
- );
29251
- const changelogPath = `${cwd}/CHANGELOG.md`;
29252
- const generatedContent = changelogResult.changelog ?? "";
29253
- logStep(4, 7, "Generate CHANGELOG", true);
29517
+ logStep(3, 8, "Check task double-listing", true);
29254
29518
  const loadedConfig = loadReleaseConfig(cwd);
29255
29519
  const pushMode = getPushMode(loadedConfig);
29256
29520
  const gitflowCfg = getGitFlowConfig(loadedConfig);
29257
29521
  const targetBranch = targetBranchFromGates ?? gitflowCfg.branches.main;
29258
29522
  if (dryRun) {
29523
+ logStep(4, 8, "Generate CHANGELOG");
29524
+ logStep(4, 8, "Generate CHANGELOG", true);
29259
29525
  const wouldCreatePR = requiresPRFromGates || pushMode === "pr";
29526
+ const filesToStagePreview = ["CHANGELOG.md", ...shouldBump ? bumpTargets.map((t) => t.file) : []];
29527
+ const wouldDo = [];
29528
+ if (shouldBump) {
29529
+ wouldDo.push(`bump version files: ${bumpTargets.map((t) => t.file).join(", ")} \u2192 ${version}`);
29530
+ }
29531
+ wouldDo.push(
29532
+ `write CHANGELOG.md: ## [${version}] - ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]} (preview only, not written in dry-run)`,
29533
+ `git add ${filesToStagePreview.join(" ")}`,
29534
+ `git commit -m "release: ship v${version} (${epicId})"`,
29535
+ `git tag -a v${version} -m "Release v${version}"`
29536
+ );
29260
29537
  const dryRunOutput = {
29261
29538
  version,
29262
29539
  epicId,
29263
29540
  dryRun: true,
29264
29541
  channel: resolvedChannel,
29265
29542
  pushMode,
29266
- wouldDo: [
29267
- `write CHANGELOG section for ${version} (${generatedContent.length} chars)`,
29268
- "git add CHANGELOG.md",
29269
- `git commit -m "release: ship v${version} (${epicId})"`,
29270
- `git tag -a v${version} -m "Release v${version}"`
29271
- ]
29543
+ wouldDo
29272
29544
  };
29273
29545
  if (wouldCreatePR) {
29274
29546
  const ghAvailable = isGhCliAvailable();
@@ -29285,15 +29557,24 @@ async function releaseShip(params, projectRoot) {
29285
29557
  dryRunOutput["wouldCreatePR"] = false;
29286
29558
  }
29287
29559
  dryRunOutput["wouldDo"].push("markReleasePushed(...)");
29288
- return { success: true, data: dryRunOutput };
29560
+ return { success: true, data: { ...dryRunOutput, steps } };
29289
29561
  }
29290
- logStep(5, 7, "Commit release");
29562
+ logStep(4, 8, "Generate CHANGELOG");
29563
+ await generateReleaseChangelog(
29564
+ version,
29565
+ () => loadTasks2(projectRoot),
29566
+ projectRoot
29567
+ );
29568
+ const changelogPath = `${cwd}/CHANGELOG.md`;
29569
+ logStep(4, 8, "Generate CHANGELOG", true);
29570
+ logStep(5, 8, "Commit release");
29291
29571
  const gitCwd = { cwd, encoding: "utf-8", stdio: "pipe" };
29572
+ const filesToStage = ["CHANGELOG.md", ...shouldBump ? bumpTargets.map((t) => t.file) : []];
29292
29573
  try {
29293
- execFileSync8("git", ["add", "CHANGELOG.md"], gitCwd);
29574
+ execFileSync8("git", ["add", ...filesToStage], gitCwd);
29294
29575
  } catch (err) {
29295
29576
  const msg = err.message ?? String(err);
29296
- logStep(5, 7, "Commit release", false, `git add failed: ${msg}`);
29577
+ logStep(5, 8, "Commit release", false, `git add failed: ${msg}`);
29297
29578
  return engineError("E_GENERAL", `git add failed: ${msg}`);
29298
29579
  }
29299
29580
  try {
@@ -29304,26 +29585,26 @@ async function releaseShip(params, projectRoot) {
29304
29585
  );
29305
29586
  } catch (err) {
29306
29587
  const msg = err.stderr ?? err.message ?? String(err);
29307
- logStep(5, 7, "Commit release", false, `git commit failed: ${msg}`);
29588
+ logStep(5, 8, "Commit release", false, `git commit failed: ${msg}`);
29308
29589
  return engineError("E_GENERAL", `git commit failed: ${msg}`);
29309
29590
  }
29310
- logStep(5, 7, "Commit release", true);
29591
+ logStep(5, 8, "Commit release", true);
29311
29592
  let commitSha;
29312
29593
  try {
29313
29594
  commitSha = execFileSync8("git", ["rev-parse", "HEAD"], gitCwd).toString().trim();
29314
29595
  } catch {
29315
29596
  }
29316
- logStep(6, 7, "Tag release");
29597
+ logStep(6, 8, "Tag release");
29317
29598
  const gitTag = `v${version.replace(/^v/, "")}`;
29318
29599
  try {
29319
29600
  execFileSync8("git", ["tag", "-a", gitTag, "-m", `Release ${gitTag}`], gitCwd);
29320
29601
  } catch (err) {
29321
29602
  const msg = err.stderr ?? err.message ?? String(err);
29322
- logStep(6, 7, "Tag release", false, `git tag failed: ${msg}`);
29603
+ logStep(6, 8, "Tag release", false, `git tag failed: ${msg}`);
29323
29604
  return engineError("E_GENERAL", `git tag failed: ${msg}`);
29324
29605
  }
29325
- logStep(6, 7, "Tag release", true);
29326
- logStep(7, 7, "Push / create PR");
29606
+ logStep(6, 8, "Tag release", true);
29607
+ logStep(7, 8, "Push / create PR");
29327
29608
  let prResult = null;
29328
29609
  const pushResult = await pushRelease(version, remote, projectRoot, {
29329
29610
  explicitPush: true,
@@ -29350,24 +29631,34 @@ async function releaseShip(params, projectRoot) {
29350
29631
  projectRoot: cwd
29351
29632
  });
29352
29633
  if (prResult.mode === "created") {
29353
- console.log(` \u2713 Push / create PR`);
29354
- console.log(` PR created: ${prResult.prUrl}`);
29355
- console.log(` \u2192 Next: merge the PR, then CI will publish to npm @${resolvedChannel}`);
29634
+ const m1 = ` \u2713 Push / create PR`;
29635
+ const m2 = ` PR created: ${prResult.prUrl}`;
29636
+ const m3 = ` \u2192 Next: merge the PR, then CI will publish to npm @${resolvedChannel}`;
29637
+ steps.push(m1, m2, m3);
29638
+ console.log(m1);
29639
+ console.log(m2);
29640
+ console.log(m3);
29356
29641
  } else if (prResult.mode === "skipped") {
29357
- console.log(` \u2713 Push / create PR`);
29358
- console.log(` PR already exists: ${prResult.prUrl}`);
29642
+ const m1 = ` \u2713 Push / create PR`;
29643
+ const m2 = ` PR already exists: ${prResult.prUrl}`;
29644
+ steps.push(m1, m2);
29645
+ console.log(m1);
29646
+ console.log(m2);
29359
29647
  } else {
29360
- console.log(` ! Push / create PR \u2014 manual PR required:`);
29361
- console.log(prResult.instructions);
29648
+ const m1 = ` ! Push / create PR \u2014 manual PR required:`;
29649
+ const m2 = prResult.instructions ?? "";
29650
+ steps.push(m1, m2);
29651
+ console.log(m1);
29652
+ console.log(m2);
29362
29653
  }
29363
29654
  } else {
29364
29655
  try {
29365
29656
  execFileSync8("git", ["push", remote ?? "origin", "--follow-tags"], gitCwd);
29366
- logStep(7, 7, "Push / create PR", true);
29657
+ logStep(7, 8, "Push / create PR", true);
29367
29658
  } catch (err) {
29368
29659
  const execError = err;
29369
29660
  const msg = (execError.stderr ?? execError.message ?? "").slice(0, 500);
29370
- logStep(7, 7, "Push / create PR", false, `git push failed: ${msg}`);
29661
+ logStep(7, 8, "Push / create PR", false, `git push failed: ${msg}`);
29371
29662
  return engineError("E_GENERAL", `git push failed: ${msg}`, {
29372
29663
  details: { exitCode: execError.status }
29373
29664
  });
@@ -29385,6 +29676,7 @@ async function releaseShip(params, projectRoot) {
29385
29676
  pushedAt,
29386
29677
  changelog: changelogPath,
29387
29678
  channel: resolvedChannel,
29679
+ steps,
29388
29680
  ...prResult ? {
29389
29681
  pr: {
29390
29682
  mode: prResult.mode,
@@ -29409,13 +29701,14 @@ var init_release_engine = __esm({
29409
29701
  init_github_pr();
29410
29702
  init_channel();
29411
29703
  init_release_config();
29704
+ init_version_bump();
29412
29705
  init_error();
29413
29706
  }
29414
29707
  });
29415
29708
 
29416
29709
  // src/dispatch/engines/template-parser.ts
29417
- import { readFileSync as readFileSync39, readdirSync as readdirSync12, existsSync as existsSync51 } from "fs";
29418
- import { join as join49 } from "path";
29710
+ import { readFileSync as readFileSync40, readdirSync as readdirSync12, existsSync as existsSync52 } from "fs";
29711
+ import { join as join50 } from "path";
29419
29712
  import { parse as parseYaml } from "yaml";
29420
29713
  function deriveSubcommand(filename) {
29421
29714
  let stem = filename.replace(/\.ya?ml$/i, "");
@@ -29429,8 +29722,8 @@ function deriveSubcommand(filename) {
29429
29722
  return firstWord.toLowerCase();
29430
29723
  }
29431
29724
  function parseTemplateFile(templateDir, filename) {
29432
- const filePath = join49(templateDir, filename);
29433
- const raw = readFileSync39(filePath, "utf-8");
29725
+ const filePath = join50(templateDir, filename);
29726
+ const raw = readFileSync40(filePath, "utf-8");
29434
29727
  const parsed = parseYaml(raw);
29435
29728
  const name = typeof parsed.name === "string" ? parsed.name : filename;
29436
29729
  const titlePrefix = typeof parsed.title === "string" ? parsed.title : "";
@@ -29479,8 +29772,8 @@ function parseTemplateFile(templateDir, filename) {
29479
29772
  };
29480
29773
  }
29481
29774
  function parseIssueTemplates(projectRoot) {
29482
- const templateDir = join49(projectRoot, ".github", "ISSUE_TEMPLATE");
29483
- if (!existsSync51(templateDir)) {
29775
+ const templateDir = join50(projectRoot, ".github", "ISSUE_TEMPLATE");
29776
+ if (!existsSync52(templateDir)) {
29484
29777
  return engineError("E_NOT_FOUND", `Issue template directory not found: ${templateDir}`);
29485
29778
  }
29486
29779
  let files;
@@ -31159,26 +31452,26 @@ var init_check = __esm({
31159
31452
  });
31160
31453
 
31161
31454
  // src/core/adrs/validate.ts
31162
- import { readFileSync as readFileSync40, readdirSync as readdirSync13, existsSync as existsSync52 } from "node:fs";
31163
- import { join as join50 } from "node:path";
31455
+ import { readFileSync as readFileSync41, readdirSync as readdirSync13, existsSync as existsSync53 } from "node:fs";
31456
+ import { join as join51 } from "node:path";
31164
31457
  import AjvModule3 from "ajv";
31165
31458
  async function validateAllAdrs(projectRoot) {
31166
- const adrsDir = join50(projectRoot, ".cleo", "adrs");
31167
- const schemaPath = join50(projectRoot, "schemas", "adr-frontmatter.schema.json");
31168
- if (!existsSync52(schemaPath)) {
31459
+ const adrsDir = join51(projectRoot, ".cleo", "adrs");
31460
+ const schemaPath = join51(projectRoot, "schemas", "adr-frontmatter.schema.json");
31461
+ if (!existsSync53(schemaPath)) {
31169
31462
  return {
31170
31463
  valid: false,
31171
31464
  errors: [{ file: "schemas/adr-frontmatter.schema.json", field: "schema", message: "Schema file not found" }],
31172
31465
  checked: 0
31173
31466
  };
31174
31467
  }
31175
- if (!existsSync52(adrsDir)) {
31468
+ if (!existsSync53(adrsDir)) {
31176
31469
  return { valid: true, errors: [], checked: 0 };
31177
31470
  }
31178
- const schema = JSON.parse(readFileSync40(schemaPath, "utf-8"));
31471
+ const schema = JSON.parse(readFileSync41(schemaPath, "utf-8"));
31179
31472
  const ajv = new Ajv3({ allErrors: true });
31180
31473
  const validate = ajv.compile(schema);
31181
- const files = readdirSync13(adrsDir).filter((f) => f.endsWith(".md") && f.startsWith("ADR-")).map((f) => join50(adrsDir, f));
31474
+ const files = readdirSync13(adrsDir).filter((f) => f.endsWith(".md") && f.startsWith("ADR-")).map((f) => join51(adrsDir, f));
31182
31475
  const errors = [];
31183
31476
  for (const filePath of files) {
31184
31477
  const record = parseAdrFile(filePath, projectRoot);
@@ -31205,15 +31498,15 @@ var init_validate = __esm({
31205
31498
  });
31206
31499
 
31207
31500
  // src/core/adrs/list.ts
31208
- import { readdirSync as readdirSync14, existsSync as existsSync53 } from "node:fs";
31209
- import { join as join51 } from "node:path";
31501
+ import { readdirSync as readdirSync14, existsSync as existsSync54 } from "node:fs";
31502
+ import { join as join52 } from "node:path";
31210
31503
  async function listAdrs(projectRoot, opts) {
31211
- const adrsDir = join51(projectRoot, ".cleo", "adrs");
31212
- if (!existsSync53(adrsDir)) {
31504
+ const adrsDir = join52(projectRoot, ".cleo", "adrs");
31505
+ if (!existsSync54(adrsDir)) {
31213
31506
  return { adrs: [], total: 0 };
31214
31507
  }
31215
31508
  const files = readdirSync14(adrsDir).filter((f) => f.endsWith(".md") && f.startsWith("ADR-")).sort();
31216
- const records = files.map((f) => parseAdrFile(join51(adrsDir, f), projectRoot));
31509
+ const records = files.map((f) => parseAdrFile(join52(adrsDir, f), projectRoot));
31217
31510
  const filtered = records.filter((r) => {
31218
31511
  if (opts?.status && r.frontmatter.Status !== opts.status) return false;
31219
31512
  if (opts?.since && r.frontmatter.Date < opts.since) return false;
@@ -31238,14 +31531,14 @@ var init_list2 = __esm({
31238
31531
  });
31239
31532
 
31240
31533
  // src/core/adrs/show.ts
31241
- import { existsSync as existsSync54, readdirSync as readdirSync15 } from "node:fs";
31242
- import { join as join52 } from "node:path";
31534
+ import { existsSync as existsSync55, readdirSync as readdirSync15 } from "node:fs";
31535
+ import { join as join53 } from "node:path";
31243
31536
  async function showAdr(projectRoot, adrId) {
31244
- const adrsDir = join52(projectRoot, ".cleo", "adrs");
31245
- if (!existsSync54(adrsDir)) return null;
31537
+ const adrsDir = join53(projectRoot, ".cleo", "adrs");
31538
+ if (!existsSync55(adrsDir)) return null;
31246
31539
  const files = readdirSync15(adrsDir).filter((f) => f.startsWith(adrId) && f.endsWith(".md"));
31247
31540
  if (files.length === 0) return null;
31248
- const filePath = join52(adrsDir, files[0]);
31541
+ const filePath = join53(adrsDir, files[0]);
31249
31542
  return parseAdrFile(filePath, projectRoot);
31250
31543
  }
31251
31544
  var init_show2 = __esm({
@@ -31256,8 +31549,8 @@ var init_show2 = __esm({
31256
31549
  });
31257
31550
 
31258
31551
  // src/core/adrs/find.ts
31259
- import { readdirSync as readdirSync16, existsSync as existsSync55 } from "node:fs";
31260
- import { join as join53 } from "node:path";
31552
+ import { readdirSync as readdirSync16, existsSync as existsSync56 } from "node:fs";
31553
+ import { join as join54 } from "node:path";
31261
31554
  function normalise(s) {
31262
31555
  return s.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
31263
31556
  }
@@ -31274,8 +31567,8 @@ function matchedTerms(target, terms) {
31274
31567
  return terms.filter((term) => t.includes(term));
31275
31568
  }
31276
31569
  async function findAdrs(projectRoot, query, opts) {
31277
- const adrsDir = join53(projectRoot, ".cleo", "adrs");
31278
- if (!existsSync55(adrsDir)) {
31570
+ const adrsDir = join54(projectRoot, ".cleo", "adrs");
31571
+ if (!existsSync56(adrsDir)) {
31279
31572
  return { adrs: [], query, total: 0 };
31280
31573
  }
31281
31574
  const files = readdirSync16(adrsDir).filter((f) => f.endsWith(".md") && f.startsWith("ADR-")).sort();
@@ -31284,7 +31577,7 @@ async function findAdrs(projectRoot, query, opts) {
31284
31577
  const filterKeywords = opts?.keywords ? parseTags(opts.keywords) : null;
31285
31578
  const results = [];
31286
31579
  for (const file of files) {
31287
- const record = parseAdrFile(join53(adrsDir, file), projectRoot);
31580
+ const record = parseAdrFile(join54(adrsDir, file), projectRoot);
31288
31581
  const fm = record.frontmatter;
31289
31582
  if (opts?.status && fm.Status !== opts.status) continue;
31290
31583
  if (filterTopics && filterTopics.length > 0) {
@@ -31362,12 +31655,12 @@ var init_adrs = __esm({
31362
31655
  });
31363
31656
 
31364
31657
  // src/core/admin/sync.ts
31365
- import { join as join54 } from "node:path";
31658
+ import { join as join55 } from "node:path";
31366
31659
  import { rm as rm2, rmdir, stat as stat2 } from "node:fs/promises";
31367
31660
  async function getSyncStatus(projectRoot) {
31368
31661
  try {
31369
31662
  const cleoDir = getCleoDir(projectRoot);
31370
- const stateFile = join54(cleoDir, "sync", "todowrite-session.json");
31663
+ const stateFile = join55(cleoDir, "sync", "todowrite-session.json");
31371
31664
  const sessionState = await readJson(stateFile);
31372
31665
  if (!sessionState) {
31373
31666
  return {
@@ -31411,8 +31704,8 @@ async function getSyncStatus(projectRoot) {
31411
31704
  async function clearSyncState(projectRoot, dryRun) {
31412
31705
  try {
31413
31706
  const cleoDir = getCleoDir(projectRoot);
31414
- const syncDir = join54(cleoDir, "sync");
31415
- const stateFile = join54(syncDir, "todowrite-session.json");
31707
+ const syncDir = join55(cleoDir, "sync");
31708
+ const stateFile = join55(syncDir, "todowrite-session.json");
31416
31709
  let exists = false;
31417
31710
  try {
31418
31711
  await stat2(stateFile);
@@ -32179,8 +32472,8 @@ var init_import_tasks = __esm({
32179
32472
  // src/core/snapshot/index.ts
32180
32473
  import { createHash as createHash8 } from "node:crypto";
32181
32474
  import { readFile as readFile13, writeFile as writeFile10, mkdir as mkdir10 } from "node:fs/promises";
32182
- import { existsSync as existsSync56 } from "node:fs";
32183
- import { join as join55, dirname as dirname14 } from "node:path";
32475
+ import { existsSync as existsSync57 } from "node:fs";
32476
+ import { join as join56, dirname as dirname14 } from "node:path";
32184
32477
  function toSnapshotTask(task) {
32185
32478
  return {
32186
32479
  id: task.id,
@@ -32233,7 +32526,7 @@ async function exportSnapshot(cwd) {
32233
32526
  }
32234
32527
  async function writeSnapshot(snapshot, outputPath) {
32235
32528
  const dir = dirname14(outputPath);
32236
- if (!existsSync56(dir)) {
32529
+ if (!existsSync57(dir)) {
32237
32530
  await mkdir10(dir, { recursive: true });
32238
32531
  }
32239
32532
  await writeFile10(outputPath, JSON.stringify(snapshot, null, 2) + "\n");
@@ -32249,7 +32542,7 @@ async function readSnapshot(inputPath) {
32249
32542
  function getDefaultSnapshotPath(cwd) {
32250
32543
  const cleoDir = getCleoDirAbsolute(cwd);
32251
32544
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
32252
- return join55(cleoDir, "snapshots", `snapshot-${timestamp}.json`);
32545
+ return join56(cleoDir, "snapshots", `snapshot-${timestamp}.json`);
32253
32546
  }
32254
32547
  async function importSnapshot(snapshot, cwd) {
32255
32548
  const accessor = await getAccessor(cwd);
@@ -32692,15 +32985,15 @@ var init_field_filter = __esm({
32692
32985
  });
32693
32986
 
32694
32987
  // src/core/project-info.ts
32695
- import { readFileSync as readFileSync41, existsSync as existsSync57 } from "node:fs";
32696
- import { join as join56 } from "node:path";
32988
+ import { readFileSync as readFileSync42, existsSync as existsSync58 } from "node:fs";
32989
+ import { join as join57 } from "node:path";
32697
32990
  function getProjectInfoSync(cwd) {
32698
32991
  const projectRoot = cwd ?? process.cwd();
32699
32992
  const cleoDir = getCleoDirAbsolute(projectRoot);
32700
- const infoPath = join56(cleoDir, "project-info.json");
32701
- if (!existsSync57(infoPath)) return null;
32993
+ const infoPath = join57(cleoDir, "project-info.json");
32994
+ if (!existsSync58(infoPath)) return null;
32702
32995
  try {
32703
- const raw = readFileSync41(infoPath, "utf-8");
32996
+ const raw = readFileSync42(infoPath, "utf-8");
32704
32997
  const data = JSON.parse(raw);
32705
32998
  if (typeof data.projectHash !== "string" || data.projectHash.length === 0) {
32706
32999
  return null;
@@ -32928,13 +33221,13 @@ var init_field_context = __esm({
32928
33221
  });
32929
33222
 
32930
33223
  // src/core/sessions/context-alert.ts
32931
- import { existsSync as existsSync58, readFileSync as readFileSync42, writeFileSync as writeFileSync8 } from "node:fs";
32932
- import { join as join57 } from "node:path";
33224
+ import { existsSync as existsSync59, readFileSync as readFileSync43, writeFileSync as writeFileSync9 } from "node:fs";
33225
+ import { join as join58 } from "node:path";
32933
33226
  function getCurrentSessionId(cwd) {
32934
33227
  if (process.env.CLEO_SESSION) return process.env.CLEO_SESSION;
32935
- const sessionFile = join57(getCleoDir(cwd), ".current-session");
32936
- if (existsSync58(sessionFile)) {
32937
- return readFileSync42(sessionFile, "utf-8").trim() || null;
33228
+ const sessionFile = join58(getCleoDir(cwd), ".current-session");
33229
+ if (existsSync59(sessionFile)) {
33230
+ return readFileSync43(sessionFile, "utf-8").trim() || null;
32938
33231
  }
32939
33232
  return null;
32940
33233
  }
@@ -33982,7 +34275,7 @@ async function stopTask2(sessionId, cwd) {
33982
34275
  }
33983
34276
  async function workHistory(sessionId, limit = 50, cwd) {
33984
34277
  const db = await getDb(cwd);
33985
- const rows = await db.select().from(taskWorkHistory).where(eq16(taskWorkHistory.sessionId, sessionId)).orderBy(desc5(taskWorkHistory.setAt)).limit(limit).all();
34278
+ const rows = await db.select().from(taskWorkHistory).where(eq16(taskWorkHistory.sessionId, sessionId)).orderBy(desc5(taskWorkHistory.setAt), desc5(taskWorkHistory.id)).limit(limit).all();
33986
34279
  return rows.map((r) => ({
33987
34280
  taskId: r.taskId,
33988
34281
  setAt: r.setAt,
@@ -34335,8 +34628,8 @@ __export(session_grade_exports, {
34335
34628
  gradeSession: () => gradeSession,
34336
34629
  readGrades: () => readGrades
34337
34630
  });
34338
- import { join as join58 } from "node:path";
34339
- import { existsSync as existsSync59 } from "node:fs";
34631
+ import { join as join59 } from "node:path";
34632
+ import { existsSync as existsSync60 } from "node:fs";
34340
34633
  import { readFile as readFile14, appendFile, mkdir as mkdir11 } from "node:fs/promises";
34341
34634
  async function gradeSession(sessionId, cwd) {
34342
34635
  const sessionEntries = await queryAudit({ sessionId });
@@ -34516,9 +34809,9 @@ function detectDuplicateCreates(entries) {
34516
34809
  async function appendGradeResult(result, cwd) {
34517
34810
  try {
34518
34811
  const cleoDir = getCleoDirAbsolute(cwd);
34519
- const metricsDir = join58(cleoDir, "metrics");
34812
+ const metricsDir = join59(cleoDir, "metrics");
34520
34813
  await mkdir11(metricsDir, { recursive: true });
34521
- const gradesPath = join58(metricsDir, "GRADES.jsonl");
34814
+ const gradesPath = join59(metricsDir, "GRADES.jsonl");
34522
34815
  const line = JSON.stringify({ ...result, evaluator: "auto" }) + "\n";
34523
34816
  await appendFile(gradesPath, line, "utf8");
34524
34817
  } catch {
@@ -34527,8 +34820,8 @@ async function appendGradeResult(result, cwd) {
34527
34820
  async function readGrades(sessionId, cwd) {
34528
34821
  try {
34529
34822
  const cleoDir = getCleoDirAbsolute(cwd);
34530
- const gradesPath = join58(cleoDir, "metrics", "GRADES.jsonl");
34531
- if (!existsSync59(gradesPath)) return [];
34823
+ const gradesPath = join59(cleoDir, "metrics", "GRADES.jsonl");
34824
+ if (!existsSync60(gradesPath)) return [];
34532
34825
  const content = await readFile14(gradesPath, "utf8");
34533
34826
  const results = content.split("\n").filter((l) => l.trim()).map((l) => JSON.parse(l));
34534
34827
  return sessionId ? results.filter((r) => r.sessionId === sessionId) : results;
@@ -36882,6 +37175,7 @@ var init_pipeline2 = __esm({
36882
37175
  "release.push",
36883
37176
  "release.gates.run",
36884
37177
  "release.rollback",
37178
+ "release.cancel",
36885
37179
  "release.ship",
36886
37180
  "manifest.append",
36887
37181
  "manifest.archive",
@@ -37246,6 +37540,20 @@ var init_pipeline2 = __esm({
37246
37540
  const result = await releaseRollback(version, reason, this.projectRoot);
37247
37541
  return this.wrapEngineResult(result, "mutate", "release.rollback", startTime);
37248
37542
  }
37543
+ case "cancel": {
37544
+ const version = params?.version;
37545
+ if (!version) {
37546
+ return this.errorResponse(
37547
+ "mutate",
37548
+ "release.cancel",
37549
+ "E_INVALID_INPUT",
37550
+ "version is required",
37551
+ startTime
37552
+ );
37553
+ }
37554
+ const result = await releaseCancel(version, this.projectRoot);
37555
+ return this.wrapEngineResult(result, "mutate", "release.cancel", startTime);
37556
+ }
37249
37557
  case "ship": {
37250
37558
  const version = params?.version;
37251
37559
  const epicId = params?.epicId;
@@ -37260,8 +37568,9 @@ var init_pipeline2 = __esm({
37260
37568
  }
37261
37569
  const remote = params?.remote;
37262
37570
  const dryRun = params?.dryRun;
37571
+ const bump = params?.bump;
37263
37572
  const result = await releaseShip(
37264
- { version, epicId, remote, dryRun },
37573
+ { version, epicId, remote, dryRun, bump },
37265
37574
  this.projectRoot
37266
37575
  );
37267
37576
  return this.wrapEngineResult(result, "mutate", "release.ship", startTime);
@@ -37787,7 +38096,7 @@ var init_build_config = __esm({
37787
38096
  "use strict";
37788
38097
  BUILD_CONFIG = {
37789
38098
  "name": "@cleocode/cleo",
37790
- "version": "2026.3.17",
38099
+ "version": "2026.3.19",
37791
38100
  "description": "CLEO V2 - TypeScript task management CLI for AI coding agents",
37792
38101
  "repository": {
37793
38102
  "owner": "kryptobaseddev",
@@ -37796,7 +38105,7 @@ var init_build_config = __esm({
37796
38105
  "url": "https://github.com/kryptobaseddev/cleo.git",
37797
38106
  "issuesUrl": "https://github.com/kryptobaseddev/cleo/issues"
37798
38107
  },
37799
- "buildDate": "2026-03-07T07:08:25.337Z",
38108
+ "buildDate": "2026-03-07T20:22:23.104Z",
37800
38109
  "templates": {
37801
38110
  "issueTemplatesDir": "templates/issue-templates"
37802
38111
  }
@@ -37805,8 +38114,8 @@ var init_build_config = __esm({
37805
38114
  });
37806
38115
 
37807
38116
  // src/core/issue/template-parser.ts
37808
- import { existsSync as existsSync60, readFileSync as readFileSync43, readdirSync as readdirSync17, writeFileSync as writeFileSync9 } from "node:fs";
37809
- import { join as join59, basename as basename10 } from "node:path";
38117
+ import { existsSync as existsSync61, readFileSync as readFileSync44, readdirSync as readdirSync17, writeFileSync as writeFileSync10 } from "node:fs";
38118
+ import { join as join60, basename as basename10 } from "node:path";
37810
38119
  function extractYamlField(content, field) {
37811
38120
  const regex = new RegExp(`^${field}:\\s*["']?(.+?)["']?\\s*$`, "m");
37812
38121
  const match = content.match(regex);
@@ -37838,9 +38147,9 @@ function extractYamlArray(content, field) {
37838
38147
  return items;
37839
38148
  }
37840
38149
  function parseTemplateFile2(filePath) {
37841
- if (!existsSync60(filePath)) return null;
38150
+ if (!existsSync61(filePath)) return null;
37842
38151
  try {
37843
- const content = readFileSync43(filePath, "utf-8");
38152
+ const content = readFileSync44(filePath, "utf-8");
37844
38153
  const fileName = basename10(filePath);
37845
38154
  const stem = fileName.replace(/\.ya?ml$/, "");
37846
38155
  const name = extractYamlField(content, "name");
@@ -37857,12 +38166,12 @@ function parseTemplateFile2(filePath) {
37857
38166
  function parseIssueTemplates2(projectDir) {
37858
38167
  try {
37859
38168
  const packageRoot = getPackageRoot();
37860
- const packagedTemplateDir = join59(packageRoot, PACKAGED_TEMPLATE_DIR);
37861
- if (existsSync60(packagedTemplateDir)) {
38169
+ const packagedTemplateDir = join60(packageRoot, PACKAGED_TEMPLATE_DIR);
38170
+ if (existsSync61(packagedTemplateDir)) {
37862
38171
  const templates3 = [];
37863
38172
  for (const file of readdirSync17(packagedTemplateDir)) {
37864
38173
  if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
37865
- const template = parseTemplateFile2(join59(packagedTemplateDir, file));
38174
+ const template = parseTemplateFile2(join60(packagedTemplateDir, file));
37866
38175
  if (template) templates3.push(template);
37867
38176
  }
37868
38177
  if (templates3.length > 0) return templates3;
@@ -37870,12 +38179,12 @@ function parseIssueTemplates2(projectDir) {
37870
38179
  } catch {
37871
38180
  }
37872
38181
  const dir = projectDir ?? getProjectRoot();
37873
- const templateDir = join59(dir, TEMPLATE_DIR);
37874
- if (!existsSync60(templateDir)) return [];
38182
+ const templateDir = join60(dir, TEMPLATE_DIR);
38183
+ if (!existsSync61(templateDir)) return [];
37875
38184
  const templates2 = [];
37876
38185
  for (const file of readdirSync17(templateDir)) {
37877
38186
  if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
37878
- const template = parseTemplateFile2(join59(templateDir, file));
38187
+ const template = parseTemplateFile2(join60(templateDir, file));
37879
38188
  if (template) templates2.push(template);
37880
38189
  }
37881
38190
  return templates2;
@@ -37884,10 +38193,10 @@ function getTemplateConfig(cwd) {
37884
38193
  const projectDir = cwd ?? getProjectRoot();
37885
38194
  const liveTemplates = parseIssueTemplates2(projectDir);
37886
38195
  if (liveTemplates.length > 0) return liveTemplates;
37887
- const cachePath = join59(getCleoDir(cwd), CACHE_FILE);
37888
- if (existsSync60(cachePath)) {
38196
+ const cachePath = join60(getCleoDir(cwd), CACHE_FILE);
38197
+ if (existsSync61(cachePath)) {
37889
38198
  try {
37890
- const cached = JSON.parse(readFileSync43(cachePath, "utf-8"));
38199
+ const cached = JSON.parse(readFileSync44(cachePath, "utf-8"));
37891
38200
  if (cached.templates?.length > 0) return cached.templates;
37892
38201
  } catch {
37893
38202
  }
@@ -38946,8 +39255,8 @@ var init_tools = __esm({
38946
39255
  });
38947
39256
 
38948
39257
  // src/core/nexus/query.ts
38949
- import { join as join60, basename as basename11 } from "node:path";
38950
- import { existsSync as existsSync61, readFileSync as readFileSync44 } from "node:fs";
39258
+ import { join as join61, basename as basename11 } from "node:path";
39259
+ import { existsSync as existsSync62, readFileSync as readFileSync45 } from "node:fs";
38951
39260
  import { z as z3 } from "zod";
38952
39261
  function validateSyntax(query) {
38953
39262
  if (!query) return false;
@@ -38983,9 +39292,9 @@ function getCurrentProject() {
38983
39292
  return process.env["NEXUS_CURRENT_PROJECT"];
38984
39293
  }
38985
39294
  try {
38986
- const infoPath = join60(process.cwd(), ".cleo", "project-info.json");
38987
- if (existsSync61(infoPath)) {
38988
- const data = JSON.parse(readFileSync44(infoPath, "utf-8"));
39295
+ const infoPath = join61(process.cwd(), ".cleo", "project-info.json");
39296
+ if (existsSync62(infoPath)) {
39297
+ const data = JSON.parse(readFileSync45(infoPath, "utf-8"));
38989
39298
  if (typeof data.name === "string" && data.name.length > 0) {
38990
39299
  return data.name;
38991
39300
  }
@@ -39021,7 +39330,7 @@ async function resolveProjectPath2(projectName) {
39021
39330
  return project.path;
39022
39331
  }
39023
39332
  async function readProjectTasks(projectPath) {
39024
- const tasksDbPath = join60(projectPath, ".cleo", "tasks.db");
39333
+ const tasksDbPath = join61(projectPath, ".cleo", "tasks.db");
39025
39334
  try {
39026
39335
  const accessor = await getAccessor(projectPath);
39027
39336
  const taskFile = await accessor.loadTaskFile();
@@ -39435,8 +39744,8 @@ var init_deps2 = __esm({
39435
39744
 
39436
39745
  // src/core/nexus/sharing/index.ts
39437
39746
  import { readFile as readFile15, writeFile as writeFile11 } from "node:fs/promises";
39438
- import { existsSync as existsSync62, readdirSync as readdirSync18, statSync as statSync9 } from "node:fs";
39439
- import { join as join61, relative as relative5 } from "node:path";
39747
+ import { existsSync as existsSync63, readdirSync as readdirSync18, statSync as statSync9 } from "node:fs";
39748
+ import { join as join62, relative as relative5 } from "node:path";
39440
39749
  function matchesPattern(filePath, pattern) {
39441
39750
  const normalizedPath = filePath.replace(/^\/+|\/+$/g, "");
39442
39751
  const normalizedPattern = pattern.replace(/^\/+|\/+$/g, "");
@@ -39461,7 +39770,7 @@ function collectCleoFiles(cleoDir) {
39461
39770
  const entries = readdirSync18(dir);
39462
39771
  for (const entry of entries) {
39463
39772
  if (entry === ".git") continue;
39464
- const fullPath = join61(dir, entry);
39773
+ const fullPath = join62(dir, entry);
39465
39774
  const relPath = relative5(cleoDir, fullPath);
39466
39775
  try {
39467
39776
  const stat3 = statSync9(fullPath);
@@ -39521,7 +39830,7 @@ function generateGitignoreEntries(sharing) {
39521
39830
  async function syncGitignore(cwd) {
39522
39831
  const config = await loadConfig2(cwd);
39523
39832
  const projectRoot = getProjectRoot(cwd);
39524
- const gitignorePath = join61(projectRoot, ".gitignore");
39833
+ const gitignorePath = join62(projectRoot, ".gitignore");
39525
39834
  const entries = generateGitignoreEntries(config.sharing);
39526
39835
  const managedSection = [
39527
39836
  "",
@@ -39531,7 +39840,7 @@ async function syncGitignore(cwd) {
39531
39840
  ""
39532
39841
  ].join("\n");
39533
39842
  let content = "";
39534
- if (existsSync62(gitignorePath)) {
39843
+ if (existsSync63(gitignorePath)) {
39535
39844
  content = await readFile15(gitignorePath, "utf-8");
39536
39845
  }
39537
39846
  const startIdx = content.indexOf(GITIGNORE_START);
@@ -43649,7 +43958,7 @@ init_logger();
43649
43958
  init_project_info();
43650
43959
  init_scaffold();
43651
43960
  init_audit_prune();
43652
- import { join as join62 } from "node:path";
43961
+ import { join as join63 } from "node:path";
43653
43962
  var serverState = null;
43654
43963
  var startupLog = getLogger("mcp:startup");
43655
43964
  async function main() {
@@ -43719,7 +44028,7 @@ async function main() {
43719
44028
  }
43720
44029
  const config = loadConfig();
43721
44030
  const projectInfo = getProjectInfoSync();
43722
- const cleoDir = join62(process.cwd(), ".cleo");
44031
+ const cleoDir = join63(process.cwd(), ".cleo");
43723
44032
  initLogger(cleoDir, {
43724
44033
  level: config.logLevel ?? "info",
43725
44034
  filePath: "logs/cleo.log",