@goodtek/vibeops 1.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +28 -12
  3. package/dist/bootstrap/installer.js +1 -1
  4. package/dist/cli.js +58 -8
  5. package/dist/commands/init.js +3 -3
  6. package/dist/commands/llm.js +3 -3
  7. package/dist/commands/status.js +1 -0
  8. package/dist/commands/task-add.js +2 -2
  9. package/dist/commands/task-merge.js +134 -0
  10. package/dist/commands/task-release.js +103 -0
  11. package/dist/commands/{task-done.js → task-ship.js} +28 -63
  12. package/dist/commands/task-sync.js +183 -0
  13. package/dist/lib/git.js +7 -0
  14. package/dist/lib/paths.js +1 -1
  15. package/dist/lib/pr-create.js +63 -0
  16. package/dist/lib/resolve-task.js +1 -1
  17. package/dist/lib/task-context.js +22 -7
  18. package/dist/lib/task-git-commit.js +32 -0
  19. package/dist/lib/task-lifecycle-target.js +41 -0
  20. package/dist/lib/task-scaffold.js +1 -1
  21. package/dist/lib/{task-done-llm.js → task-ship-llm.js} +9 -6
  22. package/dist/lib/task.js +7 -0
  23. package/package.json +1 -1
  24. package/templates/clients/claude/CLAUDE.md +5 -27
  25. package/templates/clients/cursor/.cursor/rules/02-git-safety.mdc +5 -5
  26. package/templates/clients/cursor/.cursor/rules/03-docs-before-ship.mdc +13 -0
  27. package/templates/core/AGENTS.md +16 -34
  28. package/templates/core/docs/logs/README.md +1 -1
  29. package/templates/core/docs/project/03-architecture.md +1 -1
  30. package/templates/core/docs/project/05-current-state.md +1 -1
  31. package/templates/core/docs/project/06-decisions.md +1 -1
  32. package/templates/skills-shared/implement-task/SKILL.md +7 -16
  33. package/templates/clients/cursor/.cursor/rules/03-docs-on-done.mdc +0 -13
package/CHANGELOG.md CHANGED
@@ -4,6 +4,30 @@ All notable changes to VibeOps are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 2.0.0 - 2026-06-03
8
+
9
+ ### Breaking
10
+
11
+ - **TASK lifecycle commands renamed and split:** `task done` removed. Use **`task ship`** (submit: commit, push, PR, Status → Review), **`task merge`** (merge PR into integration), **`task sync`** (integration pull, Status → Done, branch cleanup), **`task release`** (integration → production PR).
12
+ - Daily commands: `init`, `task add`, `task ship`, `task merge`, `task sync`, `status`, `llm`. Optional: `task release`.
13
+
14
+ ### Added
15
+
16
+ - **`vibeops task merge`** — default `gh pr merge` / `glab mr merge` (squash unless `--merge` / `--rebase`); `--dry-run`.
17
+ - **`vibeops task release`** — develop → main (or configured branches) release PR + merge; noop when trunk policy.
18
+
19
+ ### Changed
20
+
21
+ - **`task ship`** replaces `task done`: Done status and `doneAt` are set on **`task sync`** on the integration branch, not at ship time.
22
+ - **`status`** next hints: ship → merge → sync.
23
+ - Templates and Cursor rules reference ship / merge / sync (rule file `03-docs-before-ship.mdc`).
24
+
25
+ ## 1.1.3 - 2026-06-03
26
+
27
+ ### Added
28
+
29
+ - **`vibeops task sync [TASK-NNN]`** — after the MR is merged on the host: `git fetch --prune`, fast-forward the integration branch (e.g. `develop`), delete the local task branch (`-d`, or `-D` with `--force`), and optionally delete the remote task branch (`--no-remote-delete` to skip). Resolves the task branch from the current `task/*` checkout, the latest Done TASK, or an explicit ref.
30
+
7
31
  ## 1.1.2 - 2026-06-03
8
32
 
