@goodtek/vibeops 2.3.0 → 2.4.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,26 @@ All notable changes to VibeOps are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 2.4.0 - 2026-06-12
8
+
9
+ ### Changed
10
+
11
+ - **`vibeops task ship` is now state-aware.** It detects the TASK's PR/MR state and branches:
12
+ 1. **No PR** → push + open a new PR (first submit, Status → Shipped).
13
+ 2. **Open PR (unmerged)** → commit + push the **same** branch to update the existing PR (no new PR); CI re-runs. This replaces the manual `git add/commit/push` loop.
14
+ 3. **Merged PR** → start a **new** PR cycle (former `reship`), guarded by an interactive confirm (`This starts a NEW PR cycle after merge. Continue? (y/N)`) or `--new-cycle` in non-interactive mode.
15
+ - Every path prints a one-line summary: `Created PR #<n> → <url>`, `Updated existing PR #<n> (pushed <sha>) — CI re-running`, `Started new PR cycle → PR #<n>`, or `Nothing to ship (working tree clean, PR #<n> up to date)`.
16
+ - Commit messages are auto-scoped with the TASK id (`feat(task-001): …`). `ship` refuses when HEAD is not the TASK branch.
17
+ - PR/MR state resolution is unified in a single source (`resolveTaskMergeRequestLifecycle`).
18
+
19
+ ### Added
20
+
21
+ - **`vibeops task ship`** flags: `-m/--message <msg>` (TASK id auto-prefixed; LLM-drafted or prompted when omitted), `--new-cycle` (alias `--reship`), `--no-commit` (push committed history only), `--non-interactive`.
22
+
23
+ ### Deprecated
24
+
25
+ - **`vibeops task reship`** is now a thin alias for `vibeops task ship --new-cycle` and prints a deprecation notice.
26
+
7
27
  ## 2.3.0 - 2026-06-09
8
28
 
9
29
  ### Added
package/README.md CHANGED
@@ -14,8 +14,8 @@ VibeOps bootstraps an **agent-friendly repo**, starts numbered **TASK** files on
14
14
  | `vibeops init` | Core docs + agent packs (cursor / claude / codex) |
15
15
  | `vibeops task add` | New `TASK-NNN` file + task branch |
16
16
  | `vibeops task del` | Cancel TASK before merge (md + branch + close open MR) |
17
- | `vibeops task ship` | LLM summary, commit, push, open MR/PR (Status → **Shipped**) |
18
- | `vibeops task reship` | Shipped follow-up: move uncommitted work to task branch, commit, new MR/PR (Status stays Shipped) |
17
+ | `vibeops task ship` | State-aware submit: open a new PR · update the open PR · start a new PR cycle after merge (Status → **Shipped**) |
18
+ | `vibeops task reship` | **Deprecated** alias for `task ship --new-cycle` |
19
19
  | `vibeops task merge` | Merge TASK MR/PR into integration branch (default: squash) |
20
20
  | `vibeops task sync` | After merge: ff-only integration pull, delete task branches (no TASK md edits) |
21
21
  | `vibeops task release` | Release PR: integration → production (GitFlow) |
@@ -56,13 +56,24 @@ vibeops task add
56
56
  vibeops task ship
57
57
  vibeops task merge
58
58
  vibeops pull
59
- # Follow-up on same TASK (after merge):
60
- vibeops task reship TASK-NNN
59
+ # Same TASK, before merge — edit, then just re-run ship (updates the open PR):
60
+ vibeops task ship -m "address review"
61
+ # Same TASK, after merge — start a new PR cycle:
62
+ vibeops task ship --new-cycle
61
63
  vibeops task merge
62
64
  vibeops task sync
63
65
  vibeops task add
