@goodtek/vibeops 2.2.0 → 2.2.1

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/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@ All notable changes to VibeOps are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 2.2.1 - 2026-06-09
8
+
9
+ ### Fixed
10
+
11
+ - **`vibeops task reship --allow-open-mr`**: push then reuse the open MR/PR instead of failing on duplicate `gh pr create`. Skips reship metadata archive when updating the same PR.
12
+ - **`finishTaskWithPullRequest`**: push before checking for an open MR so follow-up commits reach the remote.
13
+
7
14
  ## 2.2.0 - 2026-06-09
8
15
 
9
16
  ### Added
package/dist/cli.js CHANGED
@@ -95,7 +95,7 @@ task
95
95
  .option("--no-integrate", "Skip merging integration branch into task branch")
96
96
  .option("--recreate-branch", "Create task branch from integration instead of reusing local/remote ref")
97
97
  .option("--skip-llm", "Do not run LLM for Result / Test Result")
98
- .option("--allow-open-mr", "Allow reship while the recorded MR is still open")
98
+ .option("--allow-open-mr", "Update the open MR/PR with new commits (merge before reship for a new PR)")
99
99
  .option("--cwd <path>", "Target directory")
100
100
  .action(async (taskRef, opts) => {
101
101
  await taskReshipCommand(taskRef, {
@@ -44,7 +44,8 @@ export async function taskReshipCommand(taskRef, options = {}) {
44
44
  }
45
45
  const integrationBranch = gitCfg?.integrationBranch ?? "develop";
46
46
  const remote = gitCfg?.remote ?? "origin";
47
- if (!dryRun && !(await assertMergeRequestNotOpen(cwd, taskFile, options.allowOpenMr === true))) {
47
+ const updateOpenMr = options.allowOpenMr === true;
48
+ if (!dryRun && !(await assertMergeRequestNotOpen(cwd, taskFile, updateOpenMr))) {
48
49
  process.exitCode = 1;
49
50
  return;
50
51
  }
@@ -113,7 +114,12 @@ export async function taskReshipCommand(taskRef, options = {}) {
113
114
  log.blank();
114
115
  if (dryRun) {
115
116
  log.info(bold("dry-run — would also:"));
116
- log.info(" · feat commit + reship metadata, then push once and open MR/PR (unless --no-pr)");
117
+ if (updateOpenMr) {
118
+ log.info(" · feat commit, push, update open MR/PR (unless --no-pr)");
119
+ }
120
+ else {
121
+ log.info(" · feat commit + reship metadata, then push once and open MR/PR (unless --no-pr)");
122
+ }
117
123
  log.blank();
118
124
  log.info(`Next: ${cyan("vibeops task merge")}, then optional ${cyan("task sync")}`);
119
125
  return;
@@ -137,19 +143,24 @@ export async function taskReshipCommand(taskRef, options = {}) {
137
143
  process.exitCode = 1;
138
144
  return;
139
145
  }
140
- gitCtx = await refreshGitContextBaseCommit(taskFile, gitCtx, cwd);
141
- gitCtx = await prepareGitContextForReship(cwd, taskFile, gitCtx);
142
- await commitReshipMetadata({
143
- cwd,
144
- taskFile,
145
- taskId: parts.id,
146
- dryRun: false,
147
- });
146
+ if (updateOpenMr) {
147
+ log.info(dim("Updating open MR/PR (--allow-open-mr)."));
148
+ }
149
+ else {
150
+ gitCtx = await refreshGitContextBaseCommit(taskFile, gitCtx, cwd);
151
+ gitCtx = await prepareGitContextForReship(cwd, taskFile, gitCtx);
152
+ await commitReshipMetadata({
153
+ cwd,
154
+ taskFile,
155
+ taskId: parts.id,
156
+ dryRun: false,
157
+ });
158
+ }
148
159
  const prResult = await finishTaskWithPullRequest({
149
160
  cwd,
150
161
  taskFile,
151
162
  skipPr: options.noPr === true,
152
- forceNewMergeRequest: true,
163
+ forceNewMergeRequest: !updateOpenMr,
153
164
  });
154
165
  if (!prResult.ok) {
155
166
  log.error("Reship failed — fix push/MR, then rerun task reship.");
@@ -7,6 +7,12 @@ import { dim, log } from "./logger.js";
7
7
  import { createMergeRequest, findMergeRequestByBranches, probeMergeRequestCli, } from "./pr-create.js";
8
8
  import { fallbackTaskPr, generateTaskPrWithLlm } from "./task-pr-llm.js";
9
9
  import { readGitContext, readTaskFile } from "./task.js";
10
+ function mergeRequestAlreadyExistsMessage(msg) {
11
+ const lower = msg.toLowerCase();
12
+ return (lower.includes("already exists") ||
13
+ lower.includes("merge request already exists") ||
14
+ lower.includes("pull request already exists"));
15
+ }
10
16
  async function buildDiffSummary(cwd, gitCtx) {
11
17
  const files = await gitDiffNameOnly(cwd, `${gitCtx.baseCommit}..HEAD`);
12
18
  const commits = await gitLogOneline(cwd, `${gitCtx.baseCommit}..HEAD`);
@@ -55,19 +61,6 @@ export async function finishTaskWithPullRequest(opts) {
55
61
  if (git.branch !== gitCtx.taskBranch) {
56
62
  log.warn(`Current branch is ${git.branch ?? "(detached)"}, not ${gitCtx.taskBranch}. Push may fail.`);
57
63
  }
58
- if (opts.skipPr !== true && opts.forceNewMergeRequest !== true) {
59
- const existing = await findMergeRequestByBranches({
60
- cwd: opts.cwd,
61
- host: gitCfg.host,
62
- headBranch: gitCtx.taskBranch,
63
- baseBranch: gitCtx.baseBranch,
64
- state: "open",
65
- });
66
- if (existing !== null) {
67
- log.info(dim(`Open ${label} already exists: ${existing.url}`));
68
- return { ok: true, mergeRequestUrl: existing.url, pushed: true };
69
- }
70
- }
71
64
  const ahead = await gitCommitsAhead(opts.cwd, gitCtx.baseCommit, "HEAD");
72
65
  if (ahead === 0) {
73
66
  log.warn("No commits ahead of base — pushing branch anyway.");
@@ -85,6 +78,20 @@ export async function finishTaskWithPullRequest(opts) {
85
78
  log.info(dim("Merge request creation skipped (--no-pr)."));
86
79
  return { ok: true, pushed: true };
87
80
  }
81
+ if (opts.forceNewMergeRequest !== true) {
82
+ const existing = await findMergeRequestByBranches({
83
+ cwd: opts.cwd,
84
+ host: gitCfg.host,
85
+ headBranch: gitCtx.taskBranch,
86
+ baseBranch: gitCtx.baseBranch,
87
+ state: "open",
88
+ });
89
+ if (existing !== null) {
90
+ log.ok(`Open ${label} updated: ${existing.url}`);
91
+ log.info(dim("MR/PR URL is on the host — not written to TASK md (use task merge)."));
92
+ return { ok: true, mergeRequestUrl: existing.url, pushed: true };
93
+ }
94
+ }
88
95
  const cliOk = await probeMergeRequestCli(gitCfg.host);
89
96
  if (!cliOk) {
90
97
  const tool = gitCfg.host === "gitlab" ? "glab" : "gh";
@@ -121,6 +128,20 @@ export async function finishTaskWithPullRequest(opts) {
121
128
  }
122
129
  catch (e) {
123
130
  const msg = e instanceof Error ? e.message : String(e);
131
+ if (mergeRequestAlreadyExistsMessage(msg)) {
132
+ const existing = await findMergeRequestByBranches({
133
+ cwd: opts.cwd,
134
+ host: gitCfg.host,
135
+ headBranch: gitCtx.taskBranch,
136
+ baseBranch: gitCtx.baseBranch,
137
+ state: "open",
138
+ });
139
+ if (existing !== null) {
140
+ log.ok(`Open ${label} updated: ${existing.url}`);
141
+ log.info(dim("MR/PR URL is on the host — not written to TASK md (use task merge)."));
142
+ return { ok: true, mergeRequestUrl: existing.url, pushed: true };
143
+ }
144
+ }
124
145
  log.error(`Could not create merge request: ${msg}`);
125
146
  log.info(dim(`Branch was pushed. Create MR manually: ${gitCtx.taskBranch} → ${gitCtx.baseBranch}`));
126
147
  return { ok: false, pushed: true };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodtek/vibeops",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Minimal CLI rails for Cursor-based TASK-driven vibe coding.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@9.15.9",