9
33
  ### Fixed
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![npm version](https://img.shields.io/npm/v/@goodtek/vibeops.svg)](https://www.npmjs.com/package/@goodtek/vibeops)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
7
 
8
- VibeOps bootstraps an **agent-friendly repo**, starts numbered **TASK** files on Git branches, and closes them while updating **project memory** in Git. You plan and implement in your chosen agent; the CLI handles files, Git, and short LLM assists.
8
+ VibeOps bootstraps an **agent-friendly repo**, starts numbered **TASK** files on Git branches, and runs a clear GitFlow lifecycle. You plan and implement in your chosen agent; the CLI handles files, Git, and short LLM assists.
9
9
 
10
10
  ## Commands
11
11
 
@@ -13,7 +13,10 @@ VibeOps bootstraps an **agent-friendly repo**, starts numbered **TASK** files on
13
13
  |---------|---------|
14
14
  | `vibeops init` | Core docs + agent packs (cursor / claude / codex) |
15
15
  | `vibeops task add` | New `TASK-NNN` file + task branch |
16
- | `vibeops task done` | LLM summary, push branch, open MR/PR (no local merge) |
16
+ | `vibeops task ship` | LLM summary, commit, push, open MR/PR (Status → Review) |
17
+ | `vibeops task merge` | Merge TASK MR/PR into integration branch (default: squash) |
18
+ | `vibeops task sync` | After merge: update integration branch, mark Done, delete task branch |
19
+ | `vibeops task release` | Release PR: integration → production (GitFlow) |
17
20
  | `vibeops status` | Briefing: TASK, Git, LLM, clients |
18
21
  | `vibeops llm` | Connect LLM providers (`connect` · `status` · `use`) |
19
22
 
@@ -45,17 +48,25 @@ Re-init **overwrites** templates (rules, skills, doc stubs). **`docs/tasks/TASK-
45
48
  vibeops init --clients cursor --git --initial-commit
46
49
 
47
50
  vibeops task add
48
- # Plan / build using your agent and the current TASK file
51
+ # Plan / build in Cursor (@docs/tasks/TASK-NNN-*.md)
49
52
 
50
- vibeops task done
51
- vibeops status
53
+ vibeops task ship
54
+ vibeops task merge
55
+ vibeops task sync
56
+ vibeops task add
57
+ ```
58
+
59
+ Occasionally (GitFlow release to production):
60
+
61
+ ```bash
62
+ vibeops task release
52
63
  ```
53
64
 
54
- If a TASK is **In Progress**, `task add` exits with a guide — it does not auto-run `task done`.
65
+ If a TASK is **In Progress** or **Review**, `task add` exits with a guide.
55
66
 
56
67
  ## LLM (optional)
57
68
 
58
- For **`task add`** / **`task done`** only (not for coding in the IDE):
69
+ For **`task add`** / **`task ship`** only (not for coding in the IDE):
59
70
 
60
71
  ```bash
61
72
  vibeops llm connect
@@ -80,16 +91,21 @@ pnpm install && pnpm build && pnpm smoke
80
91
 
81
92
  - **`init`**: `--clients`, `--yes`, `--dry-run`, `--force`, `--git`, `--initial-commit`, `--git-policy gitflow|trunk`, `--integration-branch`, `--production-branch`, `--allow-no-remote`, `--cwd`
82
93
  - **`task add`**: `--dry-run`, `--non-interactive --idea "…"`
83
- - **`task done`**: `--dry-run`, `--no-pr` (push only)
94
+ - **`task ship`**: `--dry-run`, `--no-pr`
95
+ - **`task merge`**: `--dry-run`, `--merge`, `--rebase`
96
+ - **`task sync`**: `--dry-run`, `--no-remote-delete`, `--force`
97
+ - **`task release`**: `--dry-run`, `--no-merge`, `--merge`, `--rebase`
84
98
  - **`status`**: `--json`
85
99
 
86
100
  ## Git
87
101
 
88
102
  - **Init** records branch policy in `.vibeops.json` (`integrationBranch`, `productionBranch`, `host`).
89
- - **`task add`**: branch `task/<slug>` from the **integration** branch (e.g. `develop`).
90
- - **`task done`**: commit implementation → `git push` → `gh pr create` / `glab mr create` → commit TASK closure (Status Done, Git Context) → push again. **You merge on GitHub/GitLab**; CI deploys.
91
- - Commit messages: `feat(task-nnn): …`
92
- - No force-push to shared branches
103
+ - **`task add`**: `task/<slug>` from **integration** (e.g. `develop`).
104
+ - **`task ship`**: commit → push → MR/PR → Status **Review** on task branch.
105
+ - **`task merge`**: merge MR/PR into integration (CLI default: squash).
106
+ - **`task sync`**: integration ff-only pull → Status **Done** → delete `task/*` branches.
107
+ - **`task release`**: integration → production PR + merge (skipped on trunk policy).
108
+ - No force-push to shared branches.
93
109
 
94
110
  ## License
95
111
 
@@ -12,7 +12,7 @@ function envExampleContents() {
12
12
  "# VibeOps · optional environment",
13
13
  "# Copy to .vibeops.env (gitignored). Never commit secrets.",
14
14
  "#",
15
- "# LLM for task add / task done (optional — Codex OAuth and Cursor Agent CLI also work):",
15
+ "# LLM for task add / task ship (optional — Codex OAuth and Cursor Agent CLI also work):",
16
16
  "# OPENAI_API_KEY=",
17
17
  "",
18
18
  ].join("\n");
package/dist/cli.js CHANGED
@@ -4,13 +4,16 @@ import { initCommand } from "./commands/init.js";
4
4
  import { llmConnectCommand, llmStatusCommand, llmUseCommand } from "./commands/llm.js";
5
5
  import { statusCommand } from "./commands/status.js";
6
6
  import { taskAddCommand } from "./commands/task-add.js";
7
- import { taskDoneCommand } from "./commands/task-done.js";
7
+ import { taskMergeCommand } from "./commands/task-merge.js";
8
+ import { taskReleaseCommand } from "./commands/task-release.js";
9
+ import { taskShipCommand } from "./commands/task-ship.js";
10
+ import { taskSyncCommand } from "./commands/task-sync.js";
8
11
  import { loadVibeopsEnv } from "./lib/env.js";
9
12
  import { VERSION } from "./version.js";
10
13
  const program = new Command();
11
14
  program
12
15
  .name("vibeops")
13
- .description("VibeOps v3 — Cursor-friendly TASK workflow (init · task · status · llm)")
16
+ .description("VibeOps — TASK workflow (init · task add/ship/merge/sync · status · llm)")
14
17
  .version(VERSION, "-v, --version", "Print the VibeOps version");
15
18
  program.hook("preAction", async (_thisCommand, actionCommand) => {
16
19
  const opts = actionCommand.opts();
@@ -53,10 +56,10 @@ program
53
56
  gitHost: opts.gitHost,
54
57
  });
55
58
  });
56
- const task = program.command("task").description("TASK lifecycle");
59
+ const task = program.command("task").description("TASK lifecycle (add · ship · merge · sync · release)");
57
60
  task
58
61
  .command("add")
59
- .description("Create next TASK-NNN, task branch, In Progress (guide-only if one is open)")
62
+ .description("Create next TASK-NNN, task branch, In Progress")
60
63
  .option("--dry-run", "Plan only")
61
64
  .option("--non-interactive", "CI: minimal scaffold (optional --idea)")
62
65
  .option("--idea <text>", "Short description (CI)")
@@ -70,18 +73,65 @@ task
70
73
  });
71
74
  });
72
75
  task
73
- .command("done [taskRef]")
74
- .description("Close TASK: LLM summary, push branch, open MR/PR (no local merge)")
76
+ .command("ship [taskRef]")
77
+ .description("Submit TASK: LLM summary, commit, push, open MR/PR (Status → Review)")
75
78
  .option("--dry-run", "Plan only")
76
79
  .option("--no-pr", "Push only; skip creating MR/PR")
77
80
  .option("--cwd <path>", "Target directory")
78
81
  .action(async (taskRef, opts) => {
79
- await taskDoneCommand(taskRef, {
82
+ await taskShipCommand(taskRef, {
80
83
  dryRun: Boolean(opts.dryRun),
81
84
  noPr: Boolean(opts.noPr),
82
85
  cwd: opts.cwd,
83
86
  });
84
87
  });
88
+ task
89
+ .command("merge [taskRef]")
90
+ .description("Merge TASK MR/PR into integration branch (default: squash)")
91
+ .option("--dry-run", "Plan only")
92
+ .option("--merge", "Merge commit (instead of squash)")
93
+ .option("--rebase", "Rebase merge")
94
+ .option("--cwd <path>", "Target directory")
95
+ .action(async (taskRef, opts) => {
96
+ await taskMergeCommand(taskRef, {
97
+ dryRun: Boolean(opts.dryRun),
98
+ merge: Boolean(opts.merge),
99
+ rebase: Boolean(opts.rebase),
100
+ cwd: opts.cwd,
101
+ });
102
+ });
103
+ task
104
+ .command("sync [taskRef]")
105
+ .description("After MR merge: update integration branch, mark Done, delete task branch")
106
+ .option("--dry-run", "Plan only")
107
+ .option("--no-remote-delete", "Keep the task branch on the remote")
108
+ .option("--force", "Delete local task branch with -D if not fully merged")
109
+ .option("--cwd <path>", "Target directory")
110
+ .action(async (taskRef, opts) => {
111
+ await taskSyncCommand(taskRef, {
112
+ dryRun: Boolean(opts.dryRun),
113
+ noRemoteDelete: Boolean(opts.noRemoteDelete),
114
+ force: Boolean(opts.force),
115
+ cwd: opts.cwd,
116
+ });
117
+ });
118
+ task
119
+ .command("release")
120
+ .description("Open (and merge) release MR/PR: integration branch → production")
121
+ .option("--dry-run", "Plan only")
122
+ .option("--no-merge", "Create release PR only")
123
+ .option("--merge", "Merge commit (instead of squash)")
124
+ .option("--rebase", "Rebase merge")
125
+ .option("--cwd <path>", "Target directory")
126
+ .action(async (opts) => {
127
+ await taskReleaseCommand({
128
+ dryRun: Boolean(opts.dryRun),
129
+ noMerge: Boolean(opts.noMerge),
130
+ merge: Boolean(opts.merge),
131
+ rebase: Boolean(opts.rebase),
132
+ cwd: opts.cwd,
133
+ });
134
+ });
85
135
  program
86
136
  .command("status")
87
137
  .description("Briefing: active TASK, Git, LLM, next hint")
@@ -93,7 +143,7 @@ program
93
143
  cwd: opts.cwd,
94
144
  });
95
145
  });
96
- const llm = program.command("llm").description("LLM providers for task add / task done");
146
+ const llm = program.command("llm").description("LLM providers for task add / task ship");
97
147
  llm
98
148
  .command("connect")
99
149
  .description("Interactive menu: connect providers and choose default")
@@ -124,14 +124,14 @@ function printNextSteps(clients, git) {
124
124
  if (!clients.includes("cursor") && !clients.includes("claude") && !clients.includes("codex")) {
125
125
  log.info(" 2. Open the current TASK file in your agent before coding");
126
126
  }
127
- log.info(` 3. ${cyan("vibeops llm connect")} — LLM for task add / task done`);
127
+ log.info(` 3. ${cyan("vibeops llm connect")} — LLM for task add / task ship`);
128
128
  log.info(" 4. Push branches to origin (first time only):");
129
129
  log.info(` ${dim(`git push -u ${git.remote} ${git.productionBranch}`)}`);
130
130
  if (git.integrationBranch !== git.productionBranch) {
131
131
  log.info(` ${dim(`git push -u ${git.remote} ${git.integrationBranch}`)}`);
132
132
  }
133
133
  log.info(` 5. ${cyan("vibeops task add")} — branches from ${git.integrationBranch} (pulls latest first)`);
134
- log.info(` 6. ${cyan("vibeops task done")} — push + open MR/PR; merge on ${git.host} (CI deploys)`);
134
+ log.info(` 6. ${cyan("vibeops task ship")} → merge → sync — TASK lifecycle on ${git.host}`);
135
135
  }
136
136
  export async function initCommand(options = {}) {
137
137
  const dryRun = options.dryRun === true;
@@ -272,7 +272,7 @@ export async function initCommand(options = {}) {
272
272
  if (!dryRun) {
273
273
  await writeConfig(projectRoot, buildConfig(name, clients, gitConfig, existingConfig));
274
274
  if (options.allowNoRemote === true && remote === null) {
275
- log.warn("No git remote configured (--allow-no-remote). task done push/MR will fail until origin exists.");
275
+ log.warn("No git remote configured (--allow-no-remote). task ship push/MR will fail until origin exists.");
276
276
  }
277
277
  printNextSteps(clients, gitConfig);
278
278
  }
@@ -71,7 +71,7 @@ export async function llmUseCommand(providerArg, opts = {}) {
71
71
  })),
72
72
  ];
73
73
  preference = await select({
74
- message: "Default LLM for task add / task done",
74
+ message: "Default LLM for task add / task ship",
75
75
  choices,
76
76
  loop: false,
77
77
  });
@@ -99,7 +99,7 @@ export async function llmConnectCommand(opts = {}) {
99
99
  return;
100
100
  }
101
101
  log.info(bold("vibeops llm connect"));
102
- log.info(dim("Set up providers, then pick which one task add / task done use."));
102
+ log.info(dim("Set up providers, then pick which one task add / task ship use."));
103
103
  log.blank();
104
104
  let done = false;
105
105
  while (!done) {
@@ -116,7 +116,7 @@ export async function llmConnectCommand(opts = {}) {
116
116
  const action = await select({
117
117
  message: "LLM setup",
118
118
  choices: [
119
- { name: "Choose default provider (task add / task done)", value: "pick" },
119
+ { name: "Choose default provider (task add / task ship)", value: "pick" },
120
120
  { name: "Set up Cursor Agent CLI (`agent login`)", value: "setup-cursor" },
121
121
  { name: "Show Codex OAuth setup (`codex login`)", value: "setup-codex" },
122
122
  { name: "Set up OpenAI API key (.vibeops.env)", value: "setup-openai" },
@@ -62,6 +62,7 @@ async function buildReport(cwd) {
62
62
  resultFilled,
63
63
  testFilled,
64
64
  onTaskBranch,
65
+ hasMergeRequest: typeof mergeRequestUrl === "string" && mergeRequestUrl.length > 0,
65
66
  });
66
67
  const llm = isVibeopsProject ? await buildLlmStatusReport(cwd) : null;
67
68
  return {
@@ -44,7 +44,7 @@ export async function taskAddCommand(opts = {}) {
44
44
  }
45
45
  log.info(` ${dim("file")} ${relPath(root, blocking.filePath)}`);
46
46
  log.blank();
47
- log.info(`Finish it first: ${cyan(`vibeops task done ${blocking.id}`)}`);
47
+ log.info(`Finish it first: ${cyan(`vibeops task sync ${blocking.id}`)} (after ship + merge)`);
48
48
  log.info(`Then run ${cyan("vibeops task add")} again.`);
49
49
  process.exitCode = 1;
50
50
  return;
@@ -123,5 +123,5 @@ export async function taskAddCommand(opts = {}) {
123
123
  log.info(bold("Next in Cursor"));
124
124
  log.info(` Ask: @${relFile} — plan Scope / Acceptance Criteria`);
125
125
  log.info(` Agent: same file — implement`);
126
- log.info(` Done: ${cyan(`vibeops task done ${taskId}`)}`);
126
+ log.info(` Ship: ${cyan(`vibeops task ship ${taskId}`)}`);
127
127
  }
@@ -0,0 +1,134 @@
1
+ import { resolve } from "node:path";
2
+ import { GitConfigError, requireGitConfig } from "../lib/git-config.js";
3
+ import { detectGitHost, mergeRequestLabel } from "../lib/git-host.js";
4
+ import { gitRemoteUrl } from "../lib/git.js";
5
+ import { bold, cyan, dim, log } from "../lib/logger.js";
6
+ import { projectPaths } from "../lib/paths.js";
7
+ import { taskNotFoundMessage } from "../lib/resolve-task.js";
8
+ import { getMergeRequestState, mergeMergeRequest, probeMergeRequestCli, } from "../lib/pr-create.js";
9
+ import { relPath } from "../lib/task-context.js";
10
+ import { resolveLifecycleTarget } from "../lib/task-lifecycle-target.js";
11
+ import { readGitContext } from "../lib/task.js";
12
+ function resolveMergeMethod(opts) {
13
+ if (opts.rebase === true)
14
+ return "rebase";
15
+ if (opts.merge === true)
16
+ return "merge";
17
+ return "squash";
18
+ }
19
+ export async function taskMergeCommand(taskRef, options = {}) {
20
+ const cwd = resolve(options.cwd ?? process.cwd());
21
+ const dryRun = options.dryRun === true;
22
+ const paths = projectPaths(cwd);
23
+ let gitCfg;
24
+ try {
25
+ gitCfg = await requireGitConfig(cwd);
26
+ }
27
+ catch (e) {
28
+ if (e instanceof GitConfigError) {
29
+ log.error(e.message);
30
+ process.exitCode = 1;
31
+ return;
32
+ }
33
+ throw e;
34
+ }
35
+ const target = await resolveLifecycleTarget(paths, cwd, taskRef);
36
+ if (target === null) {
37
+ if (taskRef?.trim()) {
38
+ log.error(taskNotFoundMessage(taskRef));
39
+ }
40
+ else {
41
+ log.error("No TASK to merge. Pass TASK-NNN, checkout a task/* branch, or run task ship first.");
42
+ }
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+ const ctx = await readGitContext(target.taskFile);
47
+ if (ctx === null) {
48
+ log.error("No Git Context on TASK — run task add first.");
49
+ process.exitCode = 1;
50
+ return;
51
+ }
52
+ if (!ctx.mergeRequestUrl) {
53
+ if (dryRun) {
54
+ log.info(dim(`would merge ${ctx.taskBranch} → ${ctx.baseBranch} after MR exists (run task ship)`));
55
+ log.blank();
56
+ log.info(`Next: ${cyan("vibeops task sync")}`);
57
+ return;
58
+ }
59
+ log.error("No Merge Request URL on TASK — run task ship first.");
60
+ process.exitCode = 1;
61
+ return;
62
+ }
63
+ const remoteUrl = await gitRemoteUrl(cwd, gitCfg.remote);
64
+ const host = remoteUrl !== null && detectGitHost(remoteUrl) !== null
65
+ ? detectGitHost(remoteUrl)
66
+ : gitCfg.host;
67
+ const method = resolveMergeMethod(options);
68
+ const label = mergeRequestLabel(host);
69
+ log.info(bold(`vibeops task merge ${target.taskId}`));
70
+ log.info(` ${dim("file")} ${relPath(cwd, target.taskFile)}`);
71
+ log.info(` ${dim("MR/PR")} ${ctx.mergeRequestUrl}`);
72
+ log.info(` ${dim("target")} ${ctx.baseBranch} (${gitCfg.integrationBranch})`);
73
+ log.info(` ${dim("method")} ${method}`);
74
+ log.blank();
75
+ if (dryRun) {
76
+ await mergeMergeRequest({
77
+ cwd,
78
+ host,
79
+ url: ctx.mergeRequestUrl,
80
+ method,
81
+ dryRun: true,
82
+ });
83
+ log.blank();
84
+ log.info(`Next: ${cyan("vibeops task sync")}`);
85
+ return;
86
+ }
87
+ const state = await getMergeRequestState(cwd, host, ctx.mergeRequestUrl);
88
+ if (state === "merged") {
89
+ log.ok(`${label} already merged.`);
90
+ log.blank();
91
+ log.info(`Next: ${cyan("vibeops task sync")}`);
92
+ return;
93
+ }
94
+ if (state === "open") {
95
+ const cliOk = await probeMergeRequestCli(host);
96
+ if (!cliOk) {
97
+ const tool = host === "gitlab" ? "glab" : "gh";
98
+ log.error(`${tool} CLI not found — merge in the host UI, then task sync.`);
99
+ process.exitCode = 1;
100
+ return;
101
+ }
102
+ try {
103
+ await mergeMergeRequest({ cwd, host, url: ctx.mergeRequestUrl, method });
104
+ log.ok(`${label} merged into ${ctx.baseBranch}.`);
105
+ }
106
+ catch (e) {
107
+ const msg = e instanceof Error ? e.message : String(e);
108
+ log.error(`Merge failed: ${msg}`);
109
+ log.info(dim("Merge in the host UI if branch protection blocks CLI merge."));
110
+ process.exitCode = 1;
111
+ return;
112
+ }
113
+ }
114
+ else if (state === "closed") {
115
+ log.warn(`${label} is closed but not merged — resolve manually.`);
116
+ process.exitCode = 1;
117
+ return;
118
+ }
119
+ else {
120
+ log.warn(`Could not read ${label} state — attempting merge anyway.`);
121
+ try {
122
+ await mergeMergeRequest({ cwd, host, url: ctx.mergeRequestUrl, method });
123
+ log.ok(`${label} merge command completed.`);
124
+ }
125
+ catch (e) {
126
+ const msg = e instanceof Error ? e.message : String(e);
127
+ log.error(`Merge failed: ${msg}`);
128
+ process.exitCode = 1;
129
+ return;
130
+ }
131
+ }
132
+ log.blank();
133
+ log.info(`Next: ${cyan("vibeops task sync")}`);
134
+ }
@@ -0,0 +1,103 @@
1
+ import { resolve } from "node:path";
2
+ import { GitConfigError, requireGitConfig } from "../lib/git-config.js";
3
+ import { detectGitHost, mergeRequestLabel } from "../lib/git-host.js";
4
+ import { gitRemoteUrl } from "../lib/git.js";
5
+ import { bold, dim, log } from "../lib/logger.js";
6
+ import { createMergeRequest, getMergeRequestState, mergeMergeRequest, probeMergeRequestCli, } from "../lib/pr-create.js";
7
+ function resolveMergeMethod(opts) {
8
+ if (opts.rebase === true)
9
+ return "rebase";
10
+ if (opts.merge === true)
11
+ return "merge";
12
+ return "squash";
13
+ }
14
+ export async function taskReleaseCommand(options = {}) {
15
+ const cwd = resolve(options.cwd ?? process.cwd());
16
+ const dryRun = options.dryRun === true;
17
+ let gitCfg;
18
+ try {
19
+ gitCfg = await requireGitConfig(cwd);
20
+ }
21
+ catch (e) {
22
+ if (e instanceof GitConfigError) {
23
+ log.error(e.message);
24
+ process.exitCode = 1;
25
+ return;
26
+ }
27
+ throw e;
28
+ }
29
+ const { integrationBranch, productionBranch, remote } = gitCfg;
30
+ if (integrationBranch === productionBranch) {
31
+ log.info("Trunk policy: integration and production are the same branch — no release PR step.");
32
+ return;
33
+ }
34
+ const remoteUrl = await gitRemoteUrl(cwd, remote);
35
+ const host = remoteUrl !== null && detectGitHost(remoteUrl) !== null
36
+ ? detectGitHost(remoteUrl)
37
+ : gitCfg.host;
38
+ const label = mergeRequestLabel(host);
39
+ const title = `Release: ${integrationBranch} → ${productionBranch}`;
40
+ const body = [
41
+ `Integrate \`${integrationBranch}\` into \`${productionBranch}\`.`,
42
+ "",
43
+ "Opened by `vibeops task release`.",
44
+ ].join("\n");
45
+ log.info(bold("vibeops task release"));
46
+ log.info(` ${dim("head")} ${integrationBranch}`);
47
+ log.info(` ${dim("base")} ${productionBranch}`);
48
+ log.blank();
49
+ if (dryRun) {
50
+ log.info(bold("dry-run — would:"));
51
+ log.info(` · ${label}: ${integrationBranch} → ${productionBranch}`);
52
+ if (options.noMerge !== true) {
53
+ log.info(` · merge ${label} (${resolveMergeMethod(options)})`);
54
+ }
55
+ return;
56
+ }
57
+ const cliOk = await probeMergeRequestCli(host);
58
+ if (!cliOk) {
59
+ const tool = host === "gitlab" ? "glab" : "gh";
60
+ log.error(`${tool} CLI not found — create the release ${label} manually.`);
61
+ process.exitCode = 1;
62
+ return;
63
+ }
64
+ let releaseUrl;
65
+ try {
66
+ const { url } = await createMergeRequest({
67
+ cwd,
68
+ host,
69
+ baseBranch: productionBranch,
70
+ headBranch: integrationBranch,
71
+ title,
72
+ body,
73
+ });
74
+ releaseUrl = url;
75
+ log.ok(`${label}: ${url}`);
76
+ }
77
+ catch (e) {
78
+ const msg = e instanceof Error ? e.message : String(e);
79
+ log.error(`Could not create release ${label}: ${msg}`);
80
+ process.exitCode = 1;
81
+ return;
82
+ }
83
+ if (options.noMerge === true) {
84
+ log.info(dim("Merge skipped (--no-merge)."));
85
+ return;
86
+ }
87
+ const state = await getMergeRequestState(cwd, host, releaseUrl);
88
+ if (state === "merged") {
89
+ log.ok(`Release ${label} already merged.`);
90
+ return;
91
+ }
92
+ const method = resolveMergeMethod(options);
93
+ try {
94
+ await mergeMergeRequest({ cwd, host, url: releaseUrl, method });
95
+ log.ok(`Release ${label} merged into ${productionBranch}.`);
96
+ }
97
+ catch (e) {
98
+ const msg = e instanceof Error ? e.message : String(e);
99
+ log.error(`Release merge failed: ${msg}`);
100
+ log.info(dim(`Complete merge in the host UI: ${releaseUrl}`));
101
+ process.exitCode = 1;
102
+ }
103
+ }