64
66
  ```
65
67
 
68
+ `ship` detects the TASK's PR state and does the right thing:
69
+
70
+ | State | `ship` does | Output |
71
+ |-------|-------------|--------|
72
+ | No PR yet | push + open a new PR | `Created PR #<n> → <url>` |
73
+ | PR open (unmerged) | commit + push the **same** branch (no new PR), CI re-runs | `Updated existing PR #<n> (pushed <sha>) — CI re-running` |
74
+ | PR merged | start a **new** PR cycle (needs confirm or `--new-cycle`) | `Started new PR cycle → PR #<n>` |
75
+ | No change | no-op | `Nothing to ship (working tree clean, PR #<n> up to date)` |
76
+
66
77
  Occasionally (GitFlow release to production):
67
78
 
68
79
  ```bash
@@ -73,7 +84,7 @@ Only one TASK **In Progress** at a time (`task add` blocks otherwise). **Shipped
73
84
 
74
85
  ## LLM (optional)
75
86
 
76
- For **`task add`** / **`task ship`** / **`task reship`** only (not for coding in the IDE):
87
+ For **`task add`** / **`task ship`** only (not for coding in the IDE). When `-m` is omitted, `ship` uses a connected provider to draft the commit subject:
77
88
 
78
89
  ```bash
79
90
  vibeops llm connect
@@ -99,8 +110,8 @@ pnpm install && pnpm build && pnpm smoke
99
110
  - **`init`**: `--clients`, `--yes`, `--dry-run`, `--force`, `--git`, `--initial-commit`, `--git-policy gitflow|trunk`, `--integration-branch`, `--production-branch`, `--allow-no-remote`, `--cwd`
100
111
  - **`task add`**: `--dry-run`, `--non-interactive --idea "…"`
101
112
  - **`task del`**: `--dry-run`, `--force`, `--no-remote-delete`, `--no-close-mr`
102
- - **`task ship`**: `--dry-run`, `--no-pr`
103
- - **`task reship`**: `--dry-run`, `--no-pr`, `--no-integrate`, `--recreate-branch`, `--skip-llm`, `--allow-open-mr`
113
+ - **`task ship`**: `-m/--message`, `--new-cycle` (alias `--reship`), `--no-commit`, `--dry-run`, `--no-pr`, `--non-interactive`, `--allow-open-mr`, `--no-integrate`, `--recreate-branch`, `--skip-llm`
114
+ - **`task reship`** (deprecated): `--dry-run`, `--no-pr`, `--no-integrate`, `--recreate-branch`, `--skip-llm`, `--allow-open-mr` → delegates to `task ship --new-cycle`
104
115
  - **`task merge`**: `--dry-run`, `--merge`, `--rebase`
105
116
  - **`task sync`**: `--dry-run`, `--no-remote-delete`, `--force`
106
117
  - **`task release`**: `--dry-run`, `--no-merge`, `--merge`, `--rebase`
@@ -111,8 +122,8 @@ pnpm install && pnpm build && pnpm smoke
111
122
 
112
123
  - **Init** records branch policy in `.vibeops.json` (`integrationBranch`, `productionBranch`, `host`).
113
124
  - **`task add`**: `task/<slug>` from **integration** (e.g. `develop`).
114
- - **`task ship`**: commit implementation + ship metadata (Status **Shipped**) → push once → MR/PR (URL on host; `task merge` resolves by branch).
115
- - **`task reship`**: Shipped TASK follow-up — carries uncommitted work onto the task branch (creates it from integration when missing after sync), integrates `develop`, commit → push → **new** PR.
125
+ - **`task ship`** (state-aware): **no PR** → commit + ship metadata (Status **Shipped**) → push → open MR/PR; **open PR** → commit + push the same branch, CI re-runs, no new PR; **merged PR** → new PR cycle (carries uncommitted work onto the task branch, integrates `develop`, opens a **new** PR). Commit messages are TASK-id-scoped (`feat(task-001): …`). Refuses when HEAD is not the task branch.
126
+ - **`task reship`** *(deprecated)*: alias for `task ship --new-cycle`.
116
127
  - **`task merge`**: merge MR/PR into integration (CLI or host UI; TASK md unchanged).
