@goodtek/vibeops 3.1.6 → 3.1.7
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,14 @@ All notable changes to VibeOps are documented here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 3.1.7 - 2026-10-17
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **`task ship --new-cycle` missed work already committed on the task branch** ("Nothing to do — merged with no new changes"). With no uncommitted files it now compares the branch with the fetched `<remote>/<base>` by content (earlier cycles were squash-merged, so commit counts would mislead) and opens the new PR for what is not merged yet (TASK-032).
|
|
12
|
+
- **`-m` was ignored in a new PR cycle**, so the commit used the TASK title and dropped trailers. The new cycle now resolves the message like a normal ship (body and trailers kept); `-m` with nothing to commit warns instead of making an empty commit (TASK-032).
|
|
13
|
+
- **PR body listed other TASKs' changes** after the base branch was merged into the task branch: the Commits / Files lists came from the TASK's Base Commit. They are now computed against the freshly fetched `<remote>/<base>` (commits without merges, files from the merge base); the Base Commit is only a fallback when the fetch fails (TASK-031).
|
|
14
|
+
|
|
7
15
|
## 3.1.6 - 2026-10-17
|
|
8
16
|
|
|
9
17
|
### Fixed
|
|
@@ -297,6 +297,8 @@ async function runNewCycleFromShip(ctx) {
|
|
|
297
297
|
recreateBranch: options.recreateBranch,
|
|
298
298
|
skipLlm: options.skipLlm,
|
|
299
299
|
allowOpenMr: options.allowOpenMr,
|
|
300
|
+
message: options.message,
|
|
301
|
+
nonInteractive: options.nonInteractive,
|
|
300
302
|
});
|
|
301
303
|
}
|
|
302
304
|
/** State/PR mismatch: Status is Shipped but no open/merged PR was found. */
|
package/dist/lib/git.js
CHANGED
|
@@ -490,8 +490,10 @@ export async function gitAllChangedFilesSinceTaskStart(baseCommit, cwd) {
|
|
|
490
490
|
const all = Array.from(new Set([...workingTree, ...committed]));
|
|
491
491
|
return { working, staged, untracked, committed, workingTree, all };
|
|
492
492
|
}
|
|
493
|
-
export async function gitLogOneline(cwd, range) {
|
|
493
|
+
export async function gitLogOneline(cwd, range, options = {}) {
|
|
494
494
|
const args = ["log", "--oneline", "--no-decorate"];
|
|
495
|
+
if (options.noMerges === true)
|
|
496
|
+
args.push("--no-merges");
|
|
495
497
|
if (typeof range === "string" && range.length > 0)
|
|
496
498
|
args.push(range);
|
|
497
499
|
const res = await tryGit(cwd, args);
|
|
@@ -7,11 +7,17 @@ import { gitIsAncestor, runGit } from "./git.js";
|
|
|
7
7
|
* into the base since; a squash merge then lands that revert. Fetches the base
|
|
8
8
|
* branch and the head commit first; any fetch / read failure fails closed.
|
|
9
9
|
*/
|
|
10
|
+
/** Fetch `<remote>/<baseBranch>` into its remote-tracking ref and return that ref. Throws on failure. */
|
|
11
|
+
export async function fetchBaseBranch(cwd, remote, baseBranch) {
|
|
12
|
+
const baseRef = `refs/remotes/${remote}/${baseBranch}`;
|
|
13
|
+
await runGit(cwd, ["fetch", "--no-tags", remote, `+refs/heads/${baseBranch}:${baseRef}`]);
|
|
14
|
+
return baseRef;
|
|
15
|
+
}
|
|
10
16
|
export async function checkBaseInHead(opts) {
|
|
11
17
|
const { cwd, remote, baseBranch, headSha } = opts;
|
|
12
18
|
const baseRef = `refs/remotes/${remote}/${baseBranch}`;
|
|
13
19
|
try {
|
|
14
|
-
await
|
|
20
|
+
await fetchBaseBranch(cwd, remote, baseBranch);
|
|
15
21
|
if (!(await commitExists(cwd, headSha))) {
|
|
16
22
|
await runGit(cwd, ["fetch", "--no-tags", remote, headSha]);
|
|
17
23
|
}
|
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { readText } from "./filesystem.js";
|
|
3
|
-
import { listWorkingTreeRelPaths } from "./git.js";
|
|
3
|
+
import { listWorkingTreeRelPaths, runGit } from "./git.js";
|
|
4
4
|
import { bold, cyan, dim, green, log, yellow } from "./logger.js";
|
|
5
|
+
import { fetchBaseBranch } from "./merge-base-freshness.js";
|
|
5
6
|
import { projectPaths } from "./paths.js";
|
|
6
7
|
import { prNumberFromUrl } from "./pr-create.js";
|
|
7
8
|
import { relPath } from "./task-context.js";
|
|
8
|
-
import {
|
|
9
|
+
import { resolveShipCommitMessage } from "./task-commit-msg.js";
|
|
10
|
+
import { commitDirtyWorkingTree, docsCommitMessageFor } from "./task-git-commit.js";
|
|
9
11
|
import { finishTaskWithPullRequest } from "./task-push-pr.js";
|
|
10
12
|
import { applyTaskShipMemory, llmCompleteTaskReship, writeTaskResultSections, } from "./task-ship-llm.js";
|
|
11
13
|
import { assertMergeRequestNotOpen, ensureTaskBranchForReship, integrateIntegrationIntoTaskBranch, loadGitConfigForReship, prepareGitContextForReship, refreshGitContextBaseCommit, resolveReshipTarget, } from "./task-reship.js";
|
|
12
14
|
import { hasNonEmptySection, parseTaskFilename, readGitContext } from "./task.js";
|
|
13
15
|
import { prRefLabel } from "./task-ship-state.js";
|
|
14
16
|
import { runUnderTaskLock } from "./task-lock.js";
|
|
17
|
+
/**
|
|
18
|
+
* Files this cycle would change on the base: after integrating `<remote>/<base>` the tree
|
|
19
|
+
* difference to the base is exactly the work not merged yet (earlier cycles' commits were
|
|
20
|
+
* squash-merged, so counting commits would report them again). With `--no-integrate` the
|
|
21
|
+
* branch may lack newer base changes, so compare against the merge base instead.
|
|
22
|
+
*/
|
|
23
|
+
async function unmergedFiles(cwd, remote, baseBranch, integrated) {
|
|
24
|
+
const baseRef = await fetchBaseBranch(cwd, remote, baseBranch);
|
|
25
|
+
const { stdout } = await runGit(cwd, integrated ? ["diff", "--name-only", baseRef, "HEAD"] : ["diff", "--name-only", `${baseRef}...HEAD`]);
|
|
26
|
+
return stdout
|
|
27
|
+
.split("\n")
|
|
28
|
+
.map((l) => l.trim())
|
|
29
|
+
.filter((l) => l.length > 0);
|
|
30
|
+
}
|
|
15
31
|
async function commitReshipMetadata(opts) {
|
|
16
32
|
if (opts.dryRun) {
|
|
17
33
|
log.info(dim(" would commit new-cycle metadata (Status stays Shipped)"));
|
|
@@ -147,13 +163,39 @@ export async function runPostMergeNewCycle(taskRef, options = {}) {
|
|
|
147
163
|
process.exitCode = 1;
|
|
148
164
|
return;
|
|
149
165
|
}
|
|
166
|
+
const parts = parseTaskFilename(taskFile);
|
|
150
167
|
const dirtyBefore = await listWorkingTreeRelPaths(cwd);
|
|
151
|
-
if (dirtyBefore.length
|
|
152
|
-
|
|
153
|
-
|
|
168
|
+
if (dirtyBefore.length > 0) {
|
|
169
|
+
const message = await resolveShipCommitMessage({
|
|
170
|
+
cwd,
|
|
171
|
+
taskId: parts.id,
|
|
172
|
+
title: meta.title,
|
|
173
|
+
provided: options.message,
|
|
174
|
+
nonInteractive: options.nonInteractive === true,
|
|
175
|
+
});
|
|
176
|
+
await commitDirtyWorkingTree(cwd, message, false);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
// Work may already be committed on the task branch (not merged into the base yet).
|
|
180
|
+
let pending;
|
|
181
|
+
try {
|
|
182
|
+
pending = await unmergedFiles(cwd, remote, integrationBranch, options.noIntegrate !== true);
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
const msg = e instanceof Error ? e.message.split("\n")[0] : String(e);
|
|
186
|
+
log.error(`Could not compare ${taskId}'s branch with ${remote}/${integrationBranch}: ${msg}`);
|
|
187
|
+
process.exitCode = 1;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (pending.length === 0) {
|
|
191
|
+
log.info(`Nothing to do — ${taskId} is merged with no new changes. Create a new TASK for new work (${cyan("vibeops task add")}).`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
log.info(`No uncommitted changes; ${pending.length} file(s) committed on ${initialCtx.taskBranch} are not on ${remote}/${integrationBranch} yet — opening the PR for them.`);
|
|
195
|
+
if (options.message?.trim()) {
|
|
196
|
+
log.warn("-m/--message not used: there is nothing new to commit (existing commits keep their messages).");
|
|
197
|
+
}
|
|
154
198
|
}
|
|
155
|
-
const parts = parseTaskFilename(taskFile);
|
|
156
|
-
await commitDirtyWorkingTree(cwd, featCommitMessageFor(parts.id, meta.title), false);
|
|
157
199
|
let gitCtx = await readGitContext(taskFile);
|
|
158
200
|
if (gitCtx === null) {
|
|
159
201
|
log.error("Git Context missing after commit.");
|
package/dist/lib/task-push-pr.js
CHANGED
|
@@ -5,6 +5,7 @@ import { detectGitHost, mergeRequestLabel } from "./git-host.js";
|
|
|
5
5
|
import { requireGitConfig } from "./git-config.js";
|
|
6
6
|
import { gitCommitsAhead, gitDiffNameOnly, gitLogOneline, gitPush, gitRemoteUrl, readGitInfo, } from "./git.js";
|
|
7
7
|
import { dim, log } from "./logger.js";
|
|
8
|
+
import { fetchBaseBranch } from "./merge-base-freshness.js";
|
|
8
9
|
import { createMergeRequest, findMergeRequestByBranches, probeMergeRequestCli, } from "./pr-create.js";
|
|
9
10
|
import { fallbackTaskPr, generateTaskPrWithLlm } from "./task-pr-llm.js";
|
|
10
11
|
import { readGitContext, readTaskFile } from "./task.js";
|
|
@@ -15,9 +16,24 @@ function mergeRequestAlreadyExistsMessage(msg) {
|
|
|
15
16
|
lower.includes("merge request already exists") ||
|
|
16
17
|
lower.includes("pull request already exists"));
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* What the MR/PR changes is measured from the freshly fetched `<remote>/<base>`, not the
|
|
21
|
+
* TASK's Base Commit: after the base is merged into the task branch (required by the
|
|
22
|
+
* stale-base check), `<Base Commit>..HEAD` also contains other TASKs' commits. Falls back
|
|
23
|
+
* to the Base Commit only when the base branch cannot be fetched.
|
|
24
|
+
*/
|
|
25
|
+
async function resolveDiffBase(cwd, remote, gitCtx) {
|
|
26
|
+
try {
|
|
27
|
+
return await fetchBaseBranch(cwd, remote, gitCtx.baseBranch);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
log.warn(`Could not fetch ${remote}/${gitCtx.baseBranch} — listing changes since the TASK's Base Commit.`);
|
|
31
|
+
return gitCtx.baseCommit;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function buildDiffSummary(cwd, base) {
|
|
35
|
+
const files = await gitDiffNameOnly(cwd, `${base}...HEAD`);
|
|
36
|
+
const commits = await gitLogOneline(cwd, `${base}..HEAD`, { noMerges: true });
|
|
21
37
|
const lines = [];
|
|
22
38
|
if (commits.length > 0) {
|
|
23
39
|
lines.push("Commits:");
|
|
@@ -63,7 +79,8 @@ export async function finishTaskWithPullRequest(opts) {
|
|
|
63
79
|
if (git.branch !== gitCtx.taskBranch) {
|
|
64
80
|
log.warn(`Current branch is ${git.branch ?? "(detached)"}, not ${gitCtx.taskBranch}. Push may fail.`);
|
|
65
81
|
}
|
|
66
|
-
const
|
|
82
|
+
const diffBase = await resolveDiffBase(opts.cwd, gitCfg.remote, gitCtx);
|
|
83
|
+
const ahead = await gitCommitsAhead(opts.cwd, diffBase, "HEAD");
|
|
67
84
|
if (ahead === 0) {
|
|
68
85
|
log.warn("No commits ahead of base — pushing branch anyway.");
|
|
69
86
|
}
|
|
@@ -113,7 +130,7 @@ export async function finishTaskWithPullRequest(opts) {
|
|
|
113
130
|
const preference = config?.llm?.provider ?? "auto";
|
|
114
131
|
const meta = await readTaskFile(opts.taskFile);
|
|
115
132
|
const taskBody = await readText(opts.taskFile);
|
|
116
|
-
const diffSummary = await buildDiffSummary(opts.cwd,
|
|
133
|
+
const diffSummary = await buildDiffSummary(opts.cwd, diffBase);
|
|
117
134
|
const prInput = {
|
|
118
135
|
taskId: meta.id,
|
|
119
136
|
title: meta.title,
|