@cleocode/cleo 2026.3.16 → 2026.3.18

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",
@@ -4895,13 +4915,13 @@ async function getDb(cwd) {
4895
4915
  if (!_gitTrackingChecked) {
4896
4916
  _gitTrackingChecked = true;
4897
4917
  try {
4898
- const { execFileSync: execFileSync10 } = await import("node:child_process");
4918
+ const { execFileSync: execFileSync12 } = await import("node:child_process");
4899
4919
  const gitCwd = resolve3(dbPath, "..", "..");
4900
4920
  const filesToCheck = [dbPath, dbPath + "-wal", dbPath + "-shm"];
4901
4921
  const log7 = getLogger("sqlite");
4902
4922
  for (const fileToCheck of filesToCheck) {
4903
4923
  try {
4904
- execFileSync10("git", ["ls-files", "--error-unmatch", fileToCheck], {
4924
+ execFileSync12("git", ["ls-files", "--error-unmatch", fileToCheck], {
4905
4925
  cwd: gitCwd,
4906
4926
  stdio: "pipe"
4907
4927
  });
@@ -15585,6 +15605,7 @@ async function getProjectStats(opts, accessor) {
15585
15605
  const active = tasks2.filter((t) => t.status === "active").length;
15586
15606
  const done = tasks2.filter((t) => t.status === "done").length;
15587
15607
  const blocked = tasks2.filter((t) => t.status === "blocked").length;
15608
+ const cancelled = tasks2.filter((t) => t.status === "cancelled").length;
15588
15609
  const totalActive = tasks2.length;
15589
15610
  const cutoff = new Date(Date.now() - periodDays * 864e5).toISOString();
15590
15611
  const entries = await queryAuditEntries(opts.cwd);
@@ -15601,11 +15622,45 @@ async function getProjectStats(opts, accessor) {
15601
15622
  (e) => isArchive(e) && e.timestamp >= cutoff
15602
15623
  ).length;
15603
15624
  const completionRate = createdInPeriod > 0 ? Math.round(completedInPeriod / createdInPeriod * 1e4) / 100 : 0;
15604
- const totalCreated = entries.filter(isCreate).length;
15605
- const totalCompleted = entries.filter(isComplete).length;
15606
- const totalArchived = entries.filter(isArchive).length;
15625
+ let totalCreated = 0;
15626
+ let totalCompleted = 0;
15627
+ let totalCancelled = 0;
15628
+ let totalArchived = 0;
15629
+ let archivedCompleted = 0;
15630
+ let archivedCount = 0;
15631
+ try {
15632
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_sqlite(), sqlite_exports));
15633
+ const { count: dbCount, eq: dbEq, and: dbAnd } = await import("drizzle-orm");
15634
+ const { tasks: tasksTable } = await Promise.resolve().then(() => (init_schema(), schema_exports));
15635
+ const db = await getDb2(opts.cwd);
15636
+ const statusRows = await db.select({ status: tasksTable.status, c: dbCount() }).from(tasksTable).groupBy(tasksTable.status).all();
15637
+ const statusMap = {};
15638
+ for (const row of statusRows) {
15639
+ statusMap[row.status] = row.c;
15640
+ }
15641
+ archivedCount = statusMap["archived"] ?? 0;
15642
+ totalCreated = Object.values(statusMap).reduce((sum, n) => sum + n, 0);
15643
+ totalCancelled = statusMap["cancelled"] ?? 0;
15644
+ totalArchived = archivedCount;
15645
+ const archivedDoneRow = await db.select({ c: dbCount() }).from(tasksTable).where(dbAnd(dbEq(tasksTable.status, "archived"), dbEq(tasksTable.archiveReason, "completed"))).get();
15646
+ archivedCompleted = archivedDoneRow?.c ?? 0;
15647
+ totalCompleted = (statusMap["done"] ?? 0) + archivedCompleted;
15648
+ } catch {
15649
+ totalCreated = entries.filter(isCreate).length;
15650
+ totalCompleted = entries.filter(isComplete).length;
15651
+ totalArchived = entries.filter(isArchive).length;
15652
+ }
15607
15653
  return {
15608
- currentState: { pending, active, done, blocked, totalActive },
15654
+ currentState: {
15655
+ pending,
15656
+ active,
15657
+ done,
15658
+ blocked,
15659
+ cancelled,
15660
+ totalActive,
15661
+ archived: archivedCount,
15662
+ grandTotal: totalActive + archivedCount
15663
+ },
15609
15664
  completionMetrics: {
15610
15665
  periodDays,
15611
15666
  completedInPeriod,
@@ -15617,7 +15672,7 @@ async function getProjectStats(opts, accessor) {
15617
15672
  completedInPeriod,
15618
15673
  archivedInPeriod
15619
15674
  },
15620
- allTime: { totalCreated, totalCompleted, totalArchived }
15675
+ allTime: { totalCreated, totalCompleted, totalCancelled, totalArchived, archivedCompleted }
15621
15676
  };
15622
15677
  }
15623
15678
  function rankBlockedTask(task, allTasks, focusTask) {
@@ -15661,7 +15716,18 @@ async function getDashboard(opts, accessor) {
15661
15716
  const active = tasks2.filter((t) => t.status === "active").length;
15662
15717
  const done = tasks2.filter((t) => t.status === "done").length;
15663
15718
  const blocked = tasks2.filter((t) => t.status === "blocked").length;
15719
+ const cancelled = tasks2.filter((t) => t.status === "cancelled").length;
15664
15720
  const total = tasks2.length;
15721
+ let archived = 0;
15722
+ try {
15723
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_sqlite(), sqlite_exports));
15724
+ const { count: dbCount, eq: dbEq } = await import("drizzle-orm");
15725
+ const { tasks: tasksTable } = await Promise.resolve().then(() => (init_schema(), schema_exports));
15726
+ const db = await getDb2(opts.cwd);
15727
+ const row = await db.select({ c: dbCount() }).from(tasksTable).where(dbEq(tasksTable.status, "archived")).get();
15728
+ archived = row?.c ?? 0;
15729
+ } catch {
15730
+ }
15665
15731
  const project = data.project?.name ?? "Unknown Project";
15666
15732
  const currentPhase = data.project?.currentPhase ?? null;
15667
15733
  const focusId = data.focus?.currentTask ?? null;
@@ -15669,7 +15735,7 @@ async function getDashboard(opts, accessor) {
15669
15735
  if (focusId) {
15670
15736
  focusTask = tasks2.find((t) => t.id === focusId) ?? null;
15671
15737
  }
15672
- const highPriority = tasks2.filter((t) => (t.priority === "critical" || t.priority === "high") && t.status !== "done").sort((a, b) => {
15738
+ const highPriority = tasks2.filter((t) => (t.priority === "critical" || t.priority === "high") && t.status !== "done" && t.status !== "cancelled").sort((a, b) => {
15673
15739
  const pDiff = (PRIORITY_ORDER[a.priority ?? "low"] ?? 9) - (PRIORITY_ORDER[b.priority ?? "low"] ?? 9);
15674
15740
  if (pDiff !== 0) return pDiff;
15675
15741
  return (a.createdAt ?? "").localeCompare(b.createdAt ?? "");
@@ -15682,6 +15748,7 @@ async function getDashboard(opts, accessor) {
15682
15748
  }).map((r) => r.task);
15683
15749
  const labelMap = {};
15684
15750
  for (const t of tasks2) {
15751
+ if (t.status === "cancelled") continue;
15685
15752
  for (const label of t.labels ?? []) {
15686
15753
  labelMap[label] = (labelMap[label] ?? 0) + 1;
15687
15754
  }
@@ -15690,7 +15757,7 @@ async function getDashboard(opts, accessor) {
15690
15757
  return {
15691
15758
  project,
15692
15759
  currentPhase,
15693
- summary: { pending, active, blocked, done, total },
15760
+ summary: { pending, active, blocked, done, cancelled, total, archived, grandTotal: total + archived },
15694
15761
  focus: { currentTask: focusId, task: focusTask },
15695
15762
  highPriority: { count: highPriority.length, tasks: highPriority.slice(0, 5) },
15696
15763
  blockedTasks: {
@@ -19811,7 +19878,9 @@ async function systemDash(projectRoot, params) {
19811
19878
  blocked: summary.blocked,
19812
19879
  done: summary.done,
19813
19880
  cancelled: summary.cancelled ?? 0,
19814
- total: summary.total
19881
+ total: summary.total,
19882
+ archived: summary.archived ?? 0,
19883
+ grandTotal: summary.grandTotal ?? summary.total
19815
19884
  },
19816
19885
  taskWork: data.focus ?? data.taskWork,
19817
19886
  activeSession: data.activeSession ?? null,
@@ -19831,17 +19900,18 @@ async function systemStats(projectRoot, params) {
19831
19900
  const result = await getProjectStats({ period: String(params?.period ?? 30), cwd: projectRoot }, accessor);
19832
19901
  const taskData = await accessor.loadTaskFile();
19833
19902
  const tasks2 = taskData?.tasks ?? [];
19903
+ const activeTasks = tasks2.filter((t) => t.status !== "cancelled");
19834
19904
  const byPriority = {};
19835
- for (const t of tasks2) {
19905
+ for (const t of activeTasks) {
19836
19906
  byPriority[t.priority] = (byPriority[t.priority] ?? 0) + 1;
19837
19907
  }
19838
19908
  const byType = {};
19839
- for (const t of tasks2) {
19909
+ for (const t of activeTasks) {
19840
19910
  const type = t.type || "task";
19841
19911
  byType[type] = (byType[type] ?? 0) + 1;
19842
19912
  }
19843
19913
  const byPhase = {};
19844
- for (const t of tasks2) {
19914
+ for (const t of activeTasks) {
19845
19915
  const phase = t.phase || "unassigned";
19846
19916
  byPhase[phase] = (byPhase[phase] ?? 0) + 1;
19847
19917
  }
@@ -19871,7 +19941,9 @@ async function systemStats(projectRoot, params) {
19871
19941
  done: currentState.done,
19872
19942
  blocked: currentState.blocked,
19873
19943
  cancelled: tasks2.filter((t) => t.status === "cancelled").length,
19874
- totalActive: currentState.totalActive
19944
+ totalActive: currentState.totalActive,
19945
+ archived: currentState.archived ?? 0,
19946
+ grandTotal: currentState.grandTotal ?? currentState.totalActive
19875
19947
  },
19876
19948
  byPriority,
19877
19949
  byType,
@@ -19896,10 +19968,10 @@ async function systemLog(projectRoot, filters) {
19896
19968
  }
19897
19969
  async function queryAuditLogSqlite(projectRoot, filters) {
19898
19970
  try {
19899
- const { join: join62 } = await import("node:path");
19900
- const { existsSync: existsSync62 } = await import("node:fs");
19901
- const dbPath = join62(projectRoot, ".cleo", "tasks.db");
19902
- if (!existsSync62(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)) {
19903
19975
  const offset = filters?.offset ?? 0;
19904
19976
  const limit = filters?.limit ?? 20;
19905
19977
  return {
@@ -20643,10 +20715,10 @@ async function readProjectMeta(projectPath) {
20643
20715
  }
20644
20716
  async function readProjectId(projectPath) {
20645
20717
  try {
20646
- const { readFileSync: readFileSync44, existsSync: existsSync62 } = await import("node:fs");
20718
+ const { readFileSync: readFileSync46, existsSync: existsSync64 } = await import("node:fs");
20647
20719
  const infoPath = join34(projectPath, ".cleo", "project-info.json");
20648
- if (!existsSync62(infoPath)) return "";
20649
- const data = JSON.parse(readFileSync44(infoPath, "utf-8"));
20720
+ if (!existsSync64(infoPath)) return "";
20721
+ const data = JSON.parse(readFileSync46(infoPath, "utf-8"));
20650
20722
  return typeof data.projectId === "string" ? data.projectId : "";
20651
20723
  } catch {
20652
20724
  return "";
@@ -27991,11 +28063,353 @@ var init_changelog_writer = __esm({
27991
28063
  }
27992
28064
  });
27993
28065
 
27994
- // src/core/release/release-manifest.ts
27995
- import { existsSync as existsSync49, renameSync as renameSync7 } from "node:fs";
27996
- import { readFile as readFile10 } from "node:fs/promises";
28066
+ // src/core/release/github-pr.ts
27997
28067
  import { execFileSync as execFileSync6 } from "node:child_process";
28068
+ function isGhCliAvailable() {
28069
+ try {
28070
+ execFileSync6("gh", ["--version"], { stdio: "pipe" });
28071
+ return true;
28072
+ } catch {
28073
+ return false;
28074
+ }
28075
+ }
28076
+ function extractRepoOwnerAndName(remote) {
28077
+ const trimmed = remote.trim();
28078
+ const httpsMatch = trimmed.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/);
28079
+ if (httpsMatch) {
28080
+ return { owner: httpsMatch[1], repo: httpsMatch[2] };
28081
+ }
28082
+ const sshMatch = trimmed.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/);
28083
+ if (sshMatch) {
28084
+ return { owner: sshMatch[1], repo: sshMatch[2] };
28085
+ }
28086
+ return null;
28087
+ }
28088
+ async function detectBranchProtection(branch, remote, projectRoot) {
28089
+ const cwdOpts = projectRoot ? { cwd: projectRoot } : {};
28090
+ if (isGhCliAvailable()) {
28091
+ try {
28092
+ const remoteUrl = execFileSync6("git", ["remote", "get-url", remote], {
28093
+ encoding: "utf-8",
28094
+ stdio: "pipe",
28095
+ ...cwdOpts
28096
+ }).trim();
28097
+ const identity = extractRepoOwnerAndName(remoteUrl);
28098
+ if (identity) {
28099
+ const { owner, repo } = identity;
28100
+ try {
28101
+ execFileSync6(
28102
+ "gh",
28103
+ ["api", `/repos/${owner}/${repo}/branches/${branch}/protection`],
28104
+ {
28105
+ encoding: "utf-8",
28106
+ stdio: "pipe",
28107
+ ...cwdOpts
28108
+ }
28109
+ );
28110
+ return { protected: true, detectionMethod: "gh-api" };
28111
+ } catch (apiErr) {
28112
+ const stderr = apiErr instanceof Error && "stderr" in apiErr ? String(apiErr.stderr ?? "") : "";
28113
+ if (stderr.includes("404") || stderr.includes("Not Found")) {
28114
+ return { protected: false, detectionMethod: "gh-api" };
28115
+ }
28116
+ }
28117
+ }
28118
+ } catch {
28119
+ }
28120
+ }
28121
+ try {
28122
+ const result = execFileSync6(
28123
+ "git",
28124
+ ["push", "--dry-run", remote, `HEAD:${branch}`],
28125
+ {
28126
+ encoding: "utf-8",
28127
+ stdio: "pipe",
28128
+ ...cwdOpts
28129
+ }
28130
+ );
28131
+ const output = typeof result === "string" ? result : "";
28132
+ if (output.includes("protected branch") || output.includes("GH006") || output.includes("refusing to allow")) {
28133
+ return { protected: true, detectionMethod: "push-dry-run" };
28134
+ }
28135
+ return { protected: false, detectionMethod: "push-dry-run" };
28136
+ } catch (pushErr) {
28137
+ const stderr = pushErr instanceof Error && "stderr" in pushErr ? String(pushErr.stderr ?? "") : pushErr instanceof Error ? pushErr.message : String(pushErr);
28138
+ if (stderr.includes("protected branch") || stderr.includes("GH006") || stderr.includes("refusing to allow")) {
28139
+ return { protected: true, detectionMethod: "push-dry-run" };
28140
+ }
28141
+ return {
28142
+ protected: false,
28143
+ detectionMethod: "unknown",
28144
+ error: stderr
28145
+ };
28146
+ }
28147
+ }
28148
+ function buildPRBody(opts) {
28149
+ const epicLine = opts.epicId ? `**Epic**: ${opts.epicId}
28150
+
28151
+ ` : "";
28152
+ return [
28153
+ `## Release v${opts.version}`,
28154
+ "",
28155
+ `${epicLine}This PR merges the ${opts.head} branch into ${opts.base} to publish the release.`,
28156
+ "",
28157
+ "### Checklist",
28158
+ "- [ ] CHANGELOG.md updated",
28159
+ "- [ ] All release tasks complete",
28160
+ "- [ ] Version bump committed",
28161
+ "",
28162
+ "---",
28163
+ "*Created by CLEO release pipeline*"
28164
+ ].join("\n");
28165
+ }
28166
+ function formatManualPRInstructions(opts) {
28167
+ const epicSuffix = opts.epicId ? ` (${opts.epicId})` : "";
28168
+ return [
28169
+ "Branch protection detected or gh CLI unavailable. Create the PR manually:",
28170
+ "",
28171
+ ` gh pr create \\`,
28172
+ ` --base ${opts.base} \\`,
28173
+ ` --head ${opts.head} \\`,
28174
+ ` --title "${opts.title}" \\`,
28175
+ ` --body "Release v${opts.version}${epicSuffix}"`,
28176
+ "",
28177
+ `Or visit: https://github.com/[owner]/[repo]/compare/${opts.base}...${opts.head}`,
28178
+ "",
28179
+ "After merging, CI will automatically publish to npm."
28180
+ ].join("\n");
28181
+ }
28182
+ async function createPullRequest(opts) {
28183
+ if (!isGhCliAvailable()) {
28184
+ return {
28185
+ mode: "manual",
28186
+ instructions: formatManualPRInstructions(opts)
28187
+ };
28188
+ }
28189
+ const body = buildPRBody(opts);
28190
+ const args = [
28191
+ "pr",
28192
+ "create",
28193
+ "--base",
28194
+ opts.base,
28195
+ "--head",
28196
+ opts.head,
28197
+ "--title",
28198
+ opts.title,
28199
+ "--body",
28200
+ body
28201
+ ];
28202
+ if (opts.labels && opts.labels.length > 0) {
28203
+ for (const label of opts.labels) {
28204
+ args.push("--label", label);
28205
+ }
28206
+ }
28207
+ try {
28208
+ const output = execFileSync6("gh", args, {
28209
+ encoding: "utf-8",
28210
+ stdio: "pipe",
28211
+ ...opts.projectRoot ? { cwd: opts.projectRoot } : {}
28212
+ });
28213
+ const prUrl = output.trim();
28214
+ const numberMatch = prUrl.match(/\/pull\/(\d+)$/);
28215
+ const prNumber = numberMatch ? parseInt(numberMatch[1], 10) : void 0;
28216
+ return {
28217
+ mode: "created",
28218
+ prUrl,
28219
+ prNumber
28220
+ };
28221
+ } catch (err) {
28222
+ const stderr = err instanceof Error && "stderr" in err ? String(err.stderr ?? "") : err instanceof Error ? err.message : String(err);
28223
+ if (stderr.includes("already exists")) {
28224
+ const urlMatch = stderr.match(/https:\/\/github\.com\/[^\s]+\/pull\/\d+/);
28225
+ const existingUrl = urlMatch ? urlMatch[0] : void 0;
28226
+ return {
28227
+ mode: "skipped",
28228
+ prUrl: existingUrl,
28229
+ instructions: "PR already exists"
28230
+ };
28231
+ }
28232
+ return {
28233
+ mode: "manual",
28234
+ instructions: formatManualPRInstructions(opts),
28235
+ error: stderr
28236
+ };
28237
+ }
28238
+ }
28239
+ var init_github_pr = __esm({
28240
+ "src/core/release/github-pr.ts"() {
28241
+ "use strict";
28242
+ }
28243
+ });
28244
+
28245
+ // src/core/release/channel.ts
28246
+ function getDefaultChannelConfig() {
28247
+ return {
28248
+ main: "main",
28249
+ develop: "develop",
28250
+ feature: "feature/"
28251
+ };
28252
+ }
28253
+ function resolveChannelFromBranch(branch, config) {
28254
+ const cfg = config ?? getDefaultChannelConfig();
28255
+ if (cfg.custom) {
28256
+ if (Object.prototype.hasOwnProperty.call(cfg.custom, branch)) {
28257
+ return cfg.custom[branch];
28258
+ }
28259
+ let bestPrefix = "";
28260
+ let bestChannel;
28261
+ for (const [key, channel] of Object.entries(cfg.custom)) {
28262
+ if (branch.startsWith(key) && key.length > bestPrefix.length) {
28263
+ bestPrefix = key;
28264
+ bestChannel = channel;
28265
+ }
28266
+ }
28267
+ if (bestChannel !== void 0) {
28268
+ return bestChannel;
28269
+ }
28270
+ }
28271
+ if (branch === cfg.main) {
28272
+ return "latest";
28273
+ }
28274
+ if (branch === cfg.develop) {
28275
+ return "beta";
28276
+ }
28277
+ const alphaPrefixes = ["feature/", "hotfix/", "release/"];
28278
+ if (cfg.feature && !alphaPrefixes.includes(cfg.feature)) {
28279
+ alphaPrefixes.push(cfg.feature);
28280
+ }
28281
+ for (const prefix of alphaPrefixes) {
28282
+ if (branch.startsWith(prefix)) {
28283
+ return "alpha";
28284
+ }
28285
+ }
28286
+ return "alpha";
28287
+ }
28288
+ function channelToDistTag(channel) {
28289
+ const tags = {
28290
+ latest: "latest",
28291
+ beta: "beta",
28292
+ alpha: "alpha"
28293
+ };
28294
+ return tags[channel];
28295
+ }
28296
+ function describeChannel(channel) {
28297
+ const descriptions = {
28298
+ latest: "stable release published to npm @latest",
28299
+ beta: "pre-release published to npm @beta (develop branch)",
28300
+ alpha: "early pre-release published to npm @alpha (feature/hotfix branches)"
28301
+ };
28302
+ return descriptions[channel];
28303
+ }
28304
+ var init_channel = __esm({
28305
+ "src/core/release/channel.ts"() {
28306
+ "use strict";
28307
+ }
28308
+ });
28309
+
28310
+ // src/core/release/release-config.ts
28311
+ import { existsSync as existsSync49, readFileSync as readFileSync38 } from "node:fs";
27998
28312
  import { join as join47 } from "node:path";
28313
+ function readConfigValueSync(path, defaultValue, cwd) {
28314
+ try {
28315
+ const configPath = join47(getCleoDir(cwd), "config.json");
28316
+ if (!existsSync49(configPath)) return defaultValue;
28317
+ const config = JSON.parse(readFileSync38(configPath, "utf-8"));
28318
+ const keys = path.split(".");
28319
+ let value = config;
28320
+ for (const key of keys) {
28321
+ if (value == null || typeof value !== "object") return defaultValue;
28322
+ value = value[key];
28323
+ }
28324
+ return value ?? defaultValue;
28325
+ } catch {
28326
+ return defaultValue;
28327
+ }
28328
+ }
28329
+ function loadReleaseConfig(cwd) {
28330
+ return {
28331
+ versioningScheme: readConfigValueSync("release.versioning.scheme", DEFAULTS2.versioningScheme, cwd),
28332
+ tagPrefix: readConfigValueSync("release.versioning.tagPrefix", DEFAULTS2.tagPrefix, cwd),
28333
+ changelogFormat: readConfigValueSync("release.changelog.format", DEFAULTS2.changelogFormat, cwd),
28334
+ changelogFile: readConfigValueSync("release.changelog.file", DEFAULTS2.changelogFile, cwd),
28335
+ artifactType: readConfigValueSync("release.artifact.type", DEFAULTS2.artifactType, cwd),
28336
+ gates: readConfigValueSync("release.gates", [], cwd),
28337
+ versionBump: {
28338
+ files: readConfigValueSync("release.versionBump.files", [], cwd)
28339
+ },
28340
+ security: {
28341
+ enableProvenance: readConfigValueSync("release.security.enableProvenance", false, cwd),
28342
+ slsaLevel: readConfigValueSync("release.security.slsaLevel", 3, cwd),
28343
+ requireSignedCommits: readConfigValueSync("release.security.requireSignedCommits", false, cwd)
28344
+ }
28345
+ };
28346
+ }
28347
+ function getDefaultGitFlowConfig() {
28348
+ return {
28349
+ enabled: true,
28350
+ branches: {
28351
+ main: "main",
28352
+ develop: "develop",
28353
+ featurePrefix: "feature/",
28354
+ hotfixPrefix: "hotfix/",
28355
+ releasePrefix: "release/"
28356
+ }
28357
+ };
28358
+ }
28359
+ function getGitFlowConfig(config) {
28360
+ const defaults = getDefaultGitFlowConfig();
28361
+ if (!config.gitflow) return defaults;
28362
+ return {
28363
+ enabled: config.gitflow.enabled ?? defaults.enabled,
28364
+ branches: {
28365
+ main: config.gitflow.branches?.main ?? defaults.branches.main,
28366
+ develop: config.gitflow.branches?.develop ?? defaults.branches.develop,
28367
+ featurePrefix: config.gitflow.branches?.featurePrefix ?? defaults.branches.featurePrefix,
28368
+ hotfixPrefix: config.gitflow.branches?.hotfixPrefix ?? defaults.branches.hotfixPrefix,
28369
+ releasePrefix: config.gitflow.branches?.releasePrefix ?? defaults.branches.releasePrefix
28370
+ }
28371
+ };
28372
+ }
28373
+ function getDefaultChannelConfig2() {
28374
+ return {
28375
+ main: "latest",
28376
+ develop: "beta",
28377
+ feature: "alpha"
28378
+ };
28379
+ }
28380
+ function getChannelConfig(config) {
28381
+ const defaults = getDefaultChannelConfig2();
28382
+ if (!config.channels) return defaults;
28383
+ return {
28384
+ main: config.channels.main ?? defaults.main,
28385
+ develop: config.channels.develop ?? defaults.develop,
28386
+ feature: config.channels.feature ?? defaults.feature,
28387
+ custom: config.channels.custom
28388
+ };
28389
+ }
28390
+ function getPushMode(config) {
28391
+ return config.push?.mode ?? "auto";
28392
+ }
28393
+ var DEFAULTS2;
28394
+ var init_release_config = __esm({
28395
+ "src/core/release/release-config.ts"() {
28396
+ "use strict";
28397
+ init_paths();
28398
+ DEFAULTS2 = {
28399
+ versioningScheme: "calver",
28400
+ tagPrefix: "v",
28401
+ changelogFormat: "keepachangelog",
28402
+ changelogFile: "CHANGELOG.md",
28403
+ artifactType: "generic-tarball"
28404
+ };
28405
+ }
28406
+ });
28407
+
28408
+ // src/core/release/release-manifest.ts
28409
+ import { existsSync as existsSync50, renameSync as renameSync7 } from "node:fs";
28410
+ import { readFile as readFile10 } from "node:fs/promises";
28411
+ import { execFileSync as execFileSync7 } from "node:child_process";
28412
+ import { join as join48 } from "node:path";
27999
28413
  import { eq as eq14, desc as desc4 } from "drizzle-orm";
28000
28414
  function isValidVersion(version) {
28001
28415
  return /^v?\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$/.test(version);
@@ -28087,25 +28501,78 @@ async function generateReleaseChangelog(version, loadTasksFn, cwd) {
28087
28501
  const chores = [];
28088
28502
  const docs = [];
28089
28503
  const tests = [];
28090
- const other = [];
28504
+ const changes = [];
28505
+ function stripConventionalPrefix(title) {
28506
+ return title.replace(/^(feat|fix|docs?|test|chore|refactor|style|ci|build|perf)(\([^)]+\))?:\s*/i, "");
28507
+ }
28508
+ function capitalize(s) {
28509
+ return s.length === 0 ? s : s[0].toUpperCase() + s.slice(1);
28510
+ }
28511
+ function buildEntry(task) {
28512
+ const cleanTitle = capitalize(stripConventionalPrefix(task.title));
28513
+ const safeDesc = task.description?.replace(/\r?\n/g, " ").replace(/\s{2,}/g, " ").trim();
28514
+ const desc6 = safeDesc;
28515
+ const shouldIncludeDesc = (() => {
28516
+ if (!desc6 || desc6.length === 0) return false;
28517
+ const titleNorm = cleanTitle.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim();
28518
+ const descNorm = desc6.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim();
28519
+ if (titleNorm === descNorm) return false;
28520
+ if (descNorm.startsWith(titleNorm) && descNorm.length < titleNorm.length * 1.3) return false;
28521
+ return desc6.length >= 20;
28522
+ })();
28523
+ if (shouldIncludeDesc) {
28524
+ const descDisplay = desc6.length > 150 ? desc6.slice(0, 147) + "..." : desc6;
28525
+ return `- **${cleanTitle}**: ${descDisplay} (${task.id})`;
28526
+ }
28527
+ return `- ${cleanTitle} (${task.id})`;
28528
+ }
28529
+ function categorizeTask(task) {
28530
+ if (task.type === "epic") return "changes";
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";
28537
+ if (/^feat(\([^)]+\))?:/.test(task.title.toLowerCase())) return "features";
28538
+ if (/^fix(\([^)]+\))?:/.test(task.title.toLowerCase())) return "fixes";
28539
+ if (/^docs?(\([^)]+\))?:/.test(task.title.toLowerCase())) return "docs";
28540
+ if (/^test(\([^)]+\))?:/.test(task.title.toLowerCase())) return "tests";
28541
+ if (/^(chore|refactor|style|ci|build|perf)(\([^)]+\))?:/.test(task.title.toLowerCase())) return "chores";
28542
+ const labels = task.labels ?? [];
28543
+ if (labels.some((l) => ["test", "testing"].includes(l.toLowerCase()))) return "tests";
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";
28546
+ if (labels.some((l) => ["docs", "documentation"].includes(l.toLowerCase()))) return "docs";
28547
+ if (labels.some((l) => ["chore", "refactor", "cleanup", "maintenance"].includes(l.toLowerCase()))) return "chores";
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";
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";
28553
+ if (titleLower.startsWith("doc") || titleLower.includes("documentation") || titleLower.includes("readme") || titleLower.includes("changelog")) return "docs";
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";
28555
+ if (rawTitleLower.startsWith("feat")) return "features";
28556
+ return "changes";
28557
+ }
28091
28558
  for (const taskId of releaseTasks) {
28092
28559
  const task = taskMap.get(taskId);
28093
28560
  if (!task) continue;
28094
- const titleLower = task.title.toLowerCase();
28095
- const entry = `- ${task.title} (${task.id})`;
28096
- if (titleLower.startsWith("feat") || titleLower.includes("add ") || titleLower.includes("implement")) {
28097
- features.push(entry);
28098
- } else if (titleLower.startsWith("fix") || titleLower.includes("bug")) {
28099
- fixes.push(entry);
28100
- } else if (titleLower.startsWith("doc") || titleLower.includes("documentation")) {
28101
- docs.push(entry);
28102
- } else if (titleLower.startsWith("test") || titleLower.includes("test")) {
28103
- tests.push(entry);
28104
- } else if (titleLower.startsWith("chore") || titleLower.includes("refactor")) {
28105
- chores.push(entry);
28106
- } else {
28107
- other.push(entry);
28108
- }
28561
+ if (task.type === "epic") continue;
28562
+ if (task.labels?.some((l) => l.toLowerCase() === "epic")) continue;
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;
28568
+ const category = categorizeTask(task);
28569
+ const entry = buildEntry(task);
28570
+ if (category === "features") features.push(entry);
28571
+ else if (category === "fixes") fixes.push(entry);
28572
+ else if (category === "docs") docs.push(entry);
28573
+ else if (category === "tests") tests.push(entry);
28574
+ else if (category === "chores") chores.push(entry);
28575
+ else changes.push(entry);
28109
28576
  }
28110
28577
  const sections = [];
28111
28578
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -28140,14 +28607,14 @@ async function generateReleaseChangelog(version, loadTasksFn, cwd) {
28140
28607
  sections.push(...chores);
28141
28608
  sections.push("");
28142
28609
  }
28143
- if (other.length > 0) {
28144
- sections.push("### Other");
28145
- sections.push(...other);
28610
+ if (changes.length > 0) {
28611
+ sections.push("### Changes");
28612
+ sections.push(...changes);
28146
28613
  sections.push("");
28147
28614
  }
28148
28615
  const changelog = sections.join("\n");
28149
28616
  await db.update(releaseManifests).set({ changelog }).where(eq14(releaseManifests.version, normalizedVersion)).run();
28150
- const changelogPath = join47(cwd ?? process.cwd(), "CHANGELOG.md");
28617
+ const changelogPath = join48(cwd ?? process.cwd(), "CHANGELOG.md");
28151
28618
  let existingChangelogContent = "";
28152
28619
  try {
28153
28620
  existingChangelogContent = await readFile10(changelogPath, "utf8");
@@ -28155,7 +28622,7 @@ async function generateReleaseChangelog(version, loadTasksFn, cwd) {
28155
28622
  }
28156
28623
  const { customBlocks } = parseChangelogBlocks(existingChangelogContent);
28157
28624
  const changelogBody = sections.slice(2).join("\n");
28158
- await writeChangelogSection(normalizedVersion, changelogBody, customBlocks, changelogPath);
28625
+ await writeChangelogSection(normalizedVersion.replace(/^v/, ""), changelogBody, customBlocks, changelogPath);
28159
28626
  return {
28160
28627
  version: normalizedVersion,
28161
28628
  changelog,
@@ -28166,7 +28633,7 @@ async function generateReleaseChangelog(version, loadTasksFn, cwd) {
28166
28633
  docs: docs.length,
28167
28634
  tests: tests.length,
28168
28635
  chores: chores.length,
28169
- other: other.length
28636
+ changes: changes.length
28170
28637
  }
28171
28638
  };
28172
28639
  }
@@ -28228,7 +28695,7 @@ async function tagRelease(version, cwd) {
28228
28695
  await db.update(releaseManifests).set({ status: "tagged", taggedAt }).where(eq14(releaseManifests.version, normalizedVersion)).run();
28229
28696
  return { version: normalizedVersion, status: "tagged", taggedAt };
28230
28697
  }
28231
- async function runReleaseGates(version, loadTasksFn, cwd) {
28698
+ async function runReleaseGates(version, loadTasksFn, cwd, opts) {
28232
28699
  if (!version) {
28233
28700
  throw new Error("version is required");
28234
28701
  }
@@ -28267,58 +28734,121 @@ async function runReleaseGates(version, loadTasksFn, cwd) {
28267
28734
  message: incompleteTasks.length === 0 ? "All tasks completed" : `${incompleteTasks.length} tasks not completed: ${incompleteTasks.join(", ")}`
28268
28735
  });
28269
28736
  const projectRoot = cwd ?? getProjectRoot();
28270
- const distPath = join47(projectRoot, "dist", "cli", "index.js");
28271
- const isNodeProject = existsSync49(join47(projectRoot, "package.json"));
28737
+ const distPath = join48(projectRoot, "dist", "cli", "index.js");
28738
+ const isNodeProject = existsSync50(join48(projectRoot, "package.json"));
28272
28739
  if (isNodeProject) {
28273
28740
  gates.push({
28274
28741
  name: "build_artifact",
28275
- status: existsSync49(distPath) ? "passed" : "failed",
28276
- message: existsSync49(distPath) ? "dist/cli/index.js present" : "dist/ not built \u2014 run: npm run build"
28742
+ status: existsSync50(distPath) ? "passed" : "failed",
28743
+ message: existsSync50(distPath) ? "dist/cli/index.js present" : "dist/ not built \u2014 run: npm run build"
28277
28744
  });
28278
28745
  }
28279
- let workingTreeClean = true;
28280
- let dirtyFiles = [];
28281
- try {
28282
- const porcelain = execFileSync6("git", ["status", "--porcelain"], {
28283
- cwd: projectRoot,
28284
- encoding: "utf-8",
28285
- 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)` : ""}`
28286
28769
  });
28287
- dirtyFiles = porcelain.split("\n").filter((l) => l.trim()).map((l) => l.slice(3).trim()).filter((f) => f !== "CHANGELOG.md" && f !== "VERSION" && f !== "package.json");
28288
- workingTreeClean = dirtyFiles.length === 0;
28289
- } catch {
28290
28770
  }
28291
- gates.push({
28292
- name: "clean_working_tree",
28293
- status: workingTreeClean ? "passed" : "failed",
28294
- 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)` : ""}`
28295
- });
28296
28771
  const isPreRelease = normalizedVersion.includes("-");
28297
28772
  let currentBranch = "";
28298
28773
  try {
28299
- currentBranch = execFileSync6("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
28774
+ currentBranch = execFileSync7("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
28300
28775
  cwd: projectRoot,
28301
28776
  encoding: "utf-8",
28302
28777
  stdio: "pipe"
28303
28778
  }).trim();
28304
28779
  } catch {
28305
28780
  }
28306
- const expectedBranch = isPreRelease ? "develop" : "main";
28307
- const branchOk = !currentBranch || currentBranch === expectedBranch || currentBranch === "HEAD";
28781
+ const releaseConfig = loadReleaseConfig(cwd);
28782
+ const gitFlowCfg = getGitFlowConfig(releaseConfig);
28783
+ const channelCfg = getChannelConfig(releaseConfig);
28784
+ const expectedBranch = isPreRelease ? gitFlowCfg.branches.develop : gitFlowCfg.branches.main;
28785
+ const isFeatureBranch = currentBranch.startsWith(gitFlowCfg.branches.featurePrefix) || currentBranch.startsWith(gitFlowCfg.branches.hotfixPrefix) || currentBranch.startsWith(gitFlowCfg.branches.releasePrefix);
28786
+ const branchOk = !currentBranch || currentBranch === "HEAD" || currentBranch === expectedBranch || isPreRelease && isFeatureBranch;
28787
+ const detectedChannel = currentBranch ? resolveChannelFromBranch(currentBranch, channelCfg) : isPreRelease ? "beta" : "latest";
28308
28788
  gates.push({
28309
28789
  name: "branch_target",
28310
28790
  status: branchOk ? "passed" : "failed",
28311
- message: branchOk ? `On correct branch: ${currentBranch}` : `Expected branch '${expectedBranch}' for ${isPreRelease ? "pre-release" : "stable"} release, but on '${currentBranch}'`
28791
+ message: branchOk ? `On correct branch: ${currentBranch} (channel: ${detectedChannel})` : `Expected branch '${expectedBranch}' for ${isPreRelease ? "pre-release" : "stable"} release, but on '${currentBranch}'`
28792
+ });
28793
+ const pushMode = getPushMode(releaseConfig);
28794
+ let requiresPR = false;
28795
+ if (pushMode === "pr") {
28796
+ requiresPR = true;
28797
+ } else if (pushMode === "auto") {
28798
+ try {
28799
+ const protectionResult = await detectBranchProtection(
28800
+ expectedBranch,
28801
+ "origin",
28802
+ projectRoot
28803
+ );
28804
+ requiresPR = protectionResult.protected;
28805
+ } catch {
28806
+ requiresPR = false;
28807
+ }
28808
+ }
28809
+ gates.push({
28810
+ name: "branch_protection",
28811
+ status: "passed",
28812
+ message: requiresPR ? `Branch '${expectedBranch}' is protected \u2014 release.ship will create a PR` : `Branch '${expectedBranch}' allows direct push`
28312
28813
  });
28313
28814
  const allPassed = gates.every((g) => g.status === "passed");
28815
+ const metadata = {
28816
+ channel: detectedChannel,
28817
+ requiresPR,
28818
+ targetBranch: expectedBranch,
28819
+ currentBranch
28820
+ };
28314
28821
  return {
28315
28822
  version: normalizedVersion,
28316
28823
  allPassed,
28317
28824
  gates,
28318
28825
  passedCount: gates.filter((g) => g.status === "passed").length,
28319
- failedCount: gates.filter((g) => g.status === "failed").length
28826
+ failedCount: gates.filter((g) => g.status === "failed").length,
28827
+ metadata
28320
28828
  };
28321
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
+ }
28322
28852
  async function rollbackRelease(version, reason, cwd) {
28323
28853
  if (!version) {
28324
28854
  throw new Error("version is required");
@@ -28339,7 +28869,7 @@ async function rollbackRelease(version, reason, cwd) {
28339
28869
  };
28340
28870
  }
28341
28871
  async function readPushPolicy(cwd) {
28342
- const configPath = join47(getCleoDirAbsolute(cwd), "config.json");
28872
+ const configPath = join48(getCleoDirAbsolute(cwd), "config.json");
28343
28873
  const config = await readJson(configPath);
28344
28874
  if (!config) return void 0;
28345
28875
  const release2 = config.release;
@@ -28353,6 +28883,33 @@ async function pushRelease(version, remote, cwd, opts) {
28353
28883
  const normalizedVersion = normalizeVersion(version);
28354
28884
  const projectRoot = getProjectRoot(cwd);
28355
28885
  const pushPolicy = await readPushPolicy(cwd);
28886
+ const configPushMode = getPushMode(loadReleaseConfig(cwd));
28887
+ const effectivePushMode = opts?.mode ?? pushPolicy?.mode ?? configPushMode;
28888
+ if (effectivePushMode === "pr" || effectivePushMode === "auto") {
28889
+ const targetRemoteForCheck = remote ?? pushPolicy?.remote ?? "origin";
28890
+ let branchIsProtected = effectivePushMode === "pr";
28891
+ if (effectivePushMode === "auto") {
28892
+ try {
28893
+ const protection = await detectBranchProtection(
28894
+ pushPolicy?.allowedBranches?.[0] ?? "main",
28895
+ targetRemoteForCheck,
28896
+ projectRoot
28897
+ );
28898
+ branchIsProtected = protection.protected;
28899
+ } catch {
28900
+ branchIsProtected = false;
28901
+ }
28902
+ }
28903
+ if (branchIsProtected) {
28904
+ return {
28905
+ version: normalizedVersion,
28906
+ status: "requires_pr",
28907
+ remote: targetRemoteForCheck,
28908
+ pushedAt: (/* @__PURE__ */ new Date()).toISOString(),
28909
+ requiresPR: true
28910
+ };
28911
+ }
28912
+ }
28356
28913
  if (pushPolicy && pushPolicy.enabled === false && !opts?.explicitPush) {
28357
28914
  throw new Error(
28358
28915
  "Push is disabled by config (release.push.enabled=false). Use --push to override."
@@ -28360,20 +28917,21 @@ async function pushRelease(version, remote, cwd, opts) {
28360
28917
  }
28361
28918
  const targetRemote = remote ?? pushPolicy?.remote ?? "origin";
28362
28919
  if (pushPolicy?.requireCleanTree) {
28363
- const statusOutput = execFileSync6("git", ["status", "--porcelain"], {
28920
+ const statusOutput = execFileSync7("git", ["status", "--porcelain"], {
28364
28921
  cwd: projectRoot,
28365
28922
  timeout: 1e4,
28366
28923
  encoding: "utf-8",
28367
28924
  stdio: ["pipe", "pipe", "pipe"]
28368
28925
  });
28369
- 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) {
28370
28928
  throw new Error(
28371
28929
  "Git working tree is not clean. Commit or stash changes before pushing (config: release.push.requireCleanTree=true)."
28372
28930
  );
28373
28931
  }
28374
28932
  }
28375
28933
  if (pushPolicy?.allowedBranches && pushPolicy.allowedBranches.length > 0) {
28376
- const currentBranch = execFileSync6("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
28934
+ const currentBranch = execFileSync7("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
28377
28935
  cwd: projectRoot,
28378
28936
  timeout: 1e4,
28379
28937
  encoding: "utf-8",
@@ -28385,7 +28943,7 @@ async function pushRelease(version, remote, cwd, opts) {
28385
28943
  );
28386
28944
  }
28387
28945
  }
28388
- execFileSync6("git", ["push", targetRemote, "--follow-tags"], {
28946
+ execFileSync7("git", ["push", targetRemote, "--follow-tags"], {
28389
28947
  cwd: projectRoot,
28390
28948
  timeout: 6e4,
28391
28949
  encoding: "utf-8",
@@ -28416,6 +28974,9 @@ var init_release_manifest = __esm({
28416
28974
  init_paths();
28417
28975
  init_json();
28418
28976
  init_changelog_writer();
28977
+ init_github_pr();
28978
+ init_channel();
28979
+ init_release_config();
28419
28980
  }
28420
28981
  });
28421
28982
 
@@ -28495,8 +29056,181 @@ var init_guards = __esm({
28495
29056
  }
28496
29057
  });
28497
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
+
28498
29232
  // src/dispatch/engines/release-engine.ts
28499
- import { execFileSync as execFileSync7 } from "node:child_process";
29233
+ import { execFileSync as execFileSync8 } from "node:child_process";
28500
29234
  function isAgentContext() {
28501
29235
  return !!(process.env["CLEO_SESSION_ID"] || process.env["CLAUDE_AGENT_TYPE"]);
28502
29236
  }
@@ -28617,6 +29351,23 @@ async function releaseRollback(version, reason, projectRoot) {
28617
29351
  return engineError(code, message);
28618
29352
  }
28619
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
+ }
28620
29371
  async function releasePush(version, remote, projectRoot, opts) {
28621
29372
  if (isAgentContext()) {
28622
29373
  const hasEntry = await hasManifestEntry(version, projectRoot);
@@ -28638,7 +29389,7 @@ async function releasePush(version, remote, projectRoot, opts) {
28638
29389
  const result = await pushRelease(version, remote, projectRoot, opts);
28639
29390
  let commitSha;
28640
29391
  try {
28641
- commitSha = execFileSync7("git", ["rev-parse", "HEAD"], {
29392
+ commitSha = execFileSync8("git", ["rev-parse", "HEAD"], {
28642
29393
  cwd: projectRoot ?? process.cwd(),
28643
29394
  encoding: "utf-8",
28644
29395
  stdio: "pipe"
@@ -28660,7 +29411,7 @@ async function releasePush(version, remote, projectRoot, opts) {
28660
29411
  }
28661
29412
  }
28662
29413
  async function releaseShip(params, projectRoot) {
28663
- const { version, epicId, remote, dryRun = false } = params;
29414
+ const { version, epicId, remote, dryRun = false, bump = true } = params;
28664
29415
  if (!version) {
28665
29416
  return engineError("E_INVALID_INPUT", "version is required");
28666
29417
  }
@@ -28668,18 +29419,71 @@ async function releaseShip(params, projectRoot) {
28668
29419
  return engineError("E_INVALID_INPUT", "epicId is required");
28669
29420
  }
28670
29421
  const cwd = projectRoot ?? resolveProjectRoot();
29422
+ const steps = [];
29423
+ const logStep = (n, total, label, done, error) => {
29424
+ let msg;
29425
+ if (done === void 0) {
29426
+ msg = `[Step ${n}/${total}] ${label}...`;
29427
+ } else if (done) {
29428
+ msg = ` \u2713 ${label}`;
29429
+ } else {
29430
+ msg = ` \u2717 ${label}: ${error ?? "failed"}`;
29431
+ }
29432
+ steps.push(msg);
29433
+ console.log(msg);
29434
+ };
29435
+ const bumpTargets = getVersionBumpConfig(cwd);
29436
+ const shouldBump = bump && bumpTargets.length > 0;
28671
29437
  try {
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");
28672
29453
  const gatesResult = await runReleaseGates(
28673
29454
  version,
28674
29455
  () => loadTasks2(projectRoot),
28675
- projectRoot
29456
+ projectRoot,
29457
+ { dryRun }
28676
29458
  );
28677
29459
  if (gatesResult && !gatesResult.allPassed) {
28678
29460
  const failedGates = gatesResult.gates.filter((g) => g.status === "failed");
29461
+ logStep(1, 8, "Validate release gates", false, failedGates.map((g) => g.name).join(", "));
28679
29462
  return engineError("E_LIFECYCLE_GATE_FAILED", `Release gates failed for ${version}: ${failedGates.map((g) => g.name).join(", ")}`, {
28680
29463
  details: { gates: gatesResult.gates, failedCount: gatesResult.failedCount }
28681
29464
  });
28682
29465
  }
29466
+ logStep(1, 8, "Validate release gates", true);
29467
+ let resolvedChannel = "latest";
29468
+ let currentBranchForPR = "HEAD";
29469
+ try {
29470
+ const branchName = execFileSync8("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
29471
+ cwd,
29472
+ encoding: "utf-8",
29473
+ stdio: "pipe"
29474
+ }).trim();
29475
+ currentBranchForPR = branchName;
29476
+ const channelEnum = resolveChannelFromBranch(branchName);
29477
+ resolvedChannel = channelToDistTag(channelEnum);
29478
+ } catch {
29479
+ }
29480
+ const gateMetadata = gatesResult.metadata;
29481
+ const requiresPRFromGates = gateMetadata?.requiresPR ?? false;
29482
+ const targetBranchFromGates = gateMetadata?.targetBranch;
29483
+ if (gateMetadata?.currentBranch) {
29484
+ currentBranchForPR = gateMetadata.currentBranch;
29485
+ }
29486
+ logStep(2, 8, "Check epic completeness");
28683
29487
  let releaseTaskIds = [];
28684
29488
  try {
28685
29489
  const manifest = await showManifestRelease(version, projectRoot);
@@ -28690,10 +29494,13 @@ async function releaseShip(params, projectRoot) {
28690
29494
  const epicCheck = await checkEpicCompleteness(releaseTaskIds, projectRoot, epicAccessor);
28691
29495
  if (epicCheck.hasIncomplete) {
28692
29496
  const incomplete = epicCheck.epics.filter((e) => e.missingChildren.length > 0).map((e) => `${e.epicId}: missing ${e.missingChildren.map((c) => c.id).join(", ")}`).join("; ");
29497
+ logStep(2, 8, "Check epic completeness", false, incomplete);
28693
29498
  return engineError("E_LIFECYCLE_GATE_FAILED", `Epic completeness check failed: ${incomplete}`, {
28694
29499
  details: { epics: epicCheck.epics }
28695
29500
  });
28696
29501
  }
29502
+ logStep(2, 8, "Check epic completeness", true);
29503
+ logStep(3, 8, "Check task double-listing");
28697
29504
  const allReleases = await listManifestReleases(projectRoot);
28698
29505
  const existingReleases = (allReleases.releases ?? []).filter((r) => r.version !== version);
28699
29506
  const doubleCheck = checkDoubleListing(
@@ -28702,73 +29509,160 @@ async function releaseShip(params, projectRoot) {
28702
29509
  );
28703
29510
  if (doubleCheck.hasDoubleListing) {
28704
29511
  const dupes = doubleCheck.duplicates.map((d) => `${d.taskId} (in ${d.releases.join(", ")})`).join("; ");
29512
+ logStep(3, 8, "Check task double-listing", false, dupes);
28705
29513
  return engineError("E_VALIDATION", `Double-listing detected: ${dupes}`, {
28706
29514
  details: { duplicates: doubleCheck.duplicates }
28707
29515
  });
28708
29516
  }
28709
- const changelogResult = await generateReleaseChangelog(
29517
+ logStep(3, 8, "Check task double-listing", true);
29518
+ const loadedConfig = loadReleaseConfig(cwd);
29519
+ const pushMode = getPushMode(loadedConfig);
29520
+ const gitflowCfg = getGitFlowConfig(loadedConfig);
29521
+ const targetBranch = targetBranchFromGates ?? gitflowCfg.branches.main;
29522
+ if (dryRun) {
29523
+ logStep(4, 8, "Generate CHANGELOG");
29524
+ logStep(4, 8, "Generate CHANGELOG", true);
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
+ );
29537
+ const dryRunOutput = {
29538
+ version,
29539
+ epicId,
29540
+ dryRun: true,
29541
+ channel: resolvedChannel,
29542
+ pushMode,
29543
+ wouldDo
29544
+ };
29545
+ if (wouldCreatePR) {
29546
+ const ghAvailable = isGhCliAvailable();
29547
+ dryRunOutput["wouldDo"].push(
29548
+ ghAvailable ? `gh pr create --base ${targetBranch} --head ${currentBranchForPR} --title "release: ship v${version}"` : `manual PR: ${currentBranchForPR} \u2192 ${targetBranch} (gh CLI not available)`
29549
+ );
29550
+ dryRunOutput["wouldCreatePR"] = true;
29551
+ dryRunOutput["prTitle"] = `release: ship v${version}`;
29552
+ dryRunOutput["prTargetBranch"] = targetBranch;
29553
+ } else {
29554
+ dryRunOutput["wouldDo"].push(
29555
+ `git push ${remote ?? "origin"} --follow-tags`
29556
+ );
29557
+ dryRunOutput["wouldCreatePR"] = false;
29558
+ }
29559
+ dryRunOutput["wouldDo"].push("markReleasePushed(...)");
29560
+ return { success: true, data: { ...dryRunOutput, steps } };
29561
+ }
29562
+ logStep(4, 8, "Generate CHANGELOG");
29563
+ await generateReleaseChangelog(
28710
29564
  version,
28711
29565
  () => loadTasks2(projectRoot),
28712
29566
  projectRoot
28713
29567
  );
28714
29568
  const changelogPath = `${cwd}/CHANGELOG.md`;
28715
- const generatedContent = changelogResult.changelog ?? "";
28716
- if (dryRun) {
28717
- return {
28718
- success: true,
28719
- data: {
28720
- version,
28721
- epicId,
28722
- dryRun: true,
28723
- wouldDo: [
28724
- `write CHANGELOG section for ${version} (${generatedContent.length} chars)`,
28725
- "git add CHANGELOG.md",
28726
- `git commit -m "release: ship v${version} (${epicId})"`,
28727
- `git tag -a v${version} -m "Release v${version}"`,
28728
- `git push ${remote ?? "origin"} --follow-tags`,
28729
- "markReleasePushed(...)"
28730
- ]
28731
- }
28732
- };
28733
- }
28734
- await writeChangelogSection(version, generatedContent, [], changelogPath);
29569
+ logStep(4, 8, "Generate CHANGELOG", true);
29570
+ logStep(5, 8, "Commit release");
28735
29571
  const gitCwd = { cwd, encoding: "utf-8", stdio: "pipe" };
29572
+ const filesToStage = ["CHANGELOG.md", ...shouldBump ? bumpTargets.map((t) => t.file) : []];
28736
29573
  try {
28737
- execFileSync7("git", ["add", "CHANGELOG.md"], gitCwd);
29574
+ execFileSync8("git", ["add", ...filesToStage], gitCwd);
28738
29575
  } catch (err) {
28739
29576
  const msg = err.message ?? String(err);
29577
+ logStep(5, 8, "Commit release", false, `git add failed: ${msg}`);
28740
29578
  return engineError("E_GENERAL", `git add failed: ${msg}`);
28741
29579
  }
28742
29580
  try {
28743
- execFileSync7(
29581
+ execFileSync8(
28744
29582
  "git",
28745
29583
  ["commit", "-m", `release: ship v${version} (${epicId})`],
28746
29584
  gitCwd
28747
29585
  );
28748
29586
  } catch (err) {
28749
29587
  const msg = err.stderr ?? err.message ?? String(err);
29588
+ logStep(5, 8, "Commit release", false, `git commit failed: ${msg}`);
28750
29589
  return engineError("E_GENERAL", `git commit failed: ${msg}`);
28751
29590
  }
29591
+ logStep(5, 8, "Commit release", true);
28752
29592
  let commitSha;
28753
29593
  try {
28754
- commitSha = execFileSync7("git", ["rev-parse", "HEAD"], gitCwd).toString().trim();
29594
+ commitSha = execFileSync8("git", ["rev-parse", "HEAD"], gitCwd).toString().trim();
28755
29595
  } catch {
28756
29596
  }
29597
+ logStep(6, 8, "Tag release");
28757
29598
  const gitTag = `v${version.replace(/^v/, "")}`;
28758
29599
  try {
28759
- execFileSync7("git", ["tag", "-a", gitTag, "-m", `Release ${gitTag}`], gitCwd);
29600
+ execFileSync8("git", ["tag", "-a", gitTag, "-m", `Release ${gitTag}`], gitCwd);
28760
29601
  } catch (err) {
28761
29602
  const msg = err.stderr ?? err.message ?? String(err);
29603
+ logStep(6, 8, "Tag release", false, `git tag failed: ${msg}`);
28762
29604
  return engineError("E_GENERAL", `git tag failed: ${msg}`);
28763
29605
  }
28764
- try {
28765
- execFileSync7("git", ["push", remote ?? "origin", "--follow-tags"], gitCwd);
28766
- } catch (err) {
28767
- const execError = err;
28768
- const msg = (execError.stderr ?? execError.message ?? "").slice(0, 500);
28769
- return engineError("E_GENERAL", `git push failed: ${msg}`, {
28770
- details: { exitCode: execError.status }
29606
+ logStep(6, 8, "Tag release", true);
29607
+ logStep(7, 8, "Push / create PR");
29608
+ let prResult = null;
29609
+ const pushResult = await pushRelease(version, remote, projectRoot, {
29610
+ explicitPush: true,
29611
+ mode: pushMode
29612
+ });
29613
+ if (pushResult.requiresPR || requiresPRFromGates) {
29614
+ const prBody = buildPRBody({
29615
+ base: targetBranch,
29616
+ head: currentBranchForPR,
29617
+ title: `release: ship v${version}`,
29618
+ body: "",
29619
+ version,
29620
+ epicId,
29621
+ projectRoot: cwd
29622
+ });
29623
+ prResult = await createPullRequest({
29624
+ base: targetBranch,
29625
+ head: currentBranchForPR,
29626
+ title: `release: ship v${version}`,
29627
+ body: prBody,
29628
+ labels: ["release", resolvedChannel],
29629
+ version,
29630
+ epicId,
29631
+ projectRoot: cwd
28771
29632
  });
29633
+ if (prResult.mode === "created") {
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);
29641
+ } else if (prResult.mode === "skipped") {
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);
29647
+ } else {
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);
29653
+ }
29654
+ } else {
29655
+ try {
29656
+ execFileSync8("git", ["push", remote ?? "origin", "--follow-tags"], gitCwd);
29657
+ logStep(7, 8, "Push / create PR", true);
29658
+ } catch (err) {
29659
+ const execError = err;
29660
+ const msg = (execError.stderr ?? execError.message ?? "").slice(0, 500);
29661
+ logStep(7, 8, "Push / create PR", false, `git push failed: ${msg}`);
29662
+ return engineError("E_GENERAL", `git push failed: ${msg}`, {
29663
+ details: { exitCode: execError.status }
29664
+ });
29665
+ }
28772
29666
  }
28773
29667
  const pushedAt = (/* @__PURE__ */ new Date()).toISOString();
28774
29668
  await markReleasePushed(version, pushedAt, projectRoot, { commitSha, gitTag });
@@ -28780,7 +29674,17 @@ async function releaseShip(params, projectRoot) {
28780
29674
  commitSha,
28781
29675
  gitTag,
28782
29676
  pushedAt,
28783
- changelog: changelogPath
29677
+ changelog: changelogPath,
29678
+ channel: resolvedChannel,
29679
+ steps,
29680
+ ...prResult ? {
29681
+ pr: {
29682
+ mode: prResult.mode,
29683
+ prUrl: prResult.prUrl,
29684
+ prNumber: prResult.prNumber,
29685
+ instructions: prResult.instructions
29686
+ }
29687
+ } : {}
28784
29688
  }
28785
29689
  };
28786
29690
  } catch (err) {
@@ -28793,15 +29697,18 @@ var init_release_engine = __esm({
28793
29697
  init_platform();
28794
29698
  init_data_accessor();
28795
29699
  init_release_manifest();
28796
- init_changelog_writer();
28797
29700
  init_guards();
29701
+ init_github_pr();
29702
+ init_channel();
29703
+ init_release_config();
29704
+ init_version_bump();
28798
29705
  init_error();
28799
29706
  }
28800
29707
  });
28801
29708
 
28802
29709
  // src/dispatch/engines/template-parser.ts
28803
- import { readFileSync as readFileSync38, readdirSync as readdirSync12, existsSync as existsSync50 } from "fs";
28804
- import { join as join48 } from "path";
29710
+ import { readFileSync as readFileSync40, readdirSync as readdirSync12, existsSync as existsSync52 } from "fs";
29711
+ import { join as join50 } from "path";
28805
29712
  import { parse as parseYaml } from "yaml";
28806
29713
  function deriveSubcommand(filename) {
28807
29714
  let stem = filename.replace(/\.ya?ml$/i, "");
@@ -28815,8 +29722,8 @@ function deriveSubcommand(filename) {
28815
29722
  return firstWord.toLowerCase();
28816
29723
  }
28817
29724
  function parseTemplateFile(templateDir, filename) {
28818
- const filePath = join48(templateDir, filename);
28819
- const raw = readFileSync38(filePath, "utf-8");
29725
+ const filePath = join50(templateDir, filename);
29726
+ const raw = readFileSync40(filePath, "utf-8");
28820
29727
  const parsed = parseYaml(raw);
28821
29728
  const name = typeof parsed.name === "string" ? parsed.name : filename;
28822
29729
  const titlePrefix = typeof parsed.title === "string" ? parsed.title : "";
@@ -28865,8 +29772,8 @@ function parseTemplateFile(templateDir, filename) {
28865
29772
  };
28866
29773
  }
28867
29774
  function parseIssueTemplates(projectRoot) {
28868
- const templateDir = join48(projectRoot, ".github", "ISSUE_TEMPLATE");
28869
- if (!existsSync50(templateDir)) {
29775
+ const templateDir = join50(projectRoot, ".github", "ISSUE_TEMPLATE");
29776
+ if (!existsSync52(templateDir)) {
28870
29777
  return engineError("E_NOT_FOUND", `Issue template directory not found: ${templateDir}`);
28871
29778
  }
28872
29779
  let files;
@@ -30545,26 +31452,26 @@ var init_check = __esm({
30545
31452
  });
30546
31453
 
30547
31454
  // src/core/adrs/validate.ts
30548
- import { readFileSync as readFileSync39, readdirSync as readdirSync13, existsSync as existsSync51 } from "node:fs";
30549
- import { join as join49 } 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";
30550
31457
  import AjvModule3 from "ajv";
30551
31458
  async function validateAllAdrs(projectRoot) {
30552
- const adrsDir = join49(projectRoot, ".cleo", "adrs");
30553
- const schemaPath = join49(projectRoot, "schemas", "adr-frontmatter.schema.json");
30554
- if (!existsSync51(schemaPath)) {
31459
+ const adrsDir = join51(projectRoot, ".cleo", "adrs");
31460
+ const schemaPath = join51(projectRoot, "schemas", "adr-frontmatter.schema.json");
31461
+ if (!existsSync53(schemaPath)) {
30555
31462
  return {
30556
31463
  valid: false,
30557
31464
  errors: [{ file: "schemas/adr-frontmatter.schema.json", field: "schema", message: "Schema file not found" }],
30558
31465
  checked: 0
30559
31466
  };
30560
31467
  }
30561
- if (!existsSync51(adrsDir)) {
31468
+ if (!existsSync53(adrsDir)) {
30562
31469
  return { valid: true, errors: [], checked: 0 };
30563
31470
  }
30564
- const schema = JSON.parse(readFileSync39(schemaPath, "utf-8"));
31471
+ const schema = JSON.parse(readFileSync41(schemaPath, "utf-8"));
30565
31472
  const ajv = new Ajv3({ allErrors: true });
30566
31473
  const validate = ajv.compile(schema);
30567
- const files = readdirSync13(adrsDir).filter((f) => f.endsWith(".md") && f.startsWith("ADR-")).map((f) => join49(adrsDir, f));
31474
+ const files = readdirSync13(adrsDir).filter((f) => f.endsWith(".md") && f.startsWith("ADR-")).map((f) => join51(adrsDir, f));
30568
31475
  const errors = [];
30569
31476
  for (const filePath of files) {
30570
31477
  const record = parseAdrFile(filePath, projectRoot);
@@ -30591,15 +31498,15 @@ var init_validate = __esm({
30591
31498
  });
30592
31499
 
30593
31500
  // src/core/adrs/list.ts
30594
- import { readdirSync as readdirSync14, existsSync as existsSync52 } from "node:fs";
30595
- import { join as join50 } from "node:path";
31501
+ import { readdirSync as readdirSync14, existsSync as existsSync54 } from "node:fs";
31502
+ import { join as join52 } from "node:path";
30596
31503
  async function listAdrs(projectRoot, opts) {
30597
- const adrsDir = join50(projectRoot, ".cleo", "adrs");
30598
- if (!existsSync52(adrsDir)) {
31504
+ const adrsDir = join52(projectRoot, ".cleo", "adrs");
31505
+ if (!existsSync54(adrsDir)) {
30599
31506
  return { adrs: [], total: 0 };
30600
31507
  }
30601
31508
  const files = readdirSync14(adrsDir).filter((f) => f.endsWith(".md") && f.startsWith("ADR-")).sort();
30602
- const records = files.map((f) => parseAdrFile(join50(adrsDir, f), projectRoot));
31509
+ const records = files.map((f) => parseAdrFile(join52(adrsDir, f), projectRoot));
30603
31510
  const filtered = records.filter((r) => {
30604
31511
  if (opts?.status && r.frontmatter.Status !== opts.status) return false;
30605
31512
  if (opts?.since && r.frontmatter.Date < opts.since) return false;
@@ -30624,14 +31531,14 @@ var init_list2 = __esm({
30624
31531
  });
30625
31532
 
30626
31533
  // src/core/adrs/show.ts
30627
- import { existsSync as existsSync53, readdirSync as readdirSync15 } from "node:fs";
30628
- import { join as join51 } from "node:path";
31534
+ import { existsSync as existsSync55, readdirSync as readdirSync15 } from "node:fs";
31535
+ import { join as join53 } from "node:path";
30629
31536
  async function showAdr(projectRoot, adrId) {
30630
- const adrsDir = join51(projectRoot, ".cleo", "adrs");
30631
- if (!existsSync53(adrsDir)) return null;
31537
+ const adrsDir = join53(projectRoot, ".cleo", "adrs");
31538
+ if (!existsSync55(adrsDir)) return null;
30632
31539
  const files = readdirSync15(adrsDir).filter((f) => f.startsWith(adrId) && f.endsWith(".md"));
30633
31540
  if (files.length === 0) return null;
30634
- const filePath = join51(adrsDir, files[0]);
31541
+ const filePath = join53(adrsDir, files[0]);
30635
31542
  return parseAdrFile(filePath, projectRoot);
30636
31543
  }
30637
31544
  var init_show2 = __esm({
@@ -30642,8 +31549,8 @@ var init_show2 = __esm({
30642
31549
  });
30643
31550
 
30644
31551
  // src/core/adrs/find.ts
30645
- import { readdirSync as readdirSync16, existsSync as existsSync54 } from "node:fs";
30646
- import { join as join52 } from "node:path";
31552
+ import { readdirSync as readdirSync16, existsSync as existsSync56 } from "node:fs";
31553
+ import { join as join54 } from "node:path";
30647
31554
  function normalise(s) {
30648
31555
  return s.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
30649
31556
  }
@@ -30660,8 +31567,8 @@ function matchedTerms(target, terms) {
30660
31567
  return terms.filter((term) => t.includes(term));
30661
31568
  }
30662
31569
  async function findAdrs(projectRoot, query, opts) {
30663
- const adrsDir = join52(projectRoot, ".cleo", "adrs");
30664
- if (!existsSync54(adrsDir)) {
31570
+ const adrsDir = join54(projectRoot, ".cleo", "adrs");
31571
+ if (!existsSync56(adrsDir)) {
30665
31572
  return { adrs: [], query, total: 0 };
30666
31573
  }
30667
31574
  const files = readdirSync16(adrsDir).filter((f) => f.endsWith(".md") && f.startsWith("ADR-")).sort();
@@ -30670,7 +31577,7 @@ async function findAdrs(projectRoot, query, opts) {
30670
31577
  const filterKeywords = opts?.keywords ? parseTags(opts.keywords) : null;
30671
31578
  const results = [];
30672
31579
  for (const file of files) {
30673
- const record = parseAdrFile(join52(adrsDir, file), projectRoot);
31580
+ const record = parseAdrFile(join54(adrsDir, file), projectRoot);
30674
31581
  const fm = record.frontmatter;
30675
31582
  if (opts?.status && fm.Status !== opts.status) continue;
30676
31583
  if (filterTopics && filterTopics.length > 0) {
@@ -30748,12 +31655,12 @@ var init_adrs = __esm({
30748
31655
  });
30749
31656
 
30750
31657
  // src/core/admin/sync.ts
30751
- import { join as join53 } from "node:path";
31658
+ import { join as join55 } from "node:path";
30752
31659
  import { rm as rm2, rmdir, stat as stat2 } from "node:fs/promises";
30753
31660
  async function getSyncStatus(projectRoot) {
30754
31661
  try {
30755
31662
  const cleoDir = getCleoDir(projectRoot);
30756
- const stateFile = join53(cleoDir, "sync", "todowrite-session.json");
31663
+ const stateFile = join55(cleoDir, "sync", "todowrite-session.json");
30757
31664
  const sessionState = await readJson(stateFile);
30758
31665
  if (!sessionState) {
30759
31666
  return {
@@ -30797,8 +31704,8 @@ async function getSyncStatus(projectRoot) {
30797
31704
  async function clearSyncState(projectRoot, dryRun) {
30798
31705
  try {
30799
31706
  const cleoDir = getCleoDir(projectRoot);
30800
- const syncDir = join53(cleoDir, "sync");
30801
- const stateFile = join53(syncDir, "todowrite-session.json");
31707
+ const syncDir = join55(cleoDir, "sync");
31708
+ const stateFile = join55(syncDir, "todowrite-session.json");
30802
31709
  let exists = false;
30803
31710
  try {
30804
31711
  await stat2(stateFile);
@@ -31565,8 +32472,8 @@ var init_import_tasks = __esm({
31565
32472
  // src/core/snapshot/index.ts
31566
32473
  import { createHash as createHash8 } from "node:crypto";
31567
32474
  import { readFile as readFile13, writeFile as writeFile10, mkdir as mkdir10 } from "node:fs/promises";
31568
- import { existsSync as existsSync55 } from "node:fs";
31569
- import { join as join54, 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";
31570
32477
  function toSnapshotTask(task) {
31571
32478
  return {
31572
32479
  id: task.id,
@@ -31619,7 +32526,7 @@ async function exportSnapshot(cwd) {
31619
32526
  }
31620
32527
  async function writeSnapshot(snapshot, outputPath) {
31621
32528
  const dir = dirname14(outputPath);
31622
- if (!existsSync55(dir)) {
32529
+ if (!existsSync57(dir)) {
31623
32530
  await mkdir10(dir, { recursive: true });
31624
32531
  }
31625
32532
  await writeFile10(outputPath, JSON.stringify(snapshot, null, 2) + "\n");
@@ -31635,7 +32542,7 @@ async function readSnapshot(inputPath) {
31635
32542
  function getDefaultSnapshotPath(cwd) {
31636
32543
  const cleoDir = getCleoDirAbsolute(cwd);
31637
32544
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
31638
- return join54(cleoDir, "snapshots", `snapshot-${timestamp}.json`);
32545
+ return join56(cleoDir, "snapshots", `snapshot-${timestamp}.json`);
31639
32546
  }
31640
32547
  async function importSnapshot(snapshot, cwd) {
31641
32548
  const accessor = await getAccessor(cwd);
@@ -31767,40 +32674,49 @@ var init_session_resolver = __esm({
31767
32674
  }
31768
32675
  });
31769
32676
 
32677
+ // src/core/tasks/id-generator.ts
32678
+ function normalizeTaskId(input) {
32679
+ if (typeof input !== "string") return null;
32680
+ const trimmed = input.trim();
32681
+ if (trimmed === "") return null;
32682
+ const match = trimmed.match(/^[Tt]?(\d+)(?:_.*)?$/);
32683
+ if (!match) return null;
32684
+ return `T${match[1]}`;
32685
+ }
32686
+ var init_id_generator = __esm({
32687
+ "src/core/tasks/id-generator.ts"() {
32688
+ "use strict";
32689
+ init_data_accessor();
32690
+ }
32691
+ });
32692
+
31770
32693
  // src/dispatch/lib/security.ts
31771
32694
  import { resolve as resolve8, normalize, relative as relative4, isAbsolute as isAbsolute2 } from "path";
31772
- function sanitizeTaskId(id) {
31773
- if (typeof id !== "string") {
32695
+ function sanitizeTaskId(value) {
32696
+ if (typeof value !== "string") {
31774
32697
  throw new SecurityError(
31775
32698
  "Task ID must be a string",
31776
32699
  "E_INVALID_TASK_ID",
31777
32700
  "taskId"
31778
32701
  );
31779
32702
  }
31780
- const trimmed = id.trim();
31781
- if (trimmed.length === 0) {
31782
- throw new SecurityError(
31783
- "Task ID cannot be empty",
31784
- "E_INVALID_TASK_ID",
31785
- "taskId"
31786
- );
31787
- }
31788
- if (!TASK_ID_PATTERN.test(trimmed)) {
32703
+ const normalized = normalizeTaskId(value);
32704
+ if (normalized === null) {
31789
32705
  throw new SecurityError(
31790
- `Invalid task ID format: "${trimmed}". Must match pattern T[0-9]+ (e.g., T123)`,
32706
+ `Invalid task ID format: ${value}`,
31791
32707
  "E_INVALID_TASK_ID",
31792
32708
  "taskId"
31793
32709
  );
31794
32710
  }
31795
- const numericPart = parseInt(trimmed.slice(1), 10);
32711
+ const numericPart = parseInt(normalized.slice(1), 10);
31796
32712
  if (numericPart > MAX_TASK_ID_NUMBER) {
31797
32713
  throw new SecurityError(
31798
- `Task ID numeric value exceeds maximum (${MAX_TASK_ID_NUMBER}): ${trimmed}`,
32714
+ `Task ID exceeds maximum value: ${value}`,
31799
32715
  "E_INVALID_TASK_ID",
31800
32716
  "taskId"
31801
32717
  );
31802
32718
  }
31803
- return trimmed;
32719
+ return normalized;
31804
32720
  }
31805
32721
  function sanitizePath(path, projectRoot) {
31806
32722
  if (typeof path !== "string") {
@@ -31899,14 +32815,14 @@ function sanitizeParams(params, projectRoot, context) {
31899
32815
  if (value === void 0 || value === null) {
31900
32816
  continue;
31901
32817
  }
31902
- if (typeof value === "string" && (key === "taskId" || key === "parent" || key === "epicId")) {
32818
+ if (typeof value === "string" && (key === "taskId" || key === "parent" || key === "epicId" || key === "parentId" || key === "newParentId" || key === "relatedId" || key === "targetId")) {
31903
32819
  if (key === "parent" && value === "") {
31904
32820
  continue;
31905
32821
  }
31906
32822
  sanitized[key] = sanitizeTaskId(value);
31907
32823
  continue;
31908
32824
  }
31909
- if (key === "depends" && Array.isArray(value)) {
32825
+ if ((key === "depends" || key === "addDepends" || key === "removeDepends") && Array.isArray(value)) {
31910
32826
  sanitized[key] = value.map((v) => {
31911
32827
  if (typeof v === "string") {
31912
32828
  return sanitizeTaskId(v);
@@ -31956,10 +32872,11 @@ function sanitizeParams(params, projectRoot, context) {
31956
32872
  }
31957
32873
  return sanitized;
31958
32874
  }
31959
- var SecurityError, TASK_ID_PATTERN, MAX_TASK_ID_NUMBER, CONTROL_CHAR_PATTERN, DEFAULT_MAX_CONTENT_LENGTH, ALL_VALID_STATUSES, VALID_PRIORITIES2, ARRAY_PARAMS;
32875
+ var SecurityError, MAX_TASK_ID_NUMBER, CONTROL_CHAR_PATTERN, DEFAULT_MAX_CONTENT_LENGTH, ALL_VALID_STATUSES, VALID_PRIORITIES2, ARRAY_PARAMS;
31960
32876
  var init_security = __esm({
31961
32877
  "src/dispatch/lib/security.ts"() {
31962
32878
  "use strict";
32879
+ init_id_generator();
31963
32880
  init_schema();
31964
32881
  init_status_registry();
31965
32882
  SecurityError = class extends Error {
@@ -31970,7 +32887,6 @@ var init_security = __esm({
31970
32887
  this.name = "SecurityError";
31971
32888
  }
31972
32889
  };
31973
- TASK_ID_PATTERN = /^T[0-9]+$/;
31974
32890
  MAX_TASK_ID_NUMBER = 999999;
31975
32891
  CONTROL_CHAR_PATTERN = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g;
31976
32892
  DEFAULT_MAX_CONTENT_LENGTH = 64 * 1024;
@@ -32069,15 +32985,15 @@ var init_field_filter = __esm({
32069
32985
  });
32070
32986
 
32071
32987
  // src/core/project-info.ts
32072
- import { readFileSync as readFileSync40, existsSync as existsSync56 } from "node:fs";
32073
- import { join as join55 } from "node:path";
32988
+ import { readFileSync as readFileSync42, existsSync as existsSync58 } from "node:fs";
32989
+ import { join as join57 } from "node:path";
32074
32990
  function getProjectInfoSync(cwd) {
32075
32991
  const projectRoot = cwd ?? process.cwd();
32076
32992
  const cleoDir = getCleoDirAbsolute(projectRoot);
32077
- const infoPath = join55(cleoDir, "project-info.json");
32078
- if (!existsSync56(infoPath)) return null;
32993
+ const infoPath = join57(cleoDir, "project-info.json");
32994
+ if (!existsSync58(infoPath)) return null;
32079
32995
  try {
32080
- const raw = readFileSync40(infoPath, "utf-8");
32996
+ const raw = readFileSync42(infoPath, "utf-8");
32081
32997
  const data = JSON.parse(raw);
32082
32998
  if (typeof data.projectHash !== "string" || data.projectHash.length === 0) {
32083
32999
  return null;
@@ -32305,13 +33221,13 @@ var init_field_context = __esm({
32305
33221
  });
32306
33222
 
32307
33223
  // src/core/sessions/context-alert.ts
32308
- import { existsSync as existsSync57, readFileSync as readFileSync41, writeFileSync as writeFileSync8 } from "node:fs";
32309
- import { join as join56 } 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";
32310
33226
  function getCurrentSessionId(cwd) {
32311
33227
  if (process.env.CLEO_SESSION) return process.env.CLEO_SESSION;
32312
- const sessionFile = join56(getCleoDir(cwd), ".current-session");
32313
- if (existsSync57(sessionFile)) {
32314
- return readFileSync41(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;
32315
33231
  }
32316
33232
  return null;
32317
33233
  }
@@ -33359,7 +34275,7 @@ async function stopTask2(sessionId, cwd) {
33359
34275
  }
33360
34276
  async function workHistory(sessionId, limit = 50, cwd) {
33361
34277
  const db = await getDb(cwd);
33362
- 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();
33363
34279
  return rows.map((r) => ({
33364
34280
  taskId: r.taskId,
33365
34281
  setAt: r.setAt,
@@ -33712,8 +34628,8 @@ __export(session_grade_exports, {
33712
34628
  gradeSession: () => gradeSession,
33713
34629
  readGrades: () => readGrades
33714
34630
  });
33715
- import { join as join57 } from "node:path";
33716
- import { existsSync as existsSync58 } from "node:fs";
34631
+ import { join as join59 } from "node:path";
34632
+ import { existsSync as existsSync60 } from "node:fs";
33717
34633
  import { readFile as readFile14, appendFile, mkdir as mkdir11 } from "node:fs/promises";
33718
34634
  async function gradeSession(sessionId, cwd) {
33719
34635
  const sessionEntries = await queryAudit({ sessionId });
@@ -33893,9 +34809,9 @@ function detectDuplicateCreates(entries) {
33893
34809
  async function appendGradeResult(result, cwd) {
33894
34810
  try {
33895
34811
  const cleoDir = getCleoDirAbsolute(cwd);
33896
- const metricsDir = join57(cleoDir, "metrics");
34812
+ const metricsDir = join59(cleoDir, "metrics");
33897
34813
  await mkdir11(metricsDir, { recursive: true });
33898
- const gradesPath = join57(metricsDir, "GRADES.jsonl");
34814
+ const gradesPath = join59(metricsDir, "GRADES.jsonl");
33899
34815
  const line = JSON.stringify({ ...result, evaluator: "auto" }) + "\n";
33900
34816
  await appendFile(gradesPath, line, "utf8");
33901
34817
  } catch {
@@ -33904,8 +34820,8 @@ async function appendGradeResult(result, cwd) {
33904
34820
  async function readGrades(sessionId, cwd) {
33905
34821
  try {
33906
34822
  const cleoDir = getCleoDirAbsolute(cwd);
33907
- const gradesPath = join57(cleoDir, "metrics", "GRADES.jsonl");
33908
- if (!existsSync58(gradesPath)) return [];
34823
+ const gradesPath = join59(cleoDir, "metrics", "GRADES.jsonl");
34824
+ if (!existsSync60(gradesPath)) return [];
33909
34825
  const content = await readFile14(gradesPath, "utf8");
33910
34826
  const results = content.split("\n").filter((l) => l.trim()).map((l) => JSON.parse(l));
33911
34827
  return sessionId ? results.filter((r) => r.sessionId === sessionId) : results;
@@ -35113,7 +36029,7 @@ function validateVariableType(name, varDef, value) {
35113
36029
  if (typeof value !== "string") {
35114
36030
  throw new Error(`Invalid variable type for "${name}": expected ${varDef.type}, got ${valueType(value)}`);
35115
36031
  }
35116
- if (!TASK_ID_PATTERN2.test(value)) {
36032
+ if (!TASK_ID_PATTERN.test(value)) {
35117
36033
  throw new Error(`Invalid variable format for "${name}": expected ${varDef.type} like "T1234", got "${value}"`);
35118
36034
  }
35119
36035
  return;
@@ -35271,7 +36187,7 @@ function showTessera(id) {
35271
36187
  ensureDefaults();
35272
36188
  return templates.get(id) ?? null;
35273
36189
  }
35274
- var DEFAULT_TESSERA_ID, TASK_ID_PATTERN2, PLACEHOLDER_EXACT, PLACEHOLDER_GLOBAL, templates;
36190
+ var DEFAULT_TESSERA_ID, TASK_ID_PATTERN, PLACEHOLDER_EXACT, PLACEHOLDER_GLOBAL, templates;
35275
36191
  var init_tessera_engine = __esm({
35276
36192
  "src/core/lifecycle/tessera-engine.ts"() {
35277
36193
  "use strict";
@@ -35279,7 +36195,7 @@ var init_tessera_engine = __esm({
35279
36195
  init_chain_validation();
35280
36196
  init_chain_store();
35281
36197
  DEFAULT_TESSERA_ID = "tessera-rcasd";
35282
- TASK_ID_PATTERN2 = /^T\d+$/;
36198
+ TASK_ID_PATTERN = /^T\d+$/;
35283
36199
  PLACEHOLDER_EXACT = /^\{\{\s*([A-Za-z0-9_]+)\s*\}\}$/;
35284
36200
  PLACEHOLDER_GLOBAL = /\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g;
35285
36201
  templates = /* @__PURE__ */ new Map();
@@ -36142,6 +37058,7 @@ var init_phase = __esm({
36142
37058
  });
36143
37059
 
36144
37060
  // src/dispatch/domains/pipeline.ts
37061
+ import { execFileSync as execFileSync9 } from "node:child_process";
36145
37062
  var PipelineHandler;
36146
37063
  var init_pipeline2 = __esm({
36147
37064
  "src/dispatch/domains/pipeline.ts"() {
@@ -36152,6 +37069,7 @@ var init_pipeline2 = __esm({
36152
37069
  init_data_accessor();
36153
37070
  init_engine();
36154
37071
  init_release_engine();
37072
+ init_channel();
36155
37073
  init_phase();
36156
37074
  init_phases();
36157
37075
  init_pipeline_manifest_sqlite();
@@ -36237,6 +37155,7 @@ var init_pipeline2 = __esm({
36237
37155
  "manifest.stats",
36238
37156
  "release.list",
36239
37157
  "release.show",
37158
+ "release.channel.show",
36240
37159
  "phase.show",
36241
37160
  "phase.list",
36242
37161
  "chain.show",
@@ -36256,6 +37175,7 @@ var init_pipeline2 = __esm({
36256
37175
  "release.push",
36257
37176
  "release.gates.run",
36258
37177
  "release.rollback",
37178
+ "release.cancel",
36259
37179
  "release.ship",
36260
37180
  "manifest.append",
36261
37181
  "manifest.archive",
@@ -36479,6 +37399,29 @@ var init_pipeline2 = __esm({
36479
37399
  const result = await releaseShow(version, this.projectRoot);
36480
37400
  return this.wrapEngineResult(result, "query", "release.show", startTime);
36481
37401
  }
37402
+ case "channel.show": {
37403
+ let currentBranch = "unknown";
37404
+ try {
37405
+ currentBranch = execFileSync9("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
37406
+ encoding: "utf-8",
37407
+ stdio: "pipe",
37408
+ cwd: this.projectRoot
37409
+ }).trim();
37410
+ } catch {
37411
+ }
37412
+ const resolvedChannel = resolveChannelFromBranch(currentBranch);
37413
+ const distTag = channelToDistTag(resolvedChannel);
37414
+ const description = describeChannel(resolvedChannel);
37415
+ return this.wrapEngineResult({
37416
+ success: true,
37417
+ data: {
37418
+ branch: currentBranch,
37419
+ channel: resolvedChannel,
37420
+ distTag,
37421
+ description
37422
+ }
37423
+ }, "query", "release.channel.show", startTime);
37424
+ }
36482
37425
  default:
36483
37426
  return this.errorResponse(
36484
37427
  "query",
@@ -36597,6 +37540,20 @@ var init_pipeline2 = __esm({
36597
37540
  const result = await releaseRollback(version, reason, this.projectRoot);
36598
37541
  return this.wrapEngineResult(result, "mutate", "release.rollback", startTime);
36599
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
+ }
36600
37557
  case "ship": {
36601
37558
  const version = params?.version;
36602
37559
  const epicId = params?.epicId;
@@ -36611,8 +37568,9 @@ var init_pipeline2 = __esm({
36611
37568
  }
36612
37569
  const remote = params?.remote;
36613
37570
  const dryRun = params?.dryRun;
37571
+ const bump = params?.bump;
36614
37572
  const result = await releaseShip(
36615
- { version, epicId, remote, dryRun },
37573
+ { version, epicId, remote, dryRun, bump },
36616
37574
  this.projectRoot
36617
37575
  );
36618
37576
  return this.wrapEngineResult(result, "mutate", "release.ship", startTime);
@@ -37083,11 +38041,11 @@ var init_pipeline2 = __esm({
37083
38041
  });
37084
38042
 
37085
38043
  // src/core/issue/diagnostics.ts
37086
- import { execFileSync as execFileSync8 } from "node:child_process";
38044
+ import { execFileSync as execFileSync10 } from "node:child_process";
37087
38045
  function collectDiagnostics() {
37088
38046
  const getVersion3 = (cmd, args) => {
37089
38047
  try {
37090
- return execFileSync8(cmd, args, {
38048
+ return execFileSync10(cmd, args, {
37091
38049
  encoding: "utf-8",
37092
38050
  stdio: ["pipe", "pipe", "pipe"]
37093
38051
  }).trim();
@@ -37138,7 +38096,7 @@ var init_build_config = __esm({
37138
38096
  "use strict";
37139
38097
  BUILD_CONFIG = {
37140
38098
  "name": "@cleocode/cleo",
37141
- "version": "2026.3.16",
38099
+ "version": "2026.3.18",
37142
38100
  "description": "CLEO V2 - TypeScript task management CLI for AI coding agents",
37143
38101
  "repository": {
37144
38102
  "owner": "kryptobaseddev",
@@ -37147,7 +38105,7 @@ var init_build_config = __esm({
37147
38105
  "url": "https://github.com/kryptobaseddev/cleo.git",
37148
38106
  "issuesUrl": "https://github.com/kryptobaseddev/cleo/issues"
37149
38107
  },
37150
- "buildDate": "2026-03-07T05:25:13.762Z",
38108
+ "buildDate": "2026-03-07T20:24:31.815Z",
37151
38109
  "templates": {
37152
38110
  "issueTemplatesDir": "templates/issue-templates"
37153
38111
  }
@@ -37156,8 +38114,8 @@ var init_build_config = __esm({
37156
38114
  });
37157
38115
 
37158
38116
  // src/core/issue/template-parser.ts
37159
- import { existsSync as existsSync59, readFileSync as readFileSync42, readdirSync as readdirSync17, writeFileSync as writeFileSync9 } from "node:fs";
37160
- import { join as join58, 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";
37161
38119
  function extractYamlField(content, field) {
37162
38120
  const regex = new RegExp(`^${field}:\\s*["']?(.+?)["']?\\s*$`, "m");
37163
38121
  const match = content.match(regex);
@@ -37189,9 +38147,9 @@ function extractYamlArray(content, field) {
37189
38147
  return items;
37190
38148
  }
37191
38149
  function parseTemplateFile2(filePath) {
37192
- if (!existsSync59(filePath)) return null;
38150
+ if (!existsSync61(filePath)) return null;
37193
38151
  try {
37194
- const content = readFileSync42(filePath, "utf-8");
38152
+ const content = readFileSync44(filePath, "utf-8");
37195
38153
  const fileName = basename10(filePath);
37196
38154
  const stem = fileName.replace(/\.ya?ml$/, "");
37197
38155
  const name = extractYamlField(content, "name");
@@ -37208,12 +38166,12 @@ function parseTemplateFile2(filePath) {
37208
38166
  function parseIssueTemplates2(projectDir) {
37209
38167
  try {
37210
38168
  const packageRoot = getPackageRoot();
37211
- const packagedTemplateDir = join58(packageRoot, PACKAGED_TEMPLATE_DIR);
37212
- if (existsSync59(packagedTemplateDir)) {
38169
+ const packagedTemplateDir = join60(packageRoot, PACKAGED_TEMPLATE_DIR);
38170
+ if (existsSync61(packagedTemplateDir)) {
37213
38171
  const templates3 = [];
37214
38172
  for (const file of readdirSync17(packagedTemplateDir)) {
37215
38173
  if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
37216
- const template = parseTemplateFile2(join58(packagedTemplateDir, file));
38174
+ const template = parseTemplateFile2(join60(packagedTemplateDir, file));
37217
38175
  if (template) templates3.push(template);
37218
38176
  }
37219
38177
  if (templates3.length > 0) return templates3;
@@ -37221,12 +38179,12 @@ function parseIssueTemplates2(projectDir) {
37221
38179
  } catch {
37222
38180
  }
37223
38181
  const dir = projectDir ?? getProjectRoot();
37224
- const templateDir = join58(dir, TEMPLATE_DIR);
37225
- if (!existsSync59(templateDir)) return [];
38182
+ const templateDir = join60(dir, TEMPLATE_DIR);
38183
+ if (!existsSync61(templateDir)) return [];
37226
38184
  const templates2 = [];
37227
38185
  for (const file of readdirSync17(templateDir)) {
37228
38186
  if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
37229
- const template = parseTemplateFile2(join58(templateDir, file));
38187
+ const template = parseTemplateFile2(join60(templateDir, file));
37230
38188
  if (template) templates2.push(template);
37231
38189
  }
37232
38190
  return templates2;
@@ -37235,10 +38193,10 @@ function getTemplateConfig(cwd) {
37235
38193
  const projectDir = cwd ?? getProjectRoot();
37236
38194
  const liveTemplates = parseIssueTemplates2(projectDir);
37237
38195
  if (liveTemplates.length > 0) return liveTemplates;
37238
- const cachePath = join58(getCleoDir(cwd), CACHE_FILE);
37239
- if (existsSync59(cachePath)) {
38196
+ const cachePath = join60(getCleoDir(cwd), CACHE_FILE);
38197
+ if (existsSync61(cachePath)) {
37240
38198
  try {
37241
- const cached = JSON.parse(readFileSync42(cachePath, "utf-8"));
38199
+ const cached = JSON.parse(readFileSync44(cachePath, "utf-8"));
37242
38200
  if (cached.templates?.length > 0) return cached.templates;
37243
38201
  } catch {
37244
38202
  }
@@ -37294,7 +38252,7 @@ var init_template_parser2 = __esm({
37294
38252
  });
37295
38253
 
37296
38254
  // src/core/issue/create.ts
37297
- import { execFileSync as execFileSync9 } from "node:child_process";
38255
+ import { execFileSync as execFileSync11 } from "node:child_process";
37298
38256
  function buildIssueBody(subcommand, rawBody, severity, area) {
37299
38257
  const template = getTemplateForSubcommand2(subcommand);
37300
38258
  const sectionLabel = template?.name ?? "Description";
@@ -37313,7 +38271,7 @@ function buildIssueBody(subcommand, rawBody, severity, area) {
37313
38271
  }
37314
38272
  function checkGhCli() {
37315
38273
  try {
37316
- execFileSync9("gh", ["--version"], {
38274
+ execFileSync11("gh", ["--version"], {
37317
38275
  encoding: "utf-8",
37318
38276
  stdio: ["pipe", "pipe", "pipe"]
37319
38277
  });
@@ -37323,7 +38281,7 @@ function checkGhCli() {
37323
38281
  });
37324
38282
  }
37325
38283
  try {
37326
- execFileSync9("gh", ["auth", "status", "--hostname", "github.com"], {
38284
+ execFileSync11("gh", ["auth", "status", "--hostname", "github.com"], {
37327
38285
  encoding: "utf-8",
37328
38286
  stdio: ["pipe", "pipe", "pipe"]
37329
38287
  });
@@ -37335,7 +38293,7 @@ function checkGhCli() {
37335
38293
  }
37336
38294
  function addGhIssue(title, body, labels) {
37337
38295
  try {
37338
- const result = execFileSync9("gh", [
38296
+ const result = execFileSync11("gh", [
37339
38297
  "issue",
37340
38298
  "create",
37341
38299
  "--repo",
@@ -38297,8 +39255,8 @@ var init_tools = __esm({
38297
39255
  });
38298
39256
 
38299
39257
  // src/core/nexus/query.ts
38300
- import { join as join59, basename as basename11 } from "node:path";
38301
- import { existsSync as existsSync60, readFileSync as readFileSync43 } 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";
38302
39260
  import { z as z3 } from "zod";
38303
39261
  function validateSyntax(query) {
38304
39262
  if (!query) return false;
@@ -38334,9 +39292,9 @@ function getCurrentProject() {
38334
39292
  return process.env["NEXUS_CURRENT_PROJECT"];
38335
39293
  }
38336
39294
  try {
38337
- const infoPath = join59(process.cwd(), ".cleo", "project-info.json");
38338
- if (existsSync60(infoPath)) {
38339
- const data = JSON.parse(readFileSync43(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"));
38340
39298
  if (typeof data.name === "string" && data.name.length > 0) {
38341
39299
  return data.name;
38342
39300
  }
@@ -38372,7 +39330,7 @@ async function resolveProjectPath2(projectName) {
38372
39330
  return project.path;
38373
39331
  }
38374
39332
  async function readProjectTasks(projectPath) {
38375
- const tasksDbPath = join59(projectPath, ".cleo", "tasks.db");
39333
+ const tasksDbPath = join61(projectPath, ".cleo", "tasks.db");
38376
39334
  try {
38377
39335
  const accessor = await getAccessor(projectPath);
38378
39336
  const taskFile = await accessor.loadTaskFile();
@@ -38786,8 +39744,8 @@ var init_deps2 = __esm({
38786
39744
 
38787
39745
  // src/core/nexus/sharing/index.ts
38788
39746
  import { readFile as readFile15, writeFile as writeFile11 } from "node:fs/promises";
38789
- import { existsSync as existsSync61, readdirSync as readdirSync18, statSync as statSync9 } from "node:fs";
38790
- import { join as join60, 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";
38791
39749
  function matchesPattern(filePath, pattern) {
38792
39750
  const normalizedPath = filePath.replace(/^\/+|\/+$/g, "");
38793
39751
  const normalizedPattern = pattern.replace(/^\/+|\/+$/g, "");
@@ -38812,7 +39770,7 @@ function collectCleoFiles(cleoDir) {
38812
39770
  const entries = readdirSync18(dir);
38813
39771
  for (const entry of entries) {
38814
39772
  if (entry === ".git") continue;
38815
- const fullPath = join60(dir, entry);
39773
+ const fullPath = join62(dir, entry);
38816
39774
  const relPath = relative5(cleoDir, fullPath);
38817
39775
  try {
38818
39776
  const stat3 = statSync9(fullPath);
@@ -38872,7 +39830,7 @@ function generateGitignoreEntries(sharing) {
38872
39830
  async function syncGitignore(cwd) {
38873
39831
  const config = await loadConfig2(cwd);
38874
39832
  const projectRoot = getProjectRoot(cwd);
38875
- const gitignorePath = join60(projectRoot, ".gitignore");
39833
+ const gitignorePath = join62(projectRoot, ".gitignore");
38876
39834
  const entries = generateGitignoreEntries(config.sharing);
38877
39835
  const managedSection = [
38878
39836
  "",
@@ -38882,7 +39840,7 @@ async function syncGitignore(cwd) {
38882
39840
  ""
38883
39841
  ].join("\n");
38884
39842
  let content = "";
38885
- if (existsSync61(gitignorePath)) {
39843
+ if (existsSync63(gitignorePath)) {
38886
39844
  content = await readFile15(gitignorePath, "utf-8");
38887
39845
  }
38888
39846
  const startIdx = content.indexOf(GITIGNORE_START);
@@ -42075,7 +43033,22 @@ async function validateLayer1Schema(context) {
42075
43033
  }
42076
43034
  if (context.params?.status) {
42077
43035
  const status = context.params.status;
42078
- if (!TASK_STATUSES.includes(status)) {
43036
+ const validStatuses = (() => {
43037
+ if (context.domain === "pipeline" && context.operation === "stage.record") {
43038
+ return LIFECYCLE_STAGE_STATUSES;
43039
+ }
43040
+ if (context.domain === "admin" && context.operation?.startsWith("adr.")) {
43041
+ return ADR_STATUSES;
43042
+ }
43043
+ if (context.domain === "session") {
43044
+ return SESSION_STATUSES;
43045
+ }
43046
+ if (context.domain === "pipeline" && context.operation?.startsWith("manifest.")) {
43047
+ return MANIFEST_STATUSES;
43048
+ }
43049
+ return TASK_STATUSES;
43050
+ })();
43051
+ if (!validStatuses.includes(status)) {
42079
43052
  violations.push({
42080
43053
  layer: 1 /* SCHEMA */,
42081
43054
  severity: "error" /* ERROR */,
@@ -42083,8 +43056,8 @@ async function validateLayer1Schema(context) {
42083
43056
  message: `Invalid status: ${status}`,
42084
43057
  field: "status",
42085
43058
  value: status,
42086
- constraint: `Must be one of: ${TASK_STATUSES.join(", ")}`,
42087
- fix: `Use one of: ${TASK_STATUSES.join(", ")}`
43059
+ constraint: `Must be one of: ${validStatuses.join(", ")}`,
43060
+ fix: `Use one of: ${validStatuses.join(", ")}`
42088
43061
  });
42089
43062
  }
42090
43063
  }
@@ -42985,7 +43958,7 @@ init_logger();
42985
43958
  init_project_info();
42986
43959
  init_scaffold();
42987
43960
  init_audit_prune();
42988
- import { join as join61 } from "node:path";
43961
+ import { join as join63 } from "node:path";
42989
43962
  var serverState = null;
42990
43963
  var startupLog = getLogger("mcp:startup");
42991
43964
  async function main() {
@@ -43055,7 +44028,7 @@ async function main() {
43055
44028
  }
43056
44029
  const config = loadConfig();
43057
44030
  const projectInfo = getProjectInfoSync();
43058
- const cleoDir = join61(process.cwd(), ".cleo");
44031
+ const cleoDir = join63(process.cwd(), ".cleo");
43059
44032
  initLogger(cleoDir, {
43060
44033
  level: config.logLevel ?? "info",
43061
44034
  filePath: "logs/cleo.log",