117
128
  - **`task sync`**: integration ff-only pull → delete `task/*` branches (TASK md unchanged).
118
129
  - **`pull`**: fetch + switch to integration branch + `git pull --ff-only` (one command).
package/dist/cli.js CHANGED
@@ -96,20 +96,37 @@ task
96
96
  });
97
97
  task
98
98
  .command("ship [taskRef]")
99
- .description("Submit TASK: LLM summary, commit, push, open MR/PR (Status → Shipped)")
99
+ .description("State-aware submit: new PR · update open PR · new PR cycle after merge (Status → Shipped)")
100
100
  .option("--dry-run", "Plan only")
101
+ .option("-m, --message <msg>", "Commit message (TASK id auto-prefixed; LLM/prompt when omitted)")
102
+ .option("--new-cycle", "Allow a new PR cycle after merge without prompting (alias: --reship)")
103
+ .option("--reship", "Alias for --new-cycle")
104
+ .option("--no-commit", "Push already-committed changes only (skip staging/commit)")
101
105
  .option("--no-pr", "Push only; skip creating MR/PR")
106
+ .option("--non-interactive", "CI: never prompt")
107
+ .option("--no-integrate", "New PR cycle: skip merging integration into the task branch")
108
+ .option("--recreate-branch", "New PR cycle: recreate task branch from integration")
109
+ .option("--skip-llm", "New PR cycle: do not run LLM for Result / Test Result")
110
+ .option("--allow-open-mr", "Update the open MR/PR with new commits instead of a new cycle")
102
111
  .option("--cwd <path>", "Target directory")
103
112
  .action(async (taskRef, opts) => {
104
113
  await taskShipCommand(taskRef, {
105
114
  dryRun: Boolean(opts.dryRun),
106
- noPr: Boolean(opts.noPr),
115
+ message: opts.message,
116
+ newCycle: Boolean(opts.newCycle) || Boolean(opts.reship),
117
+ noCommit: opts.commit === false,
118
+ noPr: opts.pr === false,
119
+ nonInteractive: Boolean(opts.nonInteractive),
120
+ noIntegrate: opts.integrate === false,
121
+ recreateBranch: Boolean(opts.recreateBranch),
122
+ skipLlm: Boolean(opts.skipLlm),
123
+ allowOpenMr: Boolean(opts.allowOpenMr),
107
124
  cwd: opts.cwd,
108
125
  });
109
126
  });
110
127
  task
111
128
  .command("reship [taskRef]")
112
- .description("Follow-up on Shipped TASK: move uncommitted work to task branch, commit, new MR/PR")
129
+ .description("Deprecated alias for `task ship --new-cycle` (start a new PR cycle after merge)")
113
130
  .option("--dry-run", "Plan only")
114
131
  .option("--no-pr", "Push only; skip creating MR/PR")
115
132
  .option("--no-integrate", "Skip merging integration branch into task branch")
