@goodtek/vibeops 2.1.1 → 2.1.3
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 +19 -4
- package/README.md +1 -1
- package/dist/cli.js +0 -0
- package/dist/commands/status.js +5 -3
- package/dist/commands/task-merge.js +17 -16
- package/dist/commands/task-reship.js +12 -39
- package/dist/commands/task-ship.js +16 -56
- package/dist/commands/task-sync.js +7 -8
- package/dist/lib/pr-create.js +93 -0
- package/dist/lib/task-effective-status.js +86 -23
- package/dist/lib/task-push-pr.js +20 -24
- package/dist/lib/task-reship.js +30 -5
- package/package.json +13 -11
package/CHANGELOG.md
CHANGED
|
@@ -4,14 +4,22 @@ All notable changes to VibeOps are documented here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
-
## 2.1.
|
|
7
|
+
## 2.1.3 - 2026-06-08
|
|
8
8
|
|
|
9
9
|
### Changed
|
|
10
10
|
|
|
11
|
-
- **
|
|
12
|
-
- **`task
|
|
11
|
+
- **MR/PR resolution:** `task merge`, `status`, and `task sync` resolve the open PR by **`(taskBranch, baseBranch)`** via `gh` / `glab` (host is source of truth).
|
|
12
|
+
- **`task ship` / `task reship`:** no longer write `Merge Request` or `Pushed At` into TASK md after PR creation — **single push, clean working tree**, CI once per PR.
|
|
13
|
+
- **`task reship`:** archives the current open PR URL into `Previous Merge Requests` (from host lookup) in the reship metadata commit.
|
|
14
|
+
- **Legacy TASK files** that still have `Merge Request:` in Git Context remain supported as a fallback.
|
|
13
15
|
|
|
14
|
-
## 2.1.
|
|
16
|
+
## 2.1.2 - 2026-06-08
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **`task ship`** / **`task reship`**: commit ship/reship metadata **before** push and MR/PR creation so the branch is pushed once (avoids duplicate `pull_request` CI runs on `opened` + `synchronize`).
|
|
21
|
+
|
|
22
|
+
## 2.1.1 - 2026-06-08
|
|
15
23
|
|
|
16
24
|
### Added
|
|
17
25
|
|
|
@@ -19,6 +27,13 @@ All notable changes to VibeOps are documented here.
|
|
|
19
27
|
|
|
20
28
|
### Changed
|
|
21
29
|
|
|
30
|
+
- **Docs:** living markdown, templates, and Cursor rules aligned with 2.1 lifecycle (`task reship`, two-status model, sync does not edit TASK md).
|
|
31
|
+
- **`task sync` CLI help:** description no longer says "mark Done".
|
|
32
|
+
|
|
33
|
+
## 2.1.0 - 2026-06-03
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
22
37
|
- **TASK md status:** only **In Progress** → **Shipped** (`task ship`). Merge (CLI/UI) and **`task sync`** do not edit the TASK file.
|
|
23
38
|
- **`task sync`**: Git cleanup only (integration pull, delete `task/*` branches).
|
|
24
39
|
- **`task add`**: blocks only another **In Progress** TASK (not **Shipped**).
|
package/README.md
CHANGED
|
@@ -106,7 +106,7 @@ pnpm install && pnpm build && pnpm smoke
|
|
|
106
106
|
|
|
107
107
|
- **Init** records branch policy in `.vibeops.json` (`integrationBranch`, `productionBranch`, `host`).
|
|
108
108
|
- **`task add`**: `task/<slug>` from **integration** (e.g. `develop`).
|
|
109
|
-
- **`task ship`**: commit → push → MR/PR
|
|
109
|
+
- **`task ship`**: commit implementation + ship metadata (Status **Shipped**) → push once → MR/PR (URL on host; `task merge` resolves by branch).
|
|
110
110
|
- **`task reship`**: Shipped TASK only; integrates `develop`; opens **new** PR (archives previous MR in Git Context).
|
|
111
111
|
- **`task merge`**: merge MR/PR into integration (CLI or host UI; TASK md unchanged).
|
|
112
112
|
- **`task sync`**: integration ff-only pull → delete `task/*` branches (TASK md unchanged).
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/commands/status.js
CHANGED
|
@@ -8,7 +8,7 @@ import { bold, cyan, dim, green, log, yellow } from "../lib/logger.js";
|
|
|
8
8
|
import { projectPaths } from "../lib/paths.js";
|
|
9
9
|
import { VERSION } from "../version.js";
|
|
10
10
|
import { computeNextHint, hintToText, listInProgressTasks, pickFocusTask, relPath, taskBranchExistsFor, } from "../lib/task-context.js";
|
|
11
|
-
import { isMergeRequestMerged, taskNeedsSync } from "../lib/task-effective-status.js";
|
|
11
|
+
import { hasOpenTaskMergeRequest, isMergeRequestMerged, resolveTaskMergeRequestUrl, taskNeedsSync, } from "../lib/task-effective-status.js";
|
|
12
12
|
import { countTasks, hasNonEmptySection, isOnTaskBranch, loadActionableTasks, readGitContext, statusDisplay, } from "../lib/task.js";
|
|
13
13
|
import { summarizeGoal } from "../lib/task-summary.js";
|
|
14
14
|
async function buildReport(cwd) {
|
|
@@ -42,7 +42,7 @@ async function buildReport(cwd) {
|
|
|
42
42
|
testFilled = hasNonEmptySection(body, "Test Result");
|
|
43
43
|
const ctx = await readGitContext(focus.filePath);
|
|
44
44
|
taskBranch = ctx?.taskBranch ?? null;
|
|
45
|
-
mergeRequestUrl =
|
|
45
|
+
mergeRequestUrl = await resolveTaskMergeRequestUrl(cwd, focus.filePath);
|
|
46
46
|
onTaskBranch = isOnTaskBranch(git.branch, ctx);
|
|
47
47
|
branchExists = await taskBranchExistsFor(cwd, focus);
|
|
48
48
|
mergeRequestMerged = await isMergeRequestMerged(cwd, focus);
|
|
@@ -67,7 +67,9 @@ async function buildReport(cwd) {
|
|
|
67
67
|
resultFilled,
|
|
68
68
|
testFilled,
|
|
69
69
|
onTaskBranch,
|
|
70
|
-
hasMergeRequest:
|
|
70
|
+
hasMergeRequest: focus !== null
|
|
71
|
+
? await hasOpenTaskMergeRequest(cwd, focus.filePath)
|
|
72
|
+
: false,
|
|
71
73
|
mergeRequestMerged,
|
|
72
74
|
needsSync,
|
|
73
75
|
hasLocalChanges: git.dirty === true,
|
|
@@ -8,6 +8,7 @@ import { taskNotFoundMessage } from "../lib/resolve-task.js";
|
|
|
8
8
|
import { getMergeRequestState, mergeMergeRequest, probeMergeRequestCli, } from "../lib/pr-create.js";
|
|
9
9
|
import { relPath } from "../lib/task-context.js";
|
|
10
10
|
import { resolveLifecycleTarget } from "../lib/task-lifecycle-target.js";
|
|
11
|
+
import { resolveOpenTaskMergeRequest } from "../lib/task-effective-status.js";
|
|
11
12
|
import { readGitContext } from "../lib/task.js";
|
|
12
13
|
function resolveMergeMethod(opts) {
|
|
13
14
|
if (opts.rebase === true)
|
|
@@ -49,26 +50,26 @@ export async function taskMergeCommand(taskRef, options = {}) {
|
|
|
49
50
|
process.exitCode = 1;
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
log.error("No Merge Request URL on TASK — run task ship first.");
|
|
53
|
+
const resolvedMr = dryRun
|
|
54
|
+
? null
|
|
55
|
+
: await resolveOpenTaskMergeRequest(cwd, target.taskFile);
|
|
56
|
+
if (!dryRun && resolvedMr === null) {
|
|
57
|
+
log.error(`No open MR/PR for ${ctx.taskBranch} → ${ctx.baseBranch}. Run task ship or task reship first.`);
|
|
60
58
|
process.exitCode = 1;
|
|
61
59
|
return;
|
|
62
60
|
}
|
|
61
|
+
const mergeRequestUrl = resolvedMr?.url ??
|
|
62
|
+
`(dry-run: would resolve open MR for ${ctx.taskBranch} → ${ctx.baseBranch})`;
|
|
63
63
|
const remoteUrl = await gitRemoteUrl(cwd, gitCfg.remote);
|
|
64
|
-
const host =
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
const host = resolvedMr?.host ??
|
|
65
|
+
(remoteUrl !== null && detectGitHost(remoteUrl) !== null
|
|
66
|
+
? detectGitHost(remoteUrl)
|
|
67
|
+
: gitCfg.host);
|
|
67
68
|
const method = resolveMergeMethod(options);
|
|
68
69
|
const label = mergeRequestLabel(host);
|
|
69
70
|
log.info(bold(`vibeops task merge ${target.taskId}`));
|
|
70
71
|
log.info(` ${dim("file")} ${relPath(cwd, target.taskFile)}`);
|
|
71
|
-
log.info(` ${dim("MR/PR")} ${
|
|
72
|
+
log.info(` ${dim("MR/PR")} ${mergeRequestUrl}`);
|
|
72
73
|
log.info(` ${dim("target")} ${ctx.baseBranch} (${gitCfg.integrationBranch})`);
|
|
73
74
|
log.info(` ${dim("method")} ${method}`);
|
|
74
75
|
log.blank();
|
|
@@ -76,7 +77,7 @@ export async function taskMergeCommand(taskRef, options = {}) {
|
|
|
76
77
|
await mergeMergeRequest({
|
|
77
78
|
cwd,
|
|
78
79
|
host,
|
|
79
|
-
url:
|
|
80
|
+
url: mergeRequestUrl,
|
|
80
81
|
method,
|
|
81
82
|
dryRun: true,
|
|
82
83
|
});
|
|
@@ -84,7 +85,7 @@ export async function taskMergeCommand(taskRef, options = {}) {
|
|
|
84
85
|
log.info(`Next: ${cyan("vibeops task sync")}`);
|
|
85
86
|
return;
|
|
86
87
|
}
|
|
87
|
-
const state = await getMergeRequestState(cwd, host,
|
|
88
|
+
const state = await getMergeRequestState(cwd, host, mergeRequestUrl);
|
|
88
89
|
if (state === "merged") {
|
|
89
90
|
log.ok(`${label} already merged.`);
|
|
90
91
|
log.blank();
|
|
@@ -100,7 +101,7 @@ export async function taskMergeCommand(taskRef, options = {}) {
|
|
|
100
101
|
return;
|
|
101
102
|
}
|
|
102
103
|
try {
|
|
103
|
-
await mergeMergeRequest({ cwd, host, url:
|
|
104
|
+
await mergeMergeRequest({ cwd, host, url: mergeRequestUrl, method });
|
|
104
105
|
log.ok(`${label} merged into ${ctx.baseBranch}.`);
|
|
105
106
|
}
|
|
106
107
|
catch (e) {
|
|
@@ -119,7 +120,7 @@ export async function taskMergeCommand(taskRef, options = {}) {
|
|
|
119
120
|
else {
|
|
120
121
|
log.warn(`Could not read ${label} state — attempting merge anyway.`);
|
|
121
122
|
try {
|
|
122
|
-
await mergeMergeRequest({ cwd, host, url:
|
|
123
|
+
await mergeMergeRequest({ cwd, host, url: mergeRequestUrl, method });
|
|
123
124
|
log.ok(`${label} merge command completed.`);
|
|
124
125
|
}
|
|
125
126
|
catch (e) {
|
|
@@ -3,36 +3,21 @@ import { readText } from "../lib/filesystem.js";
|
|
|
3
3
|
import { bold, cyan, dim, green, log, yellow } from "../lib/logger.js";
|
|
4
4
|
import { projectPaths } from "../lib/paths.js";
|
|
5
5
|
import { relPath } from "../lib/task-context.js";
|
|
6
|
-
import { commitDirtyWorkingTree, docsCommitMessageFor, featCommitMessageFor,
|
|
6
|
+
import { commitDirtyWorkingTree, docsCommitMessageFor, featCommitMessageFor, } from "../lib/task-git-commit.js";
|
|
7
7
|
import { finishTaskWithPullRequest } from "../lib/task-push-pr.js";
|
|
8
8
|
import { applyTaskShipMemory, llmCompleteTaskReship, writeTaskResultSections, } from "../lib/task-ship-llm.js";
|
|
9
9
|
import { assertMergeRequestNotOpen, ensureTaskBranchForReship, integrateIntegrationIntoTaskBranch, loadGitConfigForReship, prepareGitContextForReship, refreshGitContextBaseCommit, resolveReshipTarget, } from "../lib/task-reship.js";
|
|
10
|
-
import { hasNonEmptySection, parseTaskFilename, readGitContext,
|
|
10
|
+
import { hasNonEmptySection, parseTaskFilename, readGitContext, } from "../lib/task.js";
|
|
11
11
|
import { listWorkingTreeRelPaths } from "../lib/git.js";
|
|
12
|
-
async function
|
|
12
|
+
async function commitReshipMetadata(opts) {
|
|
13
13
|
if (opts.dryRun) {
|
|
14
14
|
log.info(dim(" would commit reship metadata (Status stays Shipped)"));
|
|
15
|
-
|
|
16
|
-
log.info(dim(` would git push ${opts.push.remote} ${opts.push.branch} (reship metadata)`));
|
|
17
|
-
}
|
|
18
|
-
return true;
|
|
15
|
+
return;
|
|
19
16
|
}
|
|
20
17
|
const committed = await commitDirtyWorkingTree(opts.cwd, docsCommitMessageFor(opts.taskId, "reship metadata"), false);
|
|
21
18
|
if (!committed) {
|
|
22
19
|
log.info(dim("No reship metadata commit (Git Context already on disk)."));
|
|
23
20
|
}
|
|
24
|
-
if (opts.push === null)
|
|
25
|
-
return true;
|
|
26
|
-
try {
|
|
27
|
-
await pushBranch(opts.cwd, opts.push.remote, opts.push.branch, false);
|
|
28
|
-
log.ok(`Pushed reship metadata → ${opts.push.remote}/${opts.push.branch}`);
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
catch (e) {
|
|
32
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
33
|
-
log.error(`Reship metadata push failed: ${msg}`);
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
21
|
}
|
|
37
22
|
export async function taskReshipCommand(taskRef, options = {}) {
|
|
38
23
|
const cwd = resolve(options.cwd ?? process.cwd());
|
|
@@ -129,8 +114,7 @@ export async function taskReshipCommand(taskRef, options = {}) {
|
|
|
129
114
|
log.blank();
|
|
130
115
|
if (dryRun) {
|
|
131
116
|
log.info(bold("dry-run — would also:"));
|
|
132
|
-
log.info(" ·
|
|
133
|
-
log.info(" · feat commit + reship metadata push");
|
|
117
|
+
log.info(" · feat commit + reship metadata, then push once and open MR/PR (unless --no-pr)");
|
|
134
118
|
log.blank();
|
|
135
119
|
log.info(`Next: ${cyan("vibeops task merge")}, then optional ${cyan("task sync")}`);
|
|
136
120
|
return;
|
|
@@ -155,8 +139,13 @@ export async function taskReshipCommand(taskRef, options = {}) {
|
|
|
155
139
|
return;
|
|
156
140
|
}
|
|
157
141
|
gitCtx = await refreshGitContextBaseCommit(taskFile, gitCtx, cwd);
|
|
158
|
-
gitCtx = await prepareGitContextForReship(taskFile, gitCtx);
|
|
159
|
-
await
|
|
142
|
+
gitCtx = await prepareGitContextForReship(cwd, taskFile, gitCtx);
|
|
143
|
+
await commitReshipMetadata({
|
|
144
|
+
cwd,
|
|
145
|
+
taskFile,
|
|
146
|
+
taskId: parts.id,
|
|
147
|
+
dryRun: false,
|
|
148
|
+
});
|
|
160
149
|
const prResult = await finishTaskWithPullRequest({
|
|
161
150
|
cwd,
|
|
162
151
|
taskFile,
|
|
@@ -171,22 +160,6 @@ export async function taskReshipCommand(taskRef, options = {}) {
|
|
|
171
160
|
if (prResult.mergeRequestUrl) {
|
|
172
161
|
log.info(` ${dim("MR/PR")} ${prResult.mergeRequestUrl}`);
|
|
173
162
|
}
|
|
174
|
-
const gitCtxAfter = await readGitContext(taskFile);
|
|
175
|
-
let pushTarget = null;
|
|
176
|
-
if (gitCtxAfter !== null && gitCfg !== null) {
|
|
177
|
-
pushTarget = { remote: gitCfg.remote, branch: gitCtxAfter.taskBranch };
|
|
178
|
-
}
|
|
179
|
-
const pushed = await commitReshipMetadataAndPush({
|
|
180
|
-
cwd,
|
|
181
|
-
taskFile,
|
|
182
|
-
taskId: parts.id,
|
|
183
|
-
dryRun: false,
|
|
184
|
-
push: pushTarget,
|
|
185
|
-
});
|
|
186
|
-
if (!pushed) {
|
|
187
|
-
process.exitCode = 1;
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
163
|
log.blank();
|
|
191
164
|
log.info(`Next: ${cyan("vibeops task merge")} (or merge in the host UI), then ${cyan("vibeops task sync")}`);
|
|
192
165
|
}
|
|
@@ -1,42 +1,24 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { readText } from "../lib/filesystem.js";
|
|
3
|
-
import { requireGitConfig } from "../lib/git-config.js";
|
|
4
3
|
import { bold, cyan, dim, green, log, yellow } from "../lib/logger.js";
|
|
5
4
|
import { projectPaths } from "../lib/paths.js";
|
|
6
5
|
import { resolveTask, taskNotFoundMessage } from "../lib/resolve-task.js";
|
|
7
6
|
import { relPath } from "../lib/task-context.js";
|
|
8
|
-
import { commitDirtyWorkingTree, docsCommitMessageFor, featCommitMessageFor,
|
|
7
|
+
import { commitDirtyWorkingTree, docsCommitMessageFor, featCommitMessageFor, } from "../lib/task-git-commit.js";
|
|
9
8
|
import { applyTaskShipMemory, fallbackResultSections, llmCompleteTaskShip, writeTaskResultSections, } from "../lib/task-ship-llm.js";
|
|
10
9
|
import { finishTaskWithPullRequest } from "../lib/task-push-pr.js";
|
|
11
10
|
import { hasNonEmptySection, parseTaskFilename, readGitContext, readTaskFile, updateInlineStatus, } from "../lib/task.js";
|
|
12
|
-
async function
|
|
11
|
+
async function commitShipMetadata(opts) {
|
|
13
12
|
if (opts.dryRun) {
|
|
14
13
|
log.info(dim(" would set Status → Shipped"));
|
|
15
14
|
log.info(dim(" would commit ship metadata"));
|
|
16
|
-
|
|
17
|
-
log.info(dim(` would git push ${opts.push.remote} ${opts.push.branch} (ship metadata)`));
|
|
18
|
-
}
|
|
19
|
-
return true;
|
|
15
|
+
return;
|
|
20
16
|
}
|
|
21
17
|
await updateInlineStatus(opts.taskFile, "shipped");
|
|
22
18
|
log.ok("Status → Shipped");
|
|
23
19
|
const committed = await commitDirtyWorkingTree(opts.cwd, docsCommitMessageFor(opts.taskId, "mark shipped"), false);
|
|
24
20
|
if (!committed) {
|
|
25
21
|
log.warn("No file changes for ship metadata commit — check TASK Status on the remote branch.");
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
if (opts.push === null)
|
|
29
|
-
return true;
|
|
30
|
-
try {
|
|
31
|
-
await pushBranch(opts.cwd, opts.push.remote, opts.push.branch, false);
|
|
32
|
-
log.ok(`Pushed ship metadata → ${opts.push.remote}/${opts.push.branch}`);
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
catch (e) {
|
|
36
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
37
|
-
log.error(`Ship metadata push failed: ${msg}`);
|
|
38
|
-
log.info(dim(`Fix auth/remote, then: git push ${opts.push.remote} ${opts.push.branch}`));
|
|
39
|
-
return false;
|
|
40
22
|
}
|
|
41
23
|
}
|
|
42
24
|
export async function taskShipCommand(taskRef, options = {}) {
|
|
@@ -104,24 +86,20 @@ export async function taskShipCommand(taskRef, options = {}) {
|
|
|
104
86
|
if (!resultOk || !testOk) {
|
|
105
87
|
log.info(" · LLM fill Result / Test Result + patch docs/project/*");
|
|
106
88
|
}
|
|
107
|
-
log.info(" · commit implementation
|
|
108
|
-
log.info(" ·
|
|
109
|
-
|
|
89
|
+
log.info(" · commit implementation + ship metadata (Status → Shipped)");
|
|
90
|
+
log.info(" · push task branch once, open MR/PR (unless --no-pr)");
|
|
91
|
+
const partsDry = parseTaskFilename(taskFile);
|
|
92
|
+
await commitShipMetadata({
|
|
110
93
|
cwd,
|
|
111
94
|
taskFile,
|
|
95
|
+
taskId: partsDry.id,
|
|
112
96
|
dryRun: true,
|
|
113
|
-
skipPr: options.noPr === true,
|
|
114
97
|
});
|
|
115
|
-
|
|
116
|
-
const gitCfgDry = gitCtxDry !== null ? await requireGitConfig(cwd).catch(() => null) : null;
|
|
117
|
-
await commitShipMetadataAndPush({
|
|
98
|
+
await finishTaskWithPullRequest({
|
|
118
99
|
cwd,
|
|
119
100
|
taskFile,
|
|
120
|
-
taskId,
|
|
121
101
|
dryRun: true,
|
|
122
|
-
|
|
123
|
-
? { remote: gitCfgDry.remote, branch: gitCtxDry.taskBranch }
|
|
124
|
-
: null,
|
|
102
|
+
skipPr: options.noPr === true,
|
|
125
103
|
});
|
|
126
104
|
return;
|
|
127
105
|
}
|
|
@@ -129,6 +107,12 @@ export async function taskShipCommand(taskRef, options = {}) {
|
|
|
129
107
|
await commitDirtyWorkingTree(cwd, featCommitMessageFor(parts.id, meta.title), false);
|
|
130
108
|
const gitCtx = await readGitContext(taskFile);
|
|
131
109
|
if (gitCtx !== null) {
|
|
110
|
+
await commitShipMetadata({
|
|
111
|
+
cwd,
|
|
112
|
+
taskFile,
|
|
113
|
+
taskId: parts.id,
|
|
114
|
+
dryRun: false,
|
|
115
|
+
});
|
|
132
116
|
const prResult = await finishTaskWithPullRequest({
|
|
133
117
|
cwd,
|
|
134
118
|
taskFile,
|
|
@@ -146,30 +130,6 @@ export async function taskShipCommand(taskRef, options = {}) {
|
|
|
146
130
|
else {
|
|
147
131
|
log.info(dim("No Git Context — push/MR skipped."));
|
|
148
132
|
}
|
|
149
|
-
const gitCtxAfter = await readGitContext(taskFile);
|
|
150
|
-
let pushTarget = null;
|
|
151
|
-
if (gitCtxAfter !== null) {
|
|
152
|
-
try {
|
|
153
|
-
const gitCfg = await requireGitConfig(cwd);
|
|
154
|
-
pushTarget = { remote: gitCfg.remote, branch: gitCtxAfter.taskBranch };
|
|
155
|
-
}
|
|
156
|
-
catch (e) {
|
|
157
|
-
if (e instanceof Error)
|
|
158
|
-
log.warn(e.message);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
const shipped = await commitShipMetadataAndPush({
|
|
162
|
-
cwd,
|
|
163
|
-
taskFile,
|
|
164
|
-
taskId: parts.id,
|
|
165
|
-
dryRun: false,
|
|
166
|
-
push: pushTarget,
|
|
167
|
-
});
|
|
168
|
-
if (!shipped) {
|
|
169
|
-
log.error("Ship metadata push failed — fix manually, then merge the MR.");
|
|
170
|
-
process.exitCode = 1;
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
133
|
log.blank();
|
|
174
134
|
log.info(`Next: ${cyan("vibeops task merge")} (or merge in the host UI), then ${cyan("vibeops task sync")}`);
|
|
175
135
|
}
|
|
@@ -7,9 +7,9 @@ import { probeMergeRequestCli } from "../lib/pr-create.js";
|
|
|
7
7
|
import { projectPaths } from "../lib/paths.js";
|
|
8
8
|
import { taskNotFoundMessage } from "../lib/resolve-task.js";
|
|
9
9
|
import { relPath } from "../lib/task-context.js";
|
|
10
|
-
import {
|
|
10
|
+
import { getTaskMergeRequestLifecycle, isMergeRequestMerged, } from "../lib/task-effective-status.js";
|
|
11
11
|
import { resolveLifecycleTarget } from "../lib/task-lifecycle-target.js";
|
|
12
|
-
import {
|
|
12
|
+
import { readTaskFile } from "../lib/task.js";
|
|
13
13
|
function isProtectedBranch(name, integration, production) {
|
|
14
14
|
return name === integration || name === production || name === "main" || name === "master";
|
|
15
15
|
}
|
|
@@ -42,7 +42,6 @@ export async function taskSyncCommand(taskRef, options = {}) {
|
|
|
42
42
|
}
|
|
43
43
|
const { integrationBranch, productionBranch, remote } = gitCfg;
|
|
44
44
|
const { taskId, taskBranch, taskFile } = target;
|
|
45
|
-
const ctx = await readGitContext(taskFile);
|
|
46
45
|
if (isProtectedBranch(taskBranch, integrationBranch, productionBranch)) {
|
|
47
46
|
log.error(`Refusing to delete protected branch: ${taskBranch}`);
|
|
48
47
|
process.exitCode = 1;
|
|
@@ -55,18 +54,18 @@ export async function taskSyncCommand(taskRef, options = {}) {
|
|
|
55
54
|
log.info(` ${dim("note")} ${dim("Git cleanup only — TASK md stays Shipped")}`);
|
|
56
55
|
log.blank();
|
|
57
56
|
const meta = await readTaskFile(taskFile);
|
|
58
|
-
if (
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
57
|
+
if (!dryRun) {
|
|
58
|
+
const lifecycle = await getTaskMergeRequestLifecycle(cwd, taskFile);
|
|
59
|
+
if (lifecycle !== "none") {
|
|
61
60
|
const remoteUrl = await gitRemoteUrl(cwd, remote);
|
|
62
61
|
const host = remoteUrl !== null && detectGitHost(remoteUrl) !== null
|
|
63
62
|
? detectGitHost(remoteUrl)
|
|
64
63
|
: gitCfg.host;
|
|
65
64
|
const label = mergeRequestLabel(host);
|
|
66
|
-
if (
|
|
65
|
+
if (lifecycle === "open") {
|
|
67
66
|
log.warn(`${yellow(label)} still open — merge with task merge or the host UI before sync.`);
|
|
68
67
|
}
|
|
69
|
-
else if (
|
|
68
|
+
else if (lifecycle === "merged") {
|
|
70
69
|
log.ok(`${label} merged.`);
|
|
71
70
|
}
|
|
72
71
|
}
|
package/dist/lib/pr-create.js
CHANGED
|
@@ -64,6 +64,99 @@ export async function probeMergeRequestCli(host) {
|
|
|
64
64
|
return false;
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
+
function normalizeGhMergeRequestState(raw) {
|
|
68
|
+
const state = raw.trim().toUpperCase();
|
|
69
|
+
if (state === "MERGED")
|
|
70
|
+
return "merged";
|
|
71
|
+
if (state === "OPEN")
|
|
72
|
+
return "open";
|
|
73
|
+
if (state === "CLOSED")
|
|
74
|
+
return "closed";
|
|
75
|
+
return "unknown";
|
|
76
|
+
}
|
|
77
|
+
function normalizeGlabMergeRequestState(raw) {
|
|
78
|
+
const state = raw.trim().toLowerCase();
|
|
79
|
+
if (state === "merged")
|
|
80
|
+
return "merged";
|
|
81
|
+
if (state === "opened" || state === "open")
|
|
82
|
+
return "open";
|
|
83
|
+
if (state === "closed")
|
|
84
|
+
return "closed";
|
|
85
|
+
return "unknown";
|
|
86
|
+
}
|
|
87
|
+
async function ghFindMergeRequestByBranches(opts) {
|
|
88
|
+
const state = opts.state ?? "open";
|
|
89
|
+
const { stdout } = await execFileAsync("gh", [
|
|
90
|
+
"pr",
|
|
91
|
+
"list",
|
|
92
|
+
"--head",
|
|
93
|
+
opts.headBranch,
|
|
94
|
+
"--base",
|
|
95
|
+
opts.baseBranch,
|
|
96
|
+
"--state",
|
|
97
|
+
state,
|
|
98
|
+
"--limit",
|
|
99
|
+
"1",
|
|
100
|
+
"--json",
|
|
101
|
+
"url,state",
|
|
102
|
+
], { cwd: opts.cwd, maxBuffer: 1024 * 1024 });
|
|
103
|
+
const trimmed = stdout.trim();
|
|
104
|
+
if (trimmed.length === 0 || trimmed === "[]")
|
|
105
|
+
return null;
|
|
106
|
+
const rows = JSON.parse(trimmed);
|
|
107
|
+
const row = rows[0];
|
|
108
|
+
if (row?.url === undefined || row.url.length === 0)
|
|
109
|
+
return null;
|
|
110
|
+
return {
|
|
111
|
+
url: row.url,
|
|
112
|
+
state: normalizeGhMergeRequestState(row.state ?? ""),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async function glabFindMergeRequestByBranches(opts) {
|
|
116
|
+
const state = opts.state ?? "open";
|
|
117
|
+
const glabState = state === "open" ? "opened" : state === "merged" ? "merged" : state === "closed" ? "closed" : "all";
|
|
118
|
+
const { stdout } = await execFileAsync("glab", [
|
|
119
|
+
"mr",
|
|
120
|
+
"list",
|
|
121
|
+
"--source-branch",
|
|
122
|
+
opts.headBranch,
|
|
123
|
+
"--target-branch",
|
|
124
|
+
opts.baseBranch,
|
|
125
|
+
"-S",
|
|
126
|
+
glabState,
|
|
127
|
+
"-P",
|
|
128
|
+
"1",
|
|
129
|
+
"--output",
|
|
130
|
+
"json",
|
|
131
|
+
"web_url,state",
|
|
132
|
+
], { cwd: opts.cwd, maxBuffer: 1024 * 1024 });
|
|
133
|
+
const trimmed = stdout.trim();
|
|
134
|
+
if (trimmed.length === 0 || trimmed === "[]")
|
|
135
|
+
return null;
|
|
136
|
+
const rows = JSON.parse(trimmed);
|
|
137
|
+
const row = rows[0];
|
|
138
|
+
const url = row?.web_url?.trim();
|
|
139
|
+
if (url === undefined || url.length === 0)
|
|
140
|
+
return null;
|
|
141
|
+
return {
|
|
142
|
+
url,
|
|
143
|
+
state: normalizeGlabMergeRequestState(row.state ?? ""),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/** Resolve MR/PR by `(headBranch, baseBranch)` — source of truth for ship/reship (no TASK URL). */
|
|
147
|
+
export async function findMergeRequestByBranches(opts) {
|
|
148
|
+
try {
|
|
149
|
+
if (!(await probeMergeRequestCli(opts.host)))
|
|
150
|
+
return null;
|
|
151
|
+
if (opts.host === "gitlab") {
|
|
152
|
+
return glabFindMergeRequestByBranches(opts);
|
|
153
|
+
}
|
|
154
|
+
return ghFindMergeRequestByBranches(opts);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
67
160
|
function prRefFromUrl(url) {
|
|
68
161
|
const trimmed = url.trim();
|
|
69
162
|
const pullMatch = /\/pull\/(\d+)/i.exec(trimmed);
|
|
@@ -1,37 +1,103 @@
|
|
|
1
1
|
import { GitConfigError, requireGitConfig } from "./git-config.js";
|
|
2
2
|
import { detectGitHost } from "./git-host.js";
|
|
3
3
|
import { gitBranchExists, gitRemoteBranchExists, gitRemoteUrl, } from "./git.js";
|
|
4
|
-
import { getMergeRequestState, probeMergeRequestCli } from "./pr-create.js";
|
|
4
|
+
import { findMergeRequestByBranches, getMergeRequestState, probeMergeRequestCli, } from "./pr-create.js";
|
|
5
5
|
import { readGitContext } from "./task.js";
|
|
6
|
-
|
|
7
|
-
export async function getMergeRequestStateForTask(cwd, taskFile) {
|
|
8
|
-
const ctx = await readGitContext(taskFile);
|
|
9
|
-
const url = ctx?.mergeRequestUrl?.trim();
|
|
10
|
-
if (url === undefined || url.length === 0)
|
|
11
|
-
return null;
|
|
12
|
-
let host;
|
|
6
|
+
export async function resolveGitHostForCwd(cwd) {
|
|
13
7
|
try {
|
|
14
8
|
const gitCfg = await requireGitConfig(cwd);
|
|
15
9
|
const remoteUrl = await gitRemoteUrl(cwd, gitCfg.remote);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
10
|
+
if (remoteUrl !== null && detectGitHost(remoteUrl) !== null) {
|
|
11
|
+
return detectGitHost(remoteUrl);
|
|
12
|
+
}
|
|
13
|
+
return gitCfg.host;
|
|
20
14
|
}
|
|
21
15
|
catch (e) {
|
|
22
16
|
if (e instanceof GitConfigError)
|
|
23
17
|
return null;
|
|
24
18
|
throw e;
|
|
25
19
|
}
|
|
26
|
-
|
|
20
|
+
}
|
|
21
|
+
async function readTaskGitContext(taskFile) {
|
|
22
|
+
return readGitContext(taskFile);
|
|
23
|
+
}
|
|
24
|
+
async function resolveFromBranches(cwd, ctx, state) {
|
|
25
|
+
try {
|
|
26
|
+
const host = await resolveGitHostForCwd(cwd);
|
|
27
|
+
if (host === null)
|
|
28
|
+
return null;
|
|
29
|
+
const found = await findMergeRequestByBranches({
|
|
30
|
+
cwd,
|
|
31
|
+
host,
|
|
32
|
+
headBranch: ctx.taskBranch,
|
|
33
|
+
baseBranch: ctx.baseBranch,
|
|
34
|
+
state,
|
|
35
|
+
});
|
|
36
|
+
if (found === null)
|
|
37
|
+
return null;
|
|
38
|
+
return { url: found.url, host, state: found.state, source: "branch" };
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function resolveFromTaskFileUrl(cwd, ctx) {
|
|
45
|
+
const url = ctx.mergeRequestUrl?.trim();
|
|
46
|
+
if (url === undefined || url.length === 0)
|
|
47
|
+
return null;
|
|
48
|
+
const host = await resolveGitHostForCwd(cwd);
|
|
49
|
+
if (host === null || !(await probeMergeRequestCli(host)))
|
|
27
50
|
return null;
|
|
28
|
-
|
|
51
|
+
const state = await getMergeRequestState(cwd, host, url);
|
|
52
|
+
return { url, host, state, source: "task-file" };
|
|
53
|
+
}
|
|
54
|
+
/** Open MR/PR for a TASK — branch lookup first, legacy TASK URL fallback. */
|
|
55
|
+
export async function resolveOpenTaskMergeRequest(cwd, taskFile) {
|
|
56
|
+
const ctx = await readTaskGitContext(taskFile);
|
|
57
|
+
if (ctx === null)
|
|
58
|
+
return null;
|
|
59
|
+
const fromBranch = await resolveFromBranches(cwd, ctx, "open");
|
|
60
|
+
if (fromBranch !== null)
|
|
61
|
+
return fromBranch;
|
|
62
|
+
const legacy = await resolveFromTaskFileUrl(cwd, ctx);
|
|
63
|
+
if (legacy !== null && legacy.state === "open")
|
|
64
|
+
return legacy;
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
/** MR lifecycle for hints, sync, and status — prefers host branch lookup. */
|
|
68
|
+
export async function getTaskMergeRequestLifecycle(cwd, taskFile) {
|
|
69
|
+
const ctx = await readTaskGitContext(taskFile);
|
|
70
|
+
if (ctx === null)
|
|
71
|
+
return "none";
|
|
72
|
+
const open = await resolveFromBranches(cwd, ctx, "open");
|
|
73
|
+
if (open !== null)
|
|
74
|
+
return "open";
|
|
75
|
+
const merged = await resolveFromBranches(cwd, ctx, "merged");
|
|
76
|
+
if (merged !== null)
|
|
77
|
+
return "merged";
|
|
78
|
+
const legacy = await resolveFromTaskFileUrl(cwd, ctx);
|
|
79
|
+
if (legacy !== null)
|
|
80
|
+
return legacy.state;
|
|
81
|
+
return "none";
|
|
82
|
+
}
|
|
83
|
+
/** @deprecated Use {@link getTaskMergeRequestLifecycle} or {@link resolveOpenTaskMergeRequest}. */
|
|
84
|
+
export async function getMergeRequestStateForTask(cwd, taskFile) {
|
|
85
|
+
const lifecycle = await getTaskMergeRequestLifecycle(cwd, taskFile);
|
|
86
|
+
if (lifecycle === "none")
|
|
87
|
+
return null;
|
|
88
|
+
return lifecycle;
|
|
89
|
+
}
|
|
90
|
+
export async function hasOpenTaskMergeRequest(cwd, taskFile) {
|
|
91
|
+
return (await getTaskMergeRequestLifecycle(cwd, taskFile)) === "open";
|
|
92
|
+
}
|
|
93
|
+
export async function resolveTaskMergeRequestUrl(cwd, taskFile) {
|
|
94
|
+
const open = await resolveOpenTaskMergeRequest(cwd, taskFile);
|
|
95
|
+
return open?.url ?? null;
|
|
29
96
|
}
|
|
30
97
|
export async function isMergeRequestMerged(cwd, task) {
|
|
31
98
|
if (task.status !== "shipped")
|
|
32
99
|
return false;
|
|
33
|
-
|
|
34
|
-
return state === "merged";
|
|
100
|
+
return (await getTaskMergeRequestLifecycle(cwd, task.filePath)) === "merged";
|
|
35
101
|
}
|
|
36
102
|
/**
|
|
37
103
|
* After ship: optional `task sync` when MR is merged (or no MR) but `task/*` still exists.
|
|
@@ -39,15 +105,12 @@ export async function isMergeRequestMerged(cwd, task) {
|
|
|
39
105
|
export async function taskNeedsSync(cwd, task) {
|
|
40
106
|
if (task.status !== "shipped")
|
|
41
107
|
return false;
|
|
42
|
-
const ctx = await
|
|
108
|
+
const ctx = await readTaskGitContext(task.filePath);
|
|
43
109
|
if (ctx === null)
|
|
44
110
|
return false;
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
if (state !== "merged")
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
111
|
+
const lifecycle = await getTaskMergeRequestLifecycle(cwd, task.filePath);
|
|
112
|
+
if (lifecycle === "open")
|
|
113
|
+
return false;
|
|
51
114
|
if (await gitBranchExists(cwd, ctx.taskBranch))
|
|
52
115
|
return true;
|
|
53
116
|
try {
|
package/dist/lib/task-push-pr.js
CHANGED
|
@@ -4,9 +4,9 @@ import { detectGitHost, mergeRequestLabel } from "./git-host.js";
|
|
|
4
4
|
import { requireGitConfig } from "./git-config.js";
|
|
5
5
|
import { gitCommitsAhead, gitDiffNameOnly, gitLogOneline, gitPush, gitRemoteUrl, readGitInfo, } from "./git.js";
|
|
6
6
|
import { dim, log } from "./logger.js";
|
|
7
|
-
import { createMergeRequest, probeMergeRequestCli } from "./pr-create.js";
|
|
7
|
+
import { createMergeRequest, findMergeRequestByBranches, probeMergeRequestCli, } from "./pr-create.js";
|
|
8
8
|
import { fallbackTaskPr, generateTaskPrWithLlm } from "./task-pr-llm.js";
|
|
9
|
-
import { readGitContext, readTaskFile
|
|
9
|
+
import { readGitContext, readTaskFile } from "./task.js";
|
|
10
10
|
async function buildDiffSummary(cwd, gitCtx) {
|
|
11
11
|
const files = await gitDiffNameOnly(cwd, `${gitCtx.baseCommit}..HEAD`);
|
|
12
12
|
const commits = await gitLogOneline(cwd, `${gitCtx.baseCommit}..HEAD`);
|
|
@@ -28,12 +28,7 @@ async function buildDiffSummary(cwd, gitCtx) {
|
|
|
28
28
|
}
|
|
29
29
|
return lines.join("\n") || "(no commits since task start)";
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
if (typeof url !== "string" || url.length === 0)
|
|
33
|
-
return false;
|
|
34
|
-
return url.startsWith("http://") || url.startsWith("https://");
|
|
35
|
-
}
|
|
36
|
-
/** Push task branch and open MR/PR (idempotent when URL already in Git Context). */
|
|
31
|
+
/** Push task branch and open MR/PR. Does not write MR URL into TASK md (host is source of truth). */
|
|
37
32
|
export async function finishTaskWithPullRequest(opts) {
|
|
38
33
|
let gitCfg = await requireGitConfig(opts.cwd);
|
|
39
34
|
const gitCtx = await readGitContext(opts.taskFile);
|
|
@@ -41,8 +36,9 @@ export async function finishTaskWithPullRequest(opts) {
|
|
|
41
36
|
log.warn("No Git Context on TASK — push and merge request skipped.");
|
|
42
37
|
return { ok: true };
|
|
43
38
|
}
|
|
39
|
+
const label = mergeRequestLabel(gitCfg.host);
|
|
44
40
|
if (opts.dryRun) {
|
|
45
|
-
log.info(dim(`would git push -u ${gitCfg.remote} ${gitCtx.taskBranch} and open ${
|
|
41
|
+
log.info(dim(`would git push -u ${gitCfg.remote} ${gitCtx.taskBranch} and open ${label} → ${gitCtx.baseBranch}`));
|
|
46
42
|
return { ok: true, mergeRequestUrl: "(dry-run)" };
|
|
47
43
|
}
|
|
48
44
|
const remoteUrl = await gitRemoteUrl(opts.cwd, gitCfg.remote);
|
|
@@ -59,10 +55,18 @@ export async function finishTaskWithPullRequest(opts) {
|
|
|
59
55
|
if (git.branch !== gitCtx.taskBranch) {
|
|
60
56
|
log.warn(`Current branch is ${git.branch ?? "(detached)"}, not ${gitCtx.taskBranch}. Push may fail.`);
|
|
61
57
|
}
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
+
}
|
|
66
70
|
}
|
|
67
71
|
const ahead = await gitCommitsAhead(opts.cwd, gitCtx.baseCommit, "HEAD");
|
|
68
72
|
if (ahead === 0) {
|
|
@@ -77,17 +81,14 @@ export async function finishTaskWithPullRequest(opts) {
|
|
|
77
81
|
log.error(`git push failed: ${msg}`);
|
|
78
82
|
return { ok: false };
|
|
79
83
|
}
|
|
80
|
-
const pushedAt = new Date().toISOString();
|
|
81
84
|
if (opts.skipPr === true) {
|
|
82
|
-
await upsertGitContext(opts.taskFile, { ...gitCtx, pushedAt });
|
|
83
85
|
log.info(dim("Merge request creation skipped (--no-pr)."));
|
|
84
86
|
return { ok: true, pushed: true };
|
|
85
87
|
}
|
|
86
88
|
const cliOk = await probeMergeRequestCli(gitCfg.host);
|
|
87
89
|
if (!cliOk) {
|
|
88
90
|
const tool = gitCfg.host === "gitlab" ? "glab" : "gh";
|
|
89
|
-
|
|
90
|
-
log.warn(`${tool} CLI not found — create the ${mergeRequestLabel(gitCfg.host)} manually: ${gitCtx.taskBranch} → ${gitCtx.baseBranch}`);
|
|
91
|
+
log.warn(`${tool} CLI not found — create the ${label} manually: ${gitCtx.taskBranch} → ${gitCtx.baseBranch}`);
|
|
91
92
|
return { ok: true, pushed: true };
|
|
92
93
|
}
|
|
93
94
|
const config = await readConfig(opts.cwd);
|
|
@@ -114,18 +115,13 @@ export async function finishTaskWithPullRequest(opts) {
|
|
|
114
115
|
title: prTitle,
|
|
115
116
|
body: prBody,
|
|
116
117
|
});
|
|
117
|
-
log.ok(`${
|
|
118
|
-
|
|
119
|
-
...gitCtx,
|
|
120
|
-
mergeRequestUrl: url,
|
|
121
|
-
pushedAt,
|
|
122
|
-
});
|
|
118
|
+
log.ok(`${label}: ${url}`);
|
|
119
|
+
log.info(dim("MR/PR URL is on the host — not written to TASK md (use task merge)."));
|
|
123
120
|
return { ok: true, mergeRequestUrl: url, pushed: true };
|
|
124
121
|
}
|
|
125
122
|
catch (e) {
|
|
126
123
|
const msg = e instanceof Error ? e.message : String(e);
|
|
127
124
|
log.error(`Could not create merge request: ${msg}`);
|
|
128
|
-
await upsertGitContext(opts.taskFile, { ...gitCtx, pushedAt });
|
|
129
125
|
log.info(dim(`Branch was pushed. Create MR manually: ${gitCtx.taskBranch} → ${gitCtx.baseBranch}`));
|
|
130
126
|
return { ok: false, pushed: true };
|
|
131
127
|
}
|
package/dist/lib/task-reship.js
CHANGED
|
@@ -2,9 +2,10 @@ import { resolve } from "node:path";
|
|
|
2
2
|
import { GitConfigError, requireGitConfig } from "./git-config.js";
|
|
3
3
|
import { gitBranchExists, gitCheckout, gitCheckoutNewBranch, gitFetchRemote, gitGovernanceOnlyDirty, gitHeadCommit, gitMergeRemoteBranch, gitMergeInProgress, gitPullFastForwardOnly, gitRemoteBranchExists, gitSwitchToBranch, listUnmergedRelPaths, readGitInfo, } from "./git.js";
|
|
4
4
|
import { dim, log } from "./logger.js";
|
|
5
|
-
import {
|
|
5
|
+
import { findMergeRequestByBranches } from "./pr-create.js";
|
|
6
6
|
import { normalizeTaskRef } from "./resolve-task.js";
|
|
7
|
-
import {
|
|
7
|
+
import { getTaskMergeRequestLifecycle, resolveGitHostForCwd, } from "./task-effective-status.js";
|
|
8
|
+
import { findTaskFile, loadActionableTasks, pickLatestShippedTask, readGitContext, readTaskFile, upsertGitContext, } from "./task.js";
|
|
8
9
|
export async function resolveReshipTarget(paths, cwd, taskRef) {
|
|
9
10
|
const trimmed = taskRef?.trim();
|
|
10
11
|
let taskId;
|
|
@@ -160,7 +161,7 @@ export async function integrateIntegrationIntoTaskBranch(opts) {
|
|
|
160
161
|
export async function assertMergeRequestNotOpen(cwd, taskFile, allowOpenMr) {
|
|
161
162
|
if (allowOpenMr)
|
|
162
163
|
return true;
|
|
163
|
-
const state = await
|
|
164
|
+
const state = await getTaskMergeRequestLifecycle(cwd, taskFile);
|
|
164
165
|
if (state !== "open")
|
|
165
166
|
return true;
|
|
166
167
|
log.error("Current MR/PR is still open. Merge or close it first, or rerun with --allow-open-mr.");
|
|
@@ -172,8 +173,32 @@ export async function refreshGitContextBaseCommit(taskFile, gitCtx, cwd) {
|
|
|
172
173
|
return gitCtx;
|
|
173
174
|
return { ...gitCtx, baseCommit: head };
|
|
174
175
|
}
|
|
175
|
-
export async function prepareGitContextForReship(taskFile, gitCtx) {
|
|
176
|
-
const
|
|
176
|
+
export async function prepareGitContextForReship(cwd, taskFile, gitCtx) {
|
|
177
|
+
const previous = [...(gitCtx.previousMergeRequestUrls ?? [])];
|
|
178
|
+
const host = await resolveGitHostForCwd(cwd);
|
|
179
|
+
if (host !== null) {
|
|
180
|
+
const open = await findMergeRequestByBranches({
|
|
181
|
+
cwd,
|
|
182
|
+
host,
|
|
183
|
+
headBranch: gitCtx.taskBranch,
|
|
184
|
+
baseBranch: gitCtx.baseBranch,
|
|
185
|
+
state: "open",
|
|
186
|
+
});
|
|
187
|
+
if (open !== null) {
|
|
188
|
+
previous.push(open.url);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const legacy = gitCtx.mergeRequestUrl?.trim();
|
|
192
|
+
if (legacy !== undefined && legacy.length > 0 && !previous.includes(legacy)) {
|
|
193
|
+
previous.push(legacy);
|
|
194
|
+
}
|
|
195
|
+
const archived = {
|
|
196
|
+
...gitCtx,
|
|
197
|
+
previousMergeRequestUrls: previous.length > 0 ? previous : undefined,
|
|
198
|
+
mergeRequestUrl: undefined,
|
|
199
|
+
reshipCount: (gitCtx.reshipCount ?? 0) + 1,
|
|
200
|
+
lastReshipAt: new Date().toISOString(),
|
|
201
|
+
};
|
|
177
202
|
await upsertGitContext(taskFile, archived);
|
|
178
203
|
return archived;
|
|
179
204
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodtek/vibeops",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3",
|
|
4
4
|
"description": "Minimal CLI rails for Cursor-based TASK-driven vibe coding.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"packageManager": "pnpm@9.15.9",
|
|
6
7
|
"author": "VibeOps contributors",
|
|
7
8
|
"license": "MIT",
|
|
8
9
|
"repository": {
|
|
@@ -29,6 +30,16 @@
|
|
|
29
30
|
"engines": {
|
|
30
31
|
"node": ">=20"
|
|
31
32
|
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"dev": "tsx src/cli.ts",
|
|
35
|
+
"build": "tsc -p tsconfig.json",
|
|
36
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
37
|
+
"start": "node dist/cli.js",
|
|
38
|
+
"smoke": "pnpm typecheck && pnpm build && node scripts/smoke.mjs",
|
|
39
|
+
"clean": "rm -rf dist",
|
|
40
|
+
"prepack": "pnpm clean && pnpm build",
|
|
41
|
+
"publish:dry": "pnpm publish --dry-run --no-git-checks"
|
|
42
|
+
},
|
|
32
43
|
"keywords": [
|
|
33
44
|
"cursor",
|
|
34
45
|
"ai",
|
|
@@ -47,14 +58,5 @@
|
|
|
47
58
|
"@types/node": "^20.14.0",
|
|
48
59
|
"tsx": "^4.19.0",
|
|
49
60
|
"typescript": "^5.5.4"
|
|
50
|
-
},
|
|
51
|
-
"scripts": {
|
|
52
|
-
"dev": "tsx src/cli.ts",
|
|
53
|
-
"build": "tsc -p tsconfig.json",
|
|
54
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
55
|
-
"start": "node dist/cli.js",
|
|
56
|
-
"smoke": "pnpm typecheck && pnpm build && node scripts/smoke.mjs",
|
|
57
|
-
"clean": "rm -rf dist",
|
|
58
|
-
"publish:dry": "pnpm publish --dry-run --no-git-checks"
|
|
59
61
|
}
|
|
60
|
-
}
|
|
62
|
+
}
|