@@ -120,8 +137,8 @@ task
120
137
  .action(async (taskRef, opts) => {
121
138
  await taskReshipCommand(taskRef, {
122
139
  dryRun: Boolean(opts.dryRun),
123
- noPr: Boolean(opts.noPr),
124
- noIntegrate: Boolean(opts.noIntegrate),
140
+ noPr: opts.pr === false,
141
+ noIntegrate: opts.integrate === false,
125
142
  recreateBranch: Boolean(opts.recreateBranch),
126
143
  skipLlm: Boolean(opts.skipLlm),
127
144
  allowOpenMr: Boolean(opts.allowOpenMr),
@@ -1,175 +1,21 @@
1
- import { resolve } from "node:path";
2
- import { readText } from "../lib/filesystem.js";
3
- import { bold, cyan, dim, green, log, yellow } from "../lib/logger.js";
4
- import { projectPaths } from "../lib/paths.js";
5
- import { relPath } from "../lib/task-context.js";
6
- import { commitDirtyWorkingTree, docsCommitMessageFor, featCommitMessageFor, } from "../lib/task-git-commit.js";
7
- import { finishTaskWithPullRequest } from "../lib/task-push-pr.js";
8
- import { applyTaskShipMemory, llmCompleteTaskReship, writeTaskResultSections, } from "../lib/task-ship-llm.js";
9
- import { assertMergeRequestNotOpen, ensureTaskBranchForReship, integrateIntegrationIntoTaskBranch, loadGitConfigForReship, prepareGitContextForReship, refreshGitContextBaseCommit, resolveReshipTarget, } from "../lib/task-reship.js";
10
- import { hasNonEmptySection, parseTaskFilename, readGitContext, } from "../lib/task.js";
11
- import { listWorkingTreeRelPaths } from "../lib/git.js";
12
- async function commitReshipMetadata(opts) {
13
- if (opts.dryRun) {
14
- log.info(dim(" would commit reship metadata (Status stays Shipped)"));
15
- return;
16
- }
17
- const committed = await commitDirtyWorkingTree(opts.cwd, docsCommitMessageFor(opts.taskId, "reship metadata"), false);
18
- if (!committed) {
19
- log.info(dim("No reship metadata commit (Git Context already on disk)."));
20
- }
21
- }
1
+ import { dim, log, yellow } from "../lib/logger.js";
2
+ import { taskShipCommand } from "./task-ship.js";
3
+ /**
4
+ * @deprecated `reship` is now `vibeops task ship --new-cycle`.
5
+ * Kept as a thin alias for backward compatibility.
6
+ */
22
7
  export async function taskReshipCommand(taskRef, options = {}) {
23
- const cwd = resolve(options.cwd ?? process.cwd());
24
- const paths = projectPaths(cwd);
25
- const dryRun = options.dryRun === true;
26
- const target = await resolveReshipTarget(paths, cwd, taskRef);
27
- if (target === null) {
28
- process.exitCode = 1;
29
- return;
30
- }
31
- const { taskId, taskFile, meta, gitCtx: initialCtx } = target;
32
- const relFile = relPath(cwd, taskFile);
33
- log.info(bold(`vibeops task reship ${taskId}`));
34
- log.info(` ${dim("file")} ${relFile}`);
35
- log.info(` ${dim("branch")} ${initialCtx.taskBranch}`);
36
- log.info(` ${dim("status")} Shipped (unchanged)`);
37
- log.blank();
38
- const gitCfg = dryRun
39
- ? await loadGitConfigForReship(cwd).catch(() => null)
40
- : await loadGitConfigForReship(cwd);
41
- if (!dryRun && gitCfg === null) {
42
- process.exitCode = 1;
43
- return;
44
- }
45
- const integrationBranch = gitCfg?.integrationBranch ?? "develop";
46
- const remote = gitCfg?.remote ?? "origin";
47
- const updateOpenMr = options.allowOpenMr === true;
48
- if (!dryRun && !(await assertMergeRequestNotOpen(cwd, taskFile, updateOpenMr))) {
49
- process.exitCode = 1;
50
- return;
51
- }
52
- const branchOk = await ensureTaskBranchForReship({
53
- cwd,
54
- taskFile,
55
- gitCtx: initialCtx,
56
- integrationBranch,
57
- remote,
58
- recreateBranch: options.recreateBranch === true,
59
- dryRun,
60
- });
61
- if (!branchOk) {
62
- process.exitCode = 1;
63
- return;
64
- }
65
- if (options.noIntegrate !== true) {
66
- const integrated = await integrateIntegrationIntoTaskBranch({
67
- cwd,
68
- integrationBranch,
69
- remote,
70
- dryRun,
71
- });
72
- if (!integrated) {
73
- process.exitCode = 1;
74
- return;
75
- }
76
- }
77
- else {
78
- log.info(dim("Skipped integration merge (--no-integrate)."));
79
- }
80
- const body = await readText(taskFile);
81
- let resultOk = hasNonEmptySection(body, "Result");
82
- let testOk = hasNonEmptySection(body, "Test Result");
83
- if (!options.skipLlm && !dryRun) {
84
- log.step("LLM — follow-up Result / Test Result");
85
- const patch = await llmCompleteTaskReship({
86
- cwd,
87
- taskId,
88
- taskTitle: meta.title,
89
- taskBody: body,
90
- taskFileRel: relFile,
91
- });
92
- if (patch !== null) {
93
- await writeTaskResultSections(taskFile, patch.result, patch.testResult);
94
- log.ok(`Result / Test Result updated (${patch.provider})`);
95
- const memory = await applyTaskShipMemory(cwd, patch);
96
- for (const p of memory.updated)
97
- log.ok(`Updated ${p}`);
98
- for (const p of memory.skipped)
99
- log.skip(`Skipped ${p}`);
100
- }
101
- else {
102
- log.warn("LLM unavailable — ensure Result / Test Result describe this follow-up.");
103
- }
104
- const body2 = await readText(taskFile);
105
- resultOk = hasNonEmptySection(body2, "Result");
106
- testOk = hasNonEmptySection(body2, "Test Result");
107
- }
108
- else if (options.skipLlm) {
109
- log.info(dim("LLM skipped (--skip-llm)."));
110
- }
111
- log.info(bold("Sections"));
112
- log.info(` ${resultOk ? green("✓") : yellow("·")} Result`);
113
- log.info(` ${testOk ? green("✓") : yellow("·")} Test Result`);
8
+ log.warn(`${yellow("reship is now `ship` (deprecated alias)")} — delegating to \`vibeops task ship --new-cycle\`.`);
9
+ log.info(dim("Run `vibeops task ship` directly; it detects first submit / open-PR update / new PR cycle."));
114
10
  log.blank();
115
- if (dryRun) {
116
- log.info(bold("dry-run — would also:"));
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
- }
123
- log.blank();
124
- log.info(`Next: ${cyan("vibeops task merge")}, then optional ${cyan("task sync")}`);
125
- return;
126
- }
127
- if (!resultOk || !testOk) {
128
- log.error("Fill Result and Test Result for this follow-up, then rerun.");
129
- process.exitCode = 1;
130
- return;
131
- }
132
- const dirtyBefore = await listWorkingTreeRelPaths(cwd);
133
- if (dirtyBefore.length === 0) {
134
- log.error("No changes to reship. Edit files (e.g. on develop), then rerun.");
135
- process.exitCode = 1;
136
- return;
137
- }
138
- const parts = parseTaskFilename(taskFile);
139
- await commitDirtyWorkingTree(cwd, featCommitMessageFor(parts.id, meta.title), false);
140
- let gitCtx = await readGitContext(taskFile);
141
- if (gitCtx === null) {
142
- log.error("Git Context missing after commit.");
143
- process.exitCode = 1;
144
- return;
145
- }
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
- }
159
- const prResult = await finishTaskWithPullRequest({
160
- cwd,
161
- taskFile,
162
- skipPr: options.noPr === true,
163
- forceNewMergeRequest: !updateOpenMr,
11
+ await taskShipCommand(taskRef, {
12
+ dryRun: options.dryRun,
13
+ cwd: options.cwd,
14
+ noPr: options.noPr,
15
+ newCycle: true,
16
+ noIntegrate: options.noIntegrate,
17
+ recreateBranch: options.recreateBranch,
18
+ skipLlm: options.skipLlm,
19
+ allowOpenMr: options.allowOpenMr,
164
20
  });
165
- if (!prResult.ok) {
166
- log.error("Reship failed — fix push/MR, then rerun task reship.");
167
- process.exitCode = 1;
168
- return;
169
- }
170
- if (prResult.mergeRequestUrl) {
171
- log.info(` ${dim("MR/PR")} ${prResult.mergeRequestUrl}`);
172
- }
173
- log.blank();
174
- log.info(`Next: ${cyan("vibeops task merge")} (or merge in the host UI), then ${cyan("vibeops task sync")}`);
175
